First commit, things mostly work locally, on linux x86

This commit is contained in:
Nathan Anderson 2023-10-09 22:54:00 -06:00
commit 86bce941de
7 changed files with 807 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
zig-cache/
zig-out/

2
.ignore Normal file
View File

@ -0,0 +1,2 @@
zig-cache/
zig-out/

70
build.zig Normal file
View File

@ -0,0 +1,70 @@
const std = @import("std");
// Although this function looks imperative, note that its job is to
// declaratively construct a build graph that will be executed by an external
// runner.
pub fn build(b: *std.Build) void {
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{});
// Standard optimization options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
// set a preferred release mode, allowing the user to decide how to optimize.
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "zim",
// In this case the main source file is merely a path, however, in more
// complicated build scripts, this could be a generated file.
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
// This declares intent for the executable to be installed into the
// standard location when the user invokes the "install" step (the default
// step when running `zig build`).
b.installArtifact(exe);
// This *creates* a Run step in the build graph, to be executed when another
// step is evaluated that depends on it. The next line below will establish
// such a dependency.
const run_cmd = b.addRunArtifact(exe);
// By making the run step depend on the install step, it will be run from the
// installation directory rather than directly from within the cache directory.
// This is not necessary, however, if the application depends on other installed
// files, this ensures they will be present and in the expected location.
run_cmd.step.dependOn(b.getInstallStep());
// This allows the user to pass arguments to the application in the build
// command itself, like this: `zig build run -- arg1 arg2 etc`
if (b.args) |args| {
run_cmd.addArgs(args);
}
// This creates a build step. It will be visible in the `zig build --help` menu,
// and can be selected like this: `zig build run`
// This will evaluate the `run` step rather than the default, which is "install".
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
// Creates a step for unit testing. This only builds the test executable
// but does not run it.
const unit_tests = b.addTest(.{
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
const run_unit_tests = b.addRunArtifact(unit_tests);
// Similar to creating the run step earlier, this exposes a `test` step to
// the `zig build --help` menu, providing a way for the user to request
// running the unit tests.
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_unit_tests.step);
}

237
src/main.zig Normal file
View File

@ -0,0 +1,237 @@
const std = @import("std");
const zim = @import("zim.zig");
const utils = @import("utils.zig");
const string = []const u8;
pub const ZIM_MAJOR_VER = 0;
pub const ZIM_MINOR_VER = 0;
pub const ZIM_BUILD = 1;
/// The availble arguments for the ZiM program
const Args = enum(u8) {
help,
install,
list,
use,
gir,
version,
};
/// A type of argument for the ZiM program
const ArgType = enum {
single,
expectsOneParam,
};
/// Allocates a string with the version of Zim, call `allocator.free()` with
/// the string when done using it.
pub fn versionStr(allocator: std.mem.Allocator) ![]const u8 {
var buf = try allocator.alloc(u8, 10);
_ = try std.fmt.bufPrint(buf, "{d}.{d}.{d}", .{ ZIM_MAJOR_VER, ZIM_MINOR_VER, ZIM_BUILD });
return buf;
}
/// Returns what each argument expects to follow it
fn get_arg_parse_type(arg: Args) ArgType {
return switch (arg) {
.help => ArgType.single,
.install => ArgType.expectsOneParam,
.list => ArgType.expectsOneParam,
.use => ArgType.expectsOneParam,
.gir => ArgType.single,
.version => ArgType.single,
};
}
/// For a given `Args`, returns a string description of what each does
fn command_desc(arg: Args) string {
return switch (arg) {
.help => " help\t\tDisplays this help message",
.install => " install\tInstall the zig version",
.list => " list\t\tLists all versions available on the system",
.use => " use\t\tSets the active version of zig to use",
.version => " version\t\tPrint out the zim version you are using",
.gir => " gir\t\tPrint out ascii Gir",
};
}
/// Shows the main help menu for the program
fn show_help() void {
std.debug.print("Welcome to the ZIg version Manager (ZIM)\nHelp Menu:\n\n", .{});
inline for (@typeInfo(Args).Enum.fields) |arg_type| {
const tag = utils.nameToEnumTag(arg_type.name, Args) catch {
unreachable;
};
std.debug.print("{s}\n", .{command_desc(tag)});
}
std.debug.print("\n", .{});
}
/// Shows a help menu for a specific `Args` action, like a help sub-menu
fn show_context_help(arg: Args) void {
switch (arg) {
.install => {
std.debug.print(
\\zim install <version>
\\ Used to install zig versions, that will then be available to
\\ the `zim use <version>` command.
\\
\\ To see all available versions to install, run `zim list global`
\\
\\
, .{});
},
.list => {
std.debug.print(
\\zim list <local or global>
\\ Used to list zig versions, either locally or remotely available.
\\ Running zim global will make a network request to fetch the latest
\\ versions of zig.
\\
\\
, .{});
},
.use => {
std.debug.print(
\\zim use <version>
\\ Used to switch the actively used zig version on the system. If
\\ your system PATH environment is set to use `~/.config/zim/zig`.
\\ If your system is not setup, you can run `zim init` and cross
\\ your fingers that it supports your setup.
\\
\\
, .{});
},
.gir, .version, .help => {
return;
},
}
}
/// Given `arg`, does the action with the provided optional `param`
fn do_arg_action(allocator: std.mem.Allocator, arg: Args, param: ?[]const u8) void {
switch (arg) {
.help => {
show_help();
},
.gir => {
utils.printGir();
},
.version => {
const ver = versionStr(allocator) catch {
std.debug.print("Error getting version\n", .{});
return;
};
defer allocator.free(ver);
if (param) |command| {
std.debug.print("{s}\nRunning ZiM Version {s}\n", .{ command, ver });
} else {
std.debug.print("Running ZiM Version {s}\n", .{ver});
}
utils.printGir();
},
.install => {
if (param == null) {
std.debug.print("Error: Expected version to follow\n\n", .{});
std.os.exit(1);
return;
}
const p = param.?;
if (std.mem.eql(u8, p, "help")) {
show_context_help(arg);
std.os.exit(0);
return;
}
zim.install(allocator, param.?) catch |err| {
std.debug.print("Error running `install {s}`\n{any}", .{ param.?, err });
std.os.exit(1);
};
},
.list => {
if (param == null) {
std.debug.print("Error: Expected `local` or `global` to follow.\nRun `zim list help` for details.\n\n", .{});
std.os.exit(1);
}
const p = param.?;
if (std.mem.eql(u8, p, "help")) {
show_context_help(arg);
std.os.exit(0);
return;
}
if (!std.mem.eql(u8, p, "local") and !std.mem.eql(u8, p, "global")) {
std.debug.print("Error: List available versions either `list local` or `list global`\n\n", .{});
show_context_help(arg);
std.os.exit(1);
}
zim.list(allocator, param.?) catch |err| {
std.debug.print("Error running `list {s}`\n{any}", .{ param.?, err });
std.os.exit(1);
};
},
.use => {
if (param == null) {
std.debug.print("Error: Expected version to follow\n\n", .{});
std.os.exit(1);
}
const p = param.?;
if (std.mem.eql(u8, p, "help")) {
show_context_help(arg);
std.os.exit(0);
return;
}
zim.use(allocator, param.?) catch |err| {
std.debug.print("Error running `use {s}`\n{any}", .{ param.?, err });
std.os.exit(1);
};
},
}
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var args_iter = try std.process.argsWithAllocator(allocator);
defer args_iter.deinit();
// var parsed_args = std.ArrayList(string).init(allocator);
// defer parsed_args.deinit();
const valid = arg_loop: {
var index: u8 = 0;
const command = args_iter.next();
while (args_iter.next()) |arg| {
index += 1;
const arg_tag = utils.nameToEnumTag(arg, Args) catch {
break :arg_loop false;
};
var param: ?[]const u8 = null;
if (get_arg_parse_type(arg_tag) == ArgType.expectsOneParam) {
param = args_iter.next();
}
if (arg_tag == .version) {
do_arg_action(
allocator,
arg_tag,
command,
);
break;
}
do_arg_action(allocator, arg_tag, param);
break;
}
if (index == 0) {
show_help();
}
break :arg_loop true;
};
if (!valid) {
std.debug.print("Something is not right with the command, try running `zim help` for details.\n", .{});
std.os.exit(1);
}
}

47
src/utils.zig Normal file
View File

@ -0,0 +1,47 @@
const std = @import("std");
const enumError = error{EnumFieldNotFound};
pub fn nameToEnumTag(name: []const u8, comptime Enum: type) enumError!Enum {
comptime {
if (@typeInfo(Enum) != .Enum) {
@compileError("Non enum type passed into function: " ++ @typeName(Enum));
}
}
inline for (@typeInfo(Enum).Enum.fields) |field| {
if (std.mem.eql(u8, name, field.name)) {
const tag: Enum = @enumFromInt(field.value);
return tag;
}
}
return enumError.EnumFieldNotFound;
}
test {
const State = enum {
good,
bad,
};
const enum_tag = try nameToEnumTag("good", State);
try std.testing.expect(enum_tag == State.good);
}
pub fn printGir() void {
std.debug.print(
\\ n n
\\ o ( )
\\ ___/_ / I am \
\\ / / . o ( your )
\\ 0> 0 / \ Doom! /
\\ Lu__/ ( )
\\ || - u -
\\ o^ ^ o
\\ _/ / / \_
\\ |__|
\\ O=> (O
\\
\\
, .{});
}

101
src/version.zig Normal file
View File

@ -0,0 +1,101 @@
const std = @import("std");
const string = []const u8;
pub const ZigVersion = struct {
allocator: std.mem.Allocator,
version_string: string,
tarball_url: ?string,
src_tarball_url: ?string,
platform_string: ?string,
docs_url: ?string,
pub fn init(allocator: std.mem.Allocator, scanner: *std.json.Scanner, ver_string: string) ZigVersion {
var version_string = allocator.dupe(u8, ver_string) catch unreachable;
var docs_url: ?string = null;
var tarball_url: ?string = null;
var platform_string: ?string = null;
var src_tarball_url: ?string = null;
var scanning = true;
while (scanning) {
if (scanner.*.next()) |token| {
switch (token) {
.object_begin => {},
.object_end => {
const t_type = scanner.*.peekNextTokenType() catch {
unreachable;
};
if (t_type == std.json.TokenType.object_end) {
scanning = false;
}
},
.string => {
if (std.mem.eql(u8, token.string, "docs")) {
const docs_token = scanner.*.next() catch unreachable;
docs_url = allocator.dupe(u8, docs_token.string) catch unreachable;
} else if (std.mem.eql(u8, token.string, "src")) {
// ignore the object begin
checkNextToken(scanner, .object_begin);
_ = scanner.*.next() catch unreachable;
// ignore tarball string
checkNextToken(scanner, .string);
_ = scanner.*.next() catch unreachable;
const tar_token = scanner.*.next() catch unreachable;
src_tarball_url = allocator.dupe(u8, tar_token.string) catch unreachable;
} else if (std.mem.eql(u8, token.string, "x86_64-linux")) {
platform_string = allocator.dupe(u8, token.string) catch unreachable;
// ignore object begin
checkNextToken(scanner, .object_begin);
_ = scanner.*.next() catch unreachable;
checkNextToken(scanner, .string);
const tar_token = scanner.*.next() catch unreachable;
if (std.mem.eql(u8, tar_token.string, "tarball")) {
const tar_url_token = scanner.*.next() catch unreachable;
tarball_url = allocator.dupe(u8, tar_url_token.string) catch unreachable;
}
}
},
else => {
unreachable;
},
}
} else |err| {
std.debug.print("Error scanning in ZigVersion: {any}\n", .{err});
scanning = false;
unreachable;
}
}
return ZigVersion{ .allocator = allocator, .version_string = version_string, .tarball_url = tarball_url, .src_tarball_url = src_tarball_url, .platform_string = platform_string, .docs_url = docs_url };
}
pub fn deinit() void {}
pub fn fmtPrint(zv: *const ZigVersion) void {
std.debug.print(" zig-{s}\n", .{zv.version_string});
if (zv.platform_string) |plat| {
std.debug.print("\tplatform: {s}\n", .{plat});
}
if (zv.docs_url) |docs| {
std.debug.print("\tdocs_url: {s}\n", .{docs});
}
if (zv.tarball_url) |tar| {
std.debug.print("\ttar_url: {s}\n", .{tar});
}
if (zv.src_tarball_url) |tar| {
std.debug.print("\tsrc_tar_url: {s}\n", .{tar});
}
}
pub fn hasTar(zv: *ZigVersion) bool {
return zv.tarball_url != null;
}
};
fn checkNextToken(scanner: *std.json.Scanner, t_type: std.json.TokenType) void {
const t = scanner.*.peekNextTokenType() catch unreachable;
if (t != t_type) {
std.debug.print("Expected to find {any} but got {any}\n", .{ t_type, t });
unreachable;
}
}

348
src/zim.zig Normal file
View File

@ -0,0 +1,348 @@
const std = @import("std");
const utils = @import("utils.zig");
const version = @import("version.zig");
const string = []const u8;
const ZigVersion = version.ZigVersion;
const ZimError = error{
BadParameter,
MalformedJson,
NetworkError,
NotImplemented,
NoHomeConfigured,
Unexpected,
};
const BIN_SYM_ZIG_PATH = "bin/zig";
fn createSubDir(dir: std.fs.Dir, path: string) ZimError!void {
dir.makeDir(path) catch |err| blk: {
if (err == error.PathAlreadyExists) {
// std.debug.print("{s} already exists\n", .{path});
break :blk;
}
std.debug.print("Encountered error creating directory {s}: {any}\n", .{ path, err });
return ZimError.Unexpected;
};
}
fn openZimDir(allocator: std.mem.Allocator) ZimError!std.fs.Dir {
const home_dir = std.os.getenv("HOME");
if (home_dir == null) {
return ZimError.NoHomeConfigured;
}
// std.debug.print("Found home env to be: {s}\n", .{home_dir.?});
const zimPath = std.mem.concat(allocator, u8, &[_][]const u8{ home_dir.?, "/.config/zim" }) catch {
return ZimError.Unexpected;
};
defer allocator.free(zimPath);
// std.debug.print("Creating zim directory in {s}\n", .{zimPath});
std.fs.makeDirAbsolute(zimPath) catch |err| blk: {
if (err == error.PathAlreadyExists) {
// std.debug.print("Do not need to create new directory\n", .{});
break :blk;
}
std.debug.print("Error creating directory: {any}\n", .{err});
return ZimError.Unexpected;
};
var zim_dir = std.fs.openDirAbsolute(zimPath, .{}) catch {
// std.debug.print("Encountered error opening directory: {any}\n", .{err});
return ZimError.Unexpected;
};
try createSubDir(zim_dir, "bin");
try createSubDir(zim_dir, "versions");
return zim_dir;
}
fn getLocalVersionsList(
allocator: std.mem.Allocator,
zim_dir: std.fs.Dir,
) ZimError!std.ArrayList(string) {
var versions_dir = zim_dir.openIterableDir("versions", .{}) catch {
return ZimError.Unexpected;
};
defer versions_dir.close();
var ver_walker = versions_dir.walk(allocator) catch {
return ZimError.Unexpected;
};
defer ver_walker.deinit();
var versions_list = std.ArrayList(string).init(allocator);
var walking = true;
while (walking) blk: {
var ver = ver_walker.next() catch {
walking = false;
break :blk;
};
if (ver == null) {
walking = false;
break :blk;
}
// Dont recursively enter into any of the zig directories
if (!std.mem.eql(u8, ver.?.basename, ver.?.path)) {
break :blk;
}
if (ver.?.kind == .directory) {
const path = ver.?.path;
const start_name = path[0..3];
if (std.mem.eql(u8, start_name, "zig")) {
var p = allocator.dupe(u8, path) catch {
return ZimError.Unexpected;
};
versions_list.append(p) catch {
return ZimError.Unexpected;
};
}
}
// std.debug.print("Got entry in `versions:` base: {s}, path: {s}, kind: {any}\n", .{ ver.?.basename, ver.?.path, ver.?.kind });
}
return versions_list;
}
fn getRemoteVersionsList(allocator: std.mem.Allocator) ZimError!std.ArrayList(ZigVersion) {
const versions_json_url = "https://ziglang.org/download/index.json";
const versions_json_uri = std.Uri.parse(versions_json_url) catch {
return ZimError.Unexpected;
};
var headers = std.http.Headers{ .allocator = allocator };
defer headers.deinit();
// Accept anything.
headers.append("accept", "*/*") catch {
std.debug.print("Error adding headers\n", .{});
return ZimError.Unexpected;
};
var client = std.http.Client{ .allocator = allocator };
defer client.deinit();
var request = client.request(.GET, versions_json_uri, headers, .{}) catch {
std.debug.print("Error creating request\n", .{});
return ZimError.NetworkError;
};
defer request.deinit();
request.start() catch {
std.debug.print("Error starting request\n", .{});
return ZimError.NetworkError;
};
std.debug.print("Querying ziglang.org for latest zig versions...\n", .{});
request.wait() catch {
std.debug.print("Error waiting for request\n", .{});
return ZimError.NetworkError;
};
const sixty_four_kilobytes = 65536;
const body = request.reader().readAllAlloc(allocator, sixty_four_kilobytes) catch |err| {
std.debug.print("Error getting body: {any}\n", .{err});
return ZimError.NetworkError;
};
defer allocator.free(body);
var diag = std.json.Diagnostics{};
var body_scanner = std.json.Scanner.initCompleteInput(allocator, body);
body_scanner.enableDiagnostics(&diag);
var remote_zig_versions = std.ArrayList(ZigVersion).init(allocator);
var scanning = true;
var last_version: string = "";
while (scanning) {
if (body_scanner.next()) |token| {
switch (token) {
.object_begin => {
const zv = ZigVersion.init(allocator, &body_scanner, last_version);
remote_zig_versions.append(zv) catch {
return ZimError.Unexpected;
};
},
.end_of_document => {
scanning = false;
},
.string => {
last_version = token.string;
},
else => {},
}
} else |err| switch (err) {
error.SyntaxError => {
std.debug.print("Syntax error at line {d} col {d}\n", .{ diag.line_number, diag.getColumn() });
scanning = false;
},
else => {
std.debug.print("Got error {any}\n", .{err});
scanning = false;
},
}
}
return remote_zig_versions;
}
const ShellType = enum {
bash,
zsh,
};
fn shellName(s: ?ShellType) string {
comptime switch (s) {
.bash => "bash",
.zsh => "zsh",
};
}
fn printZimPathHelp(shell_tag: ?ShellType, zim_path: string) void {
if (shell_tag == undefined) {
std.debug.print("Unrecognized shell\n", .{});
return;
}
const t = "something";
_ = t;
switch (shell_tag.?) {
.bash => {
std.debug.print("\nDetected shell as {s}\n", .{@tagName(shell_tag.?)});
std.debug.print("For ZiM to work, you need to add\n'{s}'\nto your PATH. To do that, you can\nrun the following command:\n\n", .{zim_path});
std.debug.print("\techo \"export PATH={s}:$PATH\" >> ~/.zshrc\n\nOr edit your .zshrc to add it manually.", .{zim_path});
},
.zsh => {
std.debug.print("\nDetected shell as {s}\n", .{@tagName(shell_tag.?)});
std.debug.print("For ZiM to work, you need to add\n'{s}'\nto your PATH. To do that, you can\nrun the following command:\n\n", .{zim_path});
std.debug.print("\techo \"export PATH={s}:$PATH\" >> ~/.zshrc\n\nOr edit your .zshrc to add it manually.", .{zim_path});
},
}
}
pub fn install(allocator: std.mem.Allocator, param: string) ZimError!void {
_ = param;
var zim_dir = try openZimDir(allocator);
defer zim_dir.close();
return ZimError.NotImplemented;
}
pub fn use(allocator: std.mem.Allocator, param: string) ZimError!void {
var zim_dir = try openZimDir(allocator);
defer zim_dir.close();
var versions_list = try getLocalVersionsList(allocator, zim_dir);
defer versions_list.deinit();
const version_num = std.fmt.parseInt(u8, param, 0) catch null;
if (version_num != null) {
const num = version_num.?;
if (num > versions_list.items.len) {
std.debug.print("{d} exceeds the range of available versions: {d}\n", .{ num, versions_list.items.len });
return ZimError.BadParameter;
}
const ver_path = versions_list.items[version_num.? - 1];
std.debug.print("Using version {s}\n", .{ver_path});
const zim_path = zim_dir.realpathAlloc(allocator, "./") catch "err";
defer allocator.free(zim_path);
const target_version_path = std.mem.concat(allocator, u8, &[_][]const u8{ zim_path, "/versions/", ver_path }) catch {
return ZimError.Unexpected;
};
defer allocator.free(target_version_path);
zim_dir.deleteFile(BIN_SYM_ZIG_PATH) catch |err| {
if (err != error.FileNotFound) {
std.debug.print("Error deleting old link: {any}\n", .{err});
return ZimError.Unexpected;
}
};
std.debug.print("Creating sym link to {s}\n", .{target_version_path});
zim_dir.symLink(target_version_path, BIN_SYM_ZIG_PATH, .{ .is_directory = true }) catch |err| {
std.debug.print("Could not link new version: {any}\n", .{err});
return ZimError.Unexpected;
};
const path_environment = std.os.getenv("PATH");
if (path_environment) |path_env| {
if (!std.mem.containsAtLeast(u8, path_env, 1, zim_path)) {
var shell_tag: ?ShellType = null;
const shell_env = std.os.getenv("SHELL");
if (shell_env) |s| {
var split = std.mem.splitSequence(u8, s, "/");
var shell: []const u8 = "";
while (split.next()) |field| {
shell = field;
}
shell_tag = utils.nameToEnumTag(shell, ShellType) catch null;
}
const shell_bin_path =
std.mem.concat(allocator, u8, &[_][]const u8{ zim_path, "/", BIN_SYM_ZIG_PATH }) catch "<ZimERR>";
printZimPathHelp(
shell_tag,
shell_bin_path,
);
}
}
return;
} else {
std.debug.print("Using raw string version {s}\n", .{param});
return ZimError.NotImplemented;
}
// std.debug.print("Local Zig Versions:\n", .{});
// var i: u8 = 0;
// while (versions_list.popOrNull()) |version| {
// std.debug.print("\n [{d}]\tZig Version {s}", .{ i + 1, version });
// i += 1;
// }
}
const ZimListType = enum { local, global };
/// Runs the `list` subcommand
pub fn list(allocator: std.mem.Allocator, param: string) !void {
const list_type = utils.nameToEnumTag(param, ZimListType) catch {
std.debug.print("Unexpected parameter to `list`: {s}\n", .{param});
return ZimError.BadParameter;
};
switch (list_type) {
.local => {
var zim_dir = try openZimDir(allocator);
defer zim_dir.close();
var versions_list = try getLocalVersionsList(allocator, zim_dir);
defer versions_list.deinit();
std.debug.print("Local Zig Versions:\n", .{});
var i: u8 = 0;
while (versions_list.popOrNull()) |version_str| {
std.debug.print("\n [{d}]\tZig Version {s}", .{ i + 1, version_str });
i += 1;
}
std.debug.print("\n", .{});
std.debug.print(
\\
\\ Run `zim use` to select which version you want active in your environment.
\\ You can also just specify the index of the version you would like to use.
\\
, .{});
return;
},
.global => {
var remote_versions = try getRemoteVersionsList(allocator);
if (remote_versions.items.len == 0) {
std.debug.print("Failed to get remote versions\n", .{});
return ZimError.Unexpected;
}
std.debug.print("Retrieved remote zig versions:\n", .{});
for (remote_versions.items) |remote_version| {
remote_version.fmtPrint();
}
return;
},
}
}