From 6ea3710e4f2e7b5a19720b4aa829d468b2e6bfc5 Mon Sep 17 00:00:00 2001 From: Nathan Anderson Date: Tue, 17 Oct 2023 22:14:24 -0600 Subject: [PATCH] WIP remote install and list --- src/main.zig | 24 +++-- src/utils.zig | 23 ++-- src/version.zig | 162 +++++++++++++++++++++++----- src/zim.zig | 275 ++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 382 insertions(+), 102 deletions(-) diff --git a/src/main.zig b/src/main.zig index 8639573..4366388 100644 --- a/src/main.zig +++ b/src/main.zig @@ -11,6 +11,7 @@ pub const ZIM_BUILD = 1; /// The availble arguments for the ZiM program const Args = enum(u8) { help, + init, install, list, use, @@ -35,11 +36,12 @@ pub fn versionStr(allocator: std.mem.Allocator) ![]const u8 { /// Returns what each argument expects to follow it fn get_arg_parse_type(arg: Args) ArgType { return switch (arg) { + .gir => ArgType.single, .help => ArgType.single, + .init => ArgType.single, .install => ArgType.expectsOneParam, .list => ArgType.expectsOneParam, .use => ArgType.expectsOneParam, - .gir => ArgType.single, .version => ArgType.single, }; } @@ -47,12 +49,13 @@ fn get_arg_parse_type(arg: Args) ArgType { /// For a given `Args`, returns a string description of what each does fn command_desc(arg: Args) string { return switch (arg) { + .gir => " gir\t\tPrint out ascii Gir", .help => " help\t\tDisplays this help message", + .init => " init\t\tAdds the necessary PATH to the system env", .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", }; } @@ -103,7 +106,7 @@ fn show_context_help(arg: Args) void { \\ , .{}); }, - .gir, .version, .help => { + .gir, .help, .init, .version => { return; }, } @@ -118,6 +121,13 @@ fn do_arg_action(allocator: std.mem.Allocator, arg: Args, param: ?[]const u8) vo .gir => { utils.printGir(); }, + .init => { + zim.init(allocator) catch |err| { + std.debug.print("Error running `init`\n{any}\n", .{err}); + }; + std.os.exit(1); + return; + }, .version => { const ver = versionStr(allocator) catch { std.debug.print("Error getting version\n", .{}); @@ -135,7 +145,7 @@ fn do_arg_action(allocator: std.mem.Allocator, arg: Args, param: ?[]const u8) vo }, .install => { if (param == null) { - std.debug.print("Error: Expected version to follow\n\n", .{}); + std.debug.print("Error: Expected index or version name to follow.\nRun `zim install help` for details.\n\n", .{}); std.os.exit(1); return; } @@ -167,13 +177,13 @@ fn do_arg_action(allocator: std.mem.Allocator, arg: Args, param: ?[]const u8) vo std.os.exit(1); } zim.list(allocator, param.?) catch |err| { - std.debug.print("Error running `list {s}`\n{any}", .{ param.?, err }); + std.debug.print("Error running `list {s}`\n{any}\n", .{ param.?, err }); std.os.exit(1); }; }, .use => { if (param == null) { - std.debug.print("Error: Expected version to follow\n\n", .{}); + std.debug.print("Error: Expected version or index to follow.\nRun `zim use help` for details.\n\n", .{}); std.os.exit(1); } const p = param.?; @@ -183,7 +193,7 @@ fn do_arg_action(allocator: std.mem.Allocator, arg: Args, param: ?[]const u8) vo return; } zim.use(allocator, param.?) catch |err| { - std.debug.print("Error running `use {s}`\n{any}", .{ param.?, err }); + std.debug.print("Error running `use {s}`\n{any}\n", .{ param.?, err }); std.os.exit(1); }; }, diff --git a/src/utils.zig b/src/utils.zig index 899c352..ac6b2c3 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -30,17 +30,18 @@ test { pub fn printGir() void { std.debug.print( - \\ n n - \\ o ( ) - \\ ___/_ / I am \ - \\ / / . o ( your ) - \\ 0> 0 / \ Doom! / - \\ Lu__/ ( ) - \\ || - u - - \\ o^ ^ o - \\ _/ / / \_ - \\ |__| - \\ O=> (O + \\ n n n n + \\ ( ) + \\ o ( I am ) + \\ ___/_ o ( your ) + \\ / / . ( Doom! ) + \\ 0> 0 / ( ) + \\ Lu__/ u u u u + \\ || + \\ o^ ^o + \\ ._/ | | \_ + \\ |__| + \\ O=> (O \\ \\ , .{}); diff --git a/src/version.zig b/src/version.zig index 8c653a6..c70f4e9 100644 --- a/src/version.zig +++ b/src/version.zig @@ -1,20 +1,45 @@ const std = @import("std"); +const utils = @import("utils.zig"); const string = []const u8; +const JsonTokens = enum { + src, + docs, + stdDocs, + notes, + version, + bootstrap, + other, +}; + +pub const ZVError = error{ + CannotInit, +}; + pub const ZigVersion = struct { allocator: std.mem.Allocator, version_string: string, tarball_url: ?string, src_tarball_url: ?string, + bootstrap_url: ?string, platform_string: ?string, docs_url: ?string, + std_docs_url: ?string, + notes_url: ?string, + latest: bool, - 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; + pub fn init(allocator: std.mem.Allocator, scanner: *std.json.Scanner, ver_string: string) ZVError!ZigVersion { + var version_string = allocator.dupe(u8, ver_string) catch { + return ZVError.CannotInit; + }; + var std_docs_url: ?string = null; var docs_url: ?string = null; var tarball_url: ?string = null; var platform_string: ?string = null; var src_tarball_url: ?string = null; + var bootstrap_url: ?string = null; + var notes_url: ?string = null; + var latest = false; var scanning = true; while (scanning) { @@ -30,29 +55,99 @@ pub const ZigVersion = struct { } }, .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; - } + const json_tag = utils.nameToEnumTag(token.string, JsonTokens) catch JsonTokens.other; + switch (json_tag) { + .docs => { + const docs_token = scanner.*.next() catch { + return ZVError.CannotInit; + }; + docs_url = allocator.dupe(u8, docs_token.string) catch { + return ZVError.CannotInit; + }; + }, + .stdDocs => { + const std_docs_token = scanner.*.next() catch { + return ZVError.CannotInit; + }; + std_docs_url = allocator.dupe(u8, std_docs_token.string) catch { + return ZVError.CannotInit; + }; + }, + .src => { + // ignore the object begin + checkNextToken(scanner, .object_begin); + _ = scanner.*.next() catch { + return ZVError.CannotInit; + }; + // ignore tarball string + checkNextToken(scanner, .string); + _ = scanner.*.next() catch { + return ZVError.CannotInit; + }; + const tar_token = scanner.*.next() catch { + return ZVError.CannotInit; + }; + src_tarball_url = allocator.dupe(u8, tar_token.string) catch { + return ZVError.CannotInit; + }; + }, + .notes => { + const std_docs_token = scanner.*.next() catch { + return ZVError.CannotInit; + }; + notes_url = allocator.dupe(u8, std_docs_token.string) catch { + return ZVError.CannotInit; + }; + }, + .version => { + const ver_token = scanner.*.next() catch { + return ZVError.CannotInit; + }; + version_string = allocator.dupe(u8, ver_token.string) catch { + return ZVError.CannotInit; + }; + latest = true; + }, + .bootstrap => { + checkNextToken(scanner, .object_begin); + _ = scanner.*.next() catch { + return ZVError.CannotInit; + }; + // ignore tarball string + checkNextToken(scanner, .string); + _ = scanner.*.next() catch { + return ZVError.CannotInit; + }; + const tar_token = scanner.*.next() catch { + return ZVError.CannotInit; + }; + bootstrap_url = tar_token.string; + }, + .other => { + const platform_name = "x86_64-linux"; + if (std.mem.eql(u8, token.string, platform_name)) { + platform_string = allocator.dupe(u8, token.string) catch { + return ZVError.CannotInit; + }; + // ignore object begin + checkNextToken(scanner, .object_begin); + _ = scanner.*.next() catch { + return ZVError.CannotInit; + }; + checkNextToken(scanner, .string); + const tar_token = scanner.*.next() catch { + return ZVError.CannotInit; + }; + if (std.mem.eql(u8, tar_token.string, "tarball")) { + const tar_url_token = scanner.*.next() catch { + return ZVError.CannotInit; + }; + tarball_url = allocator.dupe(u8, tar_url_token.string) catch { + return ZVError.CannotInit; + }; + } + } + }, } }, else => { @@ -66,7 +161,18 @@ pub const ZigVersion = struct { } } - 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 }; + return ZigVersion{ + .allocator = allocator, + .version_string = version_string, + .tarball_url = tarball_url, + .src_tarball_url = src_tarball_url, + .bootstrap_url = bootstrap_url, + .notes_url = notes_url, + .platform_string = platform_string, + .docs_url = docs_url, + .std_docs_url = std_docs_url, + .latest = latest, + }; } pub fn deinit() void {} @@ -87,8 +193,8 @@ pub const ZigVersion = struct { } } - pub fn hasTar(zv: *ZigVersion) bool { - return zv.tarball_url != null; + pub fn available(zv: *const ZigVersion) bool { + return zv.tarball_url != null or zv.src_tarball_url != null; } }; diff --git a/src/zim.zig b/src/zim.zig index 5d38144..6055e25 100644 --- a/src/zim.zig +++ b/src/zim.zig @@ -4,16 +4,20 @@ const version = @import("version.zig"); const string = []const u8; const ZigVersion = version.ZigVersion; +const ZigVersionData = version.ZigVersionData; const ZimError = error{ + BadIO, BadParameter, MalformedJson, + Memory, NetworkError, NotImplemented, NoHomeConfigured, Unexpected, }; +const SIXTY_FOUR_KILOBYTES = 65536; const BIN_SYM_ZIG_PATH = "bin/zig"; fn createSubDir(dir: std.fs.Dir, path: string) ZimError!void { @@ -106,51 +110,84 @@ fn getLocalVersionsList( 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; +/// Fetches all available remote versions of zig, cached locally if recently requested. +/// The caller owns the memory of the `std.ArrayList(ZigVersion)` +fn getRemoteVersionsSlice(allocator: std.mem.Allocator) ZimError![]const ZigVersion { + // Check local file first for chached network call first + const zim_dir = try openZimDir(allocator); + const now = @as(i128, @intCast(std.time.timestamp())) * 1000 * 1000; + const yesterday = now - (24 * 60 * 60 * 1000 * 1000); + var remote_versions_file = zim_dir.createFile("versions/remote_versions.json", .{ .read = true, .truncate = false }) catch { + return ZimError.BadIO; }; + defer remote_versions_file.close(); - 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; - }; + var body: string = ""; defer allocator.free(body); + const file_metadata = remote_versions_file.metadata() catch { + return ZimError.BadIO; + }; + + if (file_metadata.modified() > yesterday) { + body = remote_versions_file.readToEndAlloc(allocator, SIXTY_FOUR_KILOBYTES) catch |err| { + std.debug.print("Could not read remote versions file: {any}\n", .{err}); + return ZimError.Unexpected; + }; + } + if (body.len == 0) { + std.debug.print("No cached versions, getting remote versions\n", .{}); + + // Otherwise, make network call and parse + const versions_json_url = "https://ziglang.org/download/index.json"; + const versions_json_uri = std.Uri.parse(versions_json_url) catch { + std.debug.print("Could not parse URL for ziglang {s}\n", .{versions_json_url}); + 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.NetworkError; + }; + 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; + }; + + body = request.reader().readAllAlloc(allocator, SIXTY_FOUR_KILOBYTES) catch |err| { + std.debug.print("Error getting body: {any}\n", .{err}); + return ZimError.NetworkError; + }; + + // Write body response to file + remote_versions_file.writeAll(body) catch { + return ZimError.BadIO; + }; + } + 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 remote_zig_versions_list = std.ArrayList(ZigVersion).init(allocator); var scanning = true; var last_version: string = ""; @@ -158,9 +195,11 @@ fn getRemoteVersionsList(allocator: std.mem.Allocator) ZimError!std.ArrayList(Zi 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; + const zv = ZigVersion.init(allocator, &body_scanner, last_version) catch { + continue; + }; + remote_zig_versions_list.append(zv) catch { + return ZimError.Memory; }; }, .end_of_document => { @@ -182,7 +221,9 @@ fn getRemoteVersionsList(allocator: std.mem.Allocator) ZimError!std.ArrayList(Zi }, } } - return remote_zig_versions; + return remote_zig_versions_list.toOwnedSlice() catch { + return ZimError.Memory; + }; } const ShellType = enum { @@ -218,13 +259,130 @@ fn printZimPathHelp(shell_tag: ?ShellType, zim_path: string) void { } } -pub fn install(allocator: std.mem.Allocator, param: string) ZimError!void { - _ = param; +fn downloadAndExtractTarball(allocator: std.mem.Allocator, zv: ZigVersion) ZimError!void { + const tarball_uri = std.Uri.parse(zv.tarball_url.?) catch { + std.debug.print("Could not parse URL for tarball {s}\n", .{zv.tarball_url.?}); + 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.NetworkError; + }; + var client = std.http.Client{ .allocator = allocator }; + defer client.deinit(); + + var request = client.request(.GET, tarball_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("Downloading {s}...\n", .{zv.tarball_url.?}); + request.wait() catch { + std.debug.print("Error waiting for request\n", .{}); + return ZimError.NetworkError; + }; + + const body: string = request.reader().readAllAlloc(allocator, SIXTY_FOUR_KILOBYTES * 2000) catch |err| { + std.debug.print("Error getting body: {any}\n", .{err}); + return ZimError.NetworkError; + }; + + var zim_dir = try openZimDir(allocator); + + // Get slice of absolute path to zim directory + const buf_len = 100; + var arr: [buf_len]u8 = undefined; + var buf = arr[0..]; + const zim_path = zim_dir.realpath("./", buf) catch { + return ZimError.Memory; + }; + + // Buffer length for file name `zig-.tar` + const tar_buf_len = 38; + var tar_arr: [tar_buf_len]u8 = undefined; + var tar_buf = tar_arr[0..]; + const tarball_file_name = std.fmt.bufPrint(tar_buf, "zig-{s}.tar", .{ zim_path, zv.version_string }) catch { + return ZimError.Memory; + }; + + // Buffer lenght for `/versions/` + const full_tar_buf_len = buf_len + tar_buf_len + 10; + var full_tar_arr: [full_tar_buf_len]u8 = undefined; + var full_tar_buf = full_tar_arr[0..]; + const full_path_tarball_file_name = std.fmt.bufPrint(full_tar_buf, "{s}/versions/{s}", .{ zim_path, tarball_file_name}) catch { + return ZimError.Memory; + }; + + const ver_buf_len = 100; + var ver_arr: [ver_buf_len]u8 = undefined; + var ver_buf = ver_arr[0..]; + const versions_path = std.fmt.bufPrint(ver_buf, "{s}/versions", .{zim_path}) catch { + return ZimError.Memory; + }; + + var tarball_file = zim_dir.createFile(full_path_tarball_file_name, .{}) catch { + return ZimError.BadIO; + }; + tarball_file.writer().writeAll(body) catch { + return ZimError.BadIO; + }; + + // Buffer length for new version dir name `versions/zig-/` + var proc = std.ChildProcess.init(&.{ "tar", "-xf", full_path_tarball_file_name, "-C", versions_path }, allocator); + + std.debug.print("Unpacking {s}...\n", .{tarball_file_name}); + _ = proc.spawnAndWait() catch |err| { + std.debug.print("Could not spawn tar child process: {any}\n", .{err}); + return ZimError.Unexpected; + }; + + // Remove tarball file + zim_dir.deleteFile(full_path_tarball_file_name) catch |err| { + std.debug.print("Could not remove {s}: {any}\n", .{tarball_file_name, err}); + return ZimError.BadIO; + }; + + std.debug.print("{s} is installed and now locally available!\n", .{zv.version_string}); +} + +pub fn install(allocator: std.mem.Allocator, param: string) ZimError!void { var zim_dir = try openZimDir(allocator); defer zim_dir.close(); - return ZimError.NotImplemented; + var remote_versions = try getRemoteVersionsSlice(allocator); + defer allocator.free(remote_versions); + + const version_num: ?u8 = std.fmt.parseInt(u8, param, 0) catch null; + if (version_num != null) { + const num = version_num.?; + if (num > remote_versions.len) { + std.debug.print("{d} exceeds the range of available versions: {d}\n", .{ num, remote_versions.len }); + return ZimError.BadParameter; + } + + const zv: ZigVersion = remote_versions[version_num.? - 1]; + std.debug.print("Attempting to install {s}\n", .{zv.version_string}); + + if (zv.tarball_url != null) { + try downloadAndExtractTarball(allocator, zv); + } else { + std.debug.print("No tarball url available for version {s}\n", .{zv.version_string}); + return ZimError.Unexpected; + } + } else { + std.debug.print("Cannot install version from string yet\n", .{}); + return ZimError.NotImplemented; + } } pub fn use(allocator: std.mem.Allocator, param: string) ZimError!void { @@ -290,14 +448,6 @@ pub fn use(allocator: std.mem.Allocator, param: string) ZimError!void { 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 }; @@ -333,16 +483,29 @@ pub fn list(allocator: std.mem.Allocator, param: string) !void { return; }, .global => { - var remote_versions = try getRemoteVersionsList(allocator); - if (remote_versions.items.len == 0) { + var remote_versions = try getRemoteVersionsSlice(allocator); + defer allocator.free(remote_versions); + + if (remote_versions.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(); + var index: u8 = 0; + for (remote_versions) |remote_version| { + if (remote_version.available()) { + index = index + 1; + std.debug.print(" [{d}]\tzig-{s}\n", .{ index, remote_version.version_string }); + } } + std.debug.print("\nZig versions can be installed using `zim install `\n\n", .{}); return; }, } } + +/// Runs the `init` subcommand +pub fn init(allocator: std.mem.Allocator) !void { + _ = allocator; + return ZimError.NotImplemented; +}