commit d4c06a2ce092a5ebec66cefd6ba2aaccb6d88db9
Author: Martin Ashby <martin@ashbysoft.com>
Date: Thu, 23 May 2024 21:27:50 +0100
Initial
Diffstat:
A | .gitignore | | | 2 | ++ |
A | build.zig | | | 38 | ++++++++++++++++++++++++++++++++++++++ |
A | build.zig.zon | | | 17 | +++++++++++++++++ |
A | src/argparse.zig | | | 508 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/main.zig | | | 255 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
5 files changed, 820 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,2 @@
+zig-out
+zig-cache/
diff --git a/build.zig b/build.zig
@@ -0,0 +1,38 @@
+const std = @import("std");
+
+pub fn build(b: *std.Build) void {
+ const target = b.standardTargetOptions(.{});
+ const optimize = b.standardOptimizeOption(.{});
+
+ const minisign = b.dependency("minisign", .{ .target = target, .optimize = optimize });
+ const exe = b.addExecutable(.{
+ .name = "zigvm",
+ .root_source_file = b.path("src/main.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+ exe.root_module.addImport("minisign", minisign.module("minizign"));
+
+ b.installArtifact(exe);
+ const run_cmd = b.addRunArtifact(exe);
+
+ run_cmd.step.dependOn(b.getInstallStep());
+
+ if (b.args) |args| {
+ run_cmd.addArgs(args);
+ }
+
+ const run_step = b.step("run", "Run the app");
+ run_step.dependOn(&run_cmd.step);
+
+ const exe_unit_tests = b.addTest(.{
+ .root_source_file = b.path("src/main.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+
+ const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
+
+ const test_step = b.step("test", "Run unit tests");
+ test_step.dependOn(&run_exe_unit_tests.step);
+}
diff --git a/build.zig.zon b/build.zig.zon
@@ -0,0 +1,17 @@
+.{
+ .name = "zigvm",
+ .version = "0.0.0",
+
+ .dependencies = .{
+ .minisign = .{
+ .url = "https://github.com/jedisct1/zig-minisign/archive/b9ae25191ca5ef19525f51d38f9f7503d6b5e7e8.tar.gz",
+ .hash = "122061fe69199522c3e88f680909fbf590b9e26a8c0342e53c788af2a2e089157e9e",
+ }
+ },
+
+ .paths = .{
+ "build.zig",
+ "build.zig.zon",
+ "src",
+ },
+}
diff --git a/src/argparse.zig b/src/argparse.zig
@@ -0,0 +1,508 @@
+const std = @import("std");
+
+const Self = @This();
+
+const ArgParse = Self;
+
+pub const Positional = struct {
+ name: []const u8,
+ description: []const u8,
+ default: ?[]const u8 = null,
+
+ value: ?[]const u8 = null,
+};
+
+pub const Flag = struct {
+ long: []const u8,
+ short: ?[]const u8 = null,
+ description: []const u8,
+ hasarg: bool,
+
+ waspresent: bool = false,
+ defaultargvalue: ?[]const u8 = null,
+ argvalue: ?[]const u8 = null,
+};
+
+aa: std.heap.ArenaAllocator,
+progname: []const u8,
+description: []const u8,
+positionals: std.ArrayListUnmanaged(*Positional) = .{},
+flags: std.ArrayListUnmanaged(*Flag) = .{},
+excess: std.ArrayListUnmanaged([]const u8) = .{},
+
+pub fn init(ca: std.mem.Allocator, progname: []const u8, description: []const u8) Self {
+ return .{
+ .aa = std.heap.ArenaAllocator.init(ca),
+ .progname = progname,
+ .description = description,
+ };
+}
+
+pub fn deinit(self: *Self) void {
+ self.aa.deinit();
+}
+
+pub fn addPositional(self: *Self, pos: *Positional) !void {
+ try self.positionals.append(self.aa.allocator(), pos);
+}
+
+pub fn addFlag(self: *Self, flag: *Flag) !void {
+ try self.flags.append(self.aa.allocator(), flag);
+}
+
+// Return false if args were not parsed (but it wasn't an error)
+pub fn parseOrHelp(self: *Self) !bool {
+ self.parse() catch |e| {
+ switch (e) {
+ error.HelpWanted => {
+ try self.help(std.io.getStdOut().writer());
+ return false;
+ },
+ error.InvalidArgs => {
+ try self.help(std.io.getStdErr().writer());
+ return e;
+ },
+ else => return e,
+ }
+ };
+ return true;
+}
+
+pub fn parse(self: *Self) !void {
+ var it = try std.process.argsWithAllocator(self.aa.allocator());
+ defer it.deinit();
+ try self.parseInternal(&it);
+}
+
+pub fn help(self: *Self, writer: anytype) !void {
+ // Add the default help argument
+ var helpflag: Flag = .{
+ .long = "help",
+ .short = "h",
+ .description = "Show this help",
+ .hasarg = false,
+ };
+ try self.flags.append(self.aa.allocator(), &helpflag);
+
+ try std.fmt.format(writer, "Usage: {s}", .{self.progname});
+ for (self.positionals.items) |pos| {
+ try std.fmt.format(writer, " [{s}]", .{pos.name});
+ }
+ try std.fmt.format(writer, "\n{s}\n", .{self.description});
+ for (self.positionals.items) |pos| {
+ try std.fmt.format(writer, " {s}: {s}", .{ pos.name, pos.description });
+ if (pos.default) |df| {
+ try std.fmt.format(writer, " (Default: {s})", .{df});
+ }
+ try writer.writeByte('\n');
+ }
+ try std.fmt.format(writer, "\nFlags:\n", .{});
+ var longestFlag: u64 = 0;
+ for (self.flags.items) |flag| {
+ var cw = std.io.countingWriter(std.io.null_writer);
+ try writeFlagsLongShort(flag, cw.writer());
+ longestFlag = @max(longestFlag, cw.bytes_written);
+ }
+ longestFlag += 3;
+ for (self.flags.items) |flag| {
+ var cw = std.io.countingWriter(writer);
+ const w = cw.writer();
+ try w.writeByteNTimes(' ', 2);
+ try writeFlagsLongShort(flag, w);
+ const pad = (longestFlag - cw.bytes_written);
+ try writer.writeByteNTimes(' ', pad);
+ try std.fmt.format(writer, "{s}", .{flag.description});
+ if (flag.defaultargvalue) |dav| {
+ try std.fmt.format(writer, " (Default: {s})", .{dav});
+ }
+ try writer.writeByte('\n');
+ }
+}
+
+fn writeFlagsLongShort(flag: *Flag, writer: anytype) !void {
+ if (flag.short) |short| {
+ try std.fmt.format(writer, "-{s},", .{short});
+ }
+ try std.fmt.format(writer, "--{s}", .{flag.long});
+ if (flag.hasarg) {
+ try std.fmt.format(writer, "=ARG", .{});
+ }
+}
+
+fn parseInternal(self: *Self, it: anytype) !void {
+ var pos_i: usize = 0;
+ if (!it.skip()) return error.InvalidArgs;
+ while (it.next()) |nxt| {
+ if (std.mem.startsWith(u8, nxt, "-")) {
+ var flag = nxt[1..];
+ const longflag = std.mem.startsWith(u8, nxt, "--");
+ if (longflag) {
+ flag = nxt[2..];
+ }
+ var spl = std.mem.splitScalar(u8, flag, '=');
+ const key = spl.first();
+ if (std.mem.eql(u8, key, "help") or std.mem.eql(u8, key, "h")) {
+ return error.HelpWanted;
+ }
+
+ for (self.flags.items) |aflag| {
+ const ismatch = if (longflag) std.mem.eql(u8, key, aflag.long) else if (aflag.short) |srt| std.mem.eql(u8, key, srt) else false;
+ if (ismatch) {
+ if (aflag.waspresent) {
+ return error.InvalidArgs;
+ }
+ aflag.waspresent = true;
+ if (aflag.hasarg) {
+ if (spl.rest().len > 0) {
+ aflag.argvalue = try self.aa.allocator().dupe(u8, spl.rest());
+ } else {
+ const val = it.next() orelse return error.InvalidArgs;
+ aflag.argvalue = try self.aa.allocator().dupe(u8, val);
+ }
+ } else {
+ if (spl.rest().len > 0) {
+ return error.InvalidArgs;
+ }
+ }
+ break;
+ }
+ } else {
+ return error.InvalidArgs;
+ }
+ } else {
+ if (pos_i < self.positionals.items.len) {
+ self.positionals.items[pos_i].value = try self.aa.allocator().dupe(u8, nxt);
+ pos_i += 1;
+ } else {
+ try self.excess.append(self.aa.allocator(), try self.aa.allocator().dupe(u8, nxt));
+ }
+ }
+ }
+ for (self.flags.items) |aflag| {
+ if (aflag.argvalue) |_| {} else if (aflag.defaultargvalue) |dav| {
+ aflag.argvalue = try self.aa.allocator().dupe(u8, dav);
+ }
+ }
+ for (self.positionals.items) |pos| {
+ if (pos.value) |_| {} else if (pos.default) |dav| {
+ pos.value = try self.aa.allocator().dupe(u8, dav);
+ }
+ }
+}
+
+const TestArgs = struct {
+ args: []const []const u8,
+ pos: usize = 0,
+ pub fn next(self: *TestArgs) ?[]const u8 {
+ if (self.pos < self.args.len) {
+ defer self.pos += 1;
+ return self.args[self.pos];
+ } else {
+ return null;
+ }
+ }
+ pub fn first(self: *TestArgs) []const u8 {
+ return self.next() orelse @panic("bad test args, no first");
+ }
+ pub fn skip(self: *TestArgs) bool {
+ if (self.next()) |_| {
+ return true;
+ } else {
+ return false;
+ }
+ }
+};
+
+test "flag not present" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var flag: Flag = .{
+ .long = "myflag",
+ .short = "m",
+ .description = "Hello there",
+ .hasarg = false,
+ };
+ try ap.addFlag(&flag);
+ var ta: TestArgs = .{ .args = &.{ "prog", "foobar" } };
+ try ap.parseInternal(&ta);
+ try std.testing.expect(!flag.waspresent);
+}
+
+test "flag with no arg" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var flag: Flag = .{
+ .long = "myflag",
+ .short = "m",
+ .description = "Hello there",
+ .hasarg = false,
+ };
+ try ap.addFlag(&flag);
+ var ta: TestArgs = .{ .args = &.{ "prog", "--myflag" } };
+ try ap.parseInternal(&ta);
+ try std.testing.expect(flag.waspresent);
+}
+
+test "flag with arg, but arg not supplied" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var flag: Flag = .{
+ .long = "myflag",
+ .short = "m",
+ .description = "Hello there",
+ .hasarg = true,
+ };
+ try ap.addFlag(&flag);
+ var ta: TestArgs = .{ .args = &.{ "prog", "--myflag" } };
+ try std.testing.expectError(error.InvalidArgs, ap.parseInternal(&ta));
+}
+
+test "flag with no arg, but an arg was supplie" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var flag: Flag = .{
+ .long = "myflag",
+ .short = "m",
+ .description = "Hello there",
+ .hasarg = false,
+ };
+ try ap.addFlag(&flag);
+ var ta: TestArgs = .{ .args = &.{ "prog", "--myflag=foobar" } };
+ try std.testing.expectError(error.InvalidArgs, ap.parseInternal(&ta));
+}
+
+test "flag with arg, separated by space" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var flag: Flag = .{
+ .long = "myflag",
+ .short = "m",
+ .description = "Hello there",
+ .hasarg = true,
+ };
+ try ap.addFlag(&flag);
+ var ta: TestArgs = .{ .args = &.{ "prog", "--myflag", "myvalue" } };
+ try ap.parseInternal(&ta);
+ try std.testing.expect(flag.waspresent);
+ try std.testing.expectEqualStrings("myvalue", flag.argvalue.?);
+}
+
+test "flag with an arg, separated by an equals" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var flag: Flag = .{
+ .long = "myflag",
+ .short = "m",
+ .description = "Hello there",
+ .hasarg = true,
+ };
+ try ap.addFlag(&flag);
+ var ta: TestArgs = .{ .args = &.{ "prog", "--myflag=myvalue" } };
+ try ap.parseInternal(&ta);
+ try std.testing.expect(flag.waspresent);
+ try std.testing.expectEqualStrings("myvalue", flag.argvalue.?);
+}
+
+test "short flag" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var flag: Flag = .{
+ .long = "myflag",
+ .short = "m",
+ .description = "Hello there",
+ .hasarg = true,
+ };
+ try ap.addFlag(&flag);
+ var ta: TestArgs = .{ .args = &.{ "prog", "-m=myvalue" } };
+ try ap.parseInternal(&ta);
+ try std.testing.expect(flag.waspresent);
+ try std.testing.expectEqualStrings("myvalue", flag.argvalue.?);
+}
+
+test "unexpected flag" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var ta: TestArgs = .{ .args = &.{ "prog", "--myflag=myvalue" } };
+ try std.testing.expectError(error.InvalidArgs, ap.parseInternal(&ta));
+}
+
+test "duplicate flag" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var flag: Flag = .{
+ .long = "myflag",
+ .short = "m",
+ .description = "Hello there",
+ .hasarg = true,
+ };
+ try ap.addFlag(&flag);
+ var ta: TestArgs = .{ .args = &.{ "prog", "--myflag=myvalue", "--myflag", "someothervalue" } };
+ try std.testing.expectError(error.InvalidArgs, ap.parseInternal(&ta));
+}
+
+test "postitional argument not supplied" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var pos: Positional = .{
+ .name = "foo",
+ .description = "A positional argument",
+ };
+ try ap.addPositional(&pos);
+ var ta: TestArgs = .{ .args = &.{"prog"} };
+ try ap.parseInternal(&ta);
+ try std.testing.expectEqual(null, pos.value);
+}
+
+test "postitional argument" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var pos: Positional = .{
+ .name = "foo",
+ .description = "A positional argument",
+ };
+ try ap.addPositional(&pos);
+ var ta: TestArgs = .{ .args = &.{ "prog", "bar" } };
+ try ap.parseInternal(&ta);
+ try std.testing.expectEqualStrings("bar", pos.value.?);
+}
+
+test "two postitional arguments, first is supplied" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var pos: Positional = .{
+ .name = "foo",
+ .description = "A positional argument",
+ };
+ try ap.addPositional(&pos);
+ var pos2: Positional = .{
+ .name = "foo2",
+ .description = "A second positional argument",
+ };
+ try ap.addPositional(&pos2);
+ var ta: TestArgs = .{ .args = &.{ "prog", "bar" } };
+ try ap.parseInternal(&ta);
+ try std.testing.expectEqualStrings("bar", pos.value.?);
+ try std.testing.expectEqual(null, pos2.value);
+}
+
+test "two positional arguments, both are supplied and some extras" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var pos: Positional = .{
+ .name = "foo",
+ .description = "A positional argument",
+ };
+ try ap.addPositional(&pos);
+ var pos2: Positional = .{
+ .name = "foo2",
+ .description = "A second positional argument",
+ };
+ try ap.addPositional(&pos2);
+ var ta: TestArgs = .{ .args = &.{ "prog", "bar", "baz", "bing", "bam" } };
+ try ap.parseInternal(&ta);
+ try std.testing.expectEqualStrings("bar", pos.value.?);
+ try std.testing.expectEqualStrings("baz", pos2.value.?);
+ try std.testing.expectEqual(@as(usize, 2), ap.excess.items.len);
+ try std.testing.expectEqualStrings("bing", ap.excess.items[0]);
+ try std.testing.expectEqualStrings("bam", ap.excess.items[1]);
+}
+
+test "positional argument with a default" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var pos: Positional = .{
+ .name = "foo",
+ .description = "A positional argument",
+ };
+ try ap.addPositional(&pos);
+ var pos2: Positional = .{
+ .name = "foo2",
+ .description = "A second positional argument",
+ .default = "defaultbar",
+ };
+ try ap.addPositional(&pos2);
+ var ta: TestArgs = .{ .args = &.{ "prog", "bar" } };
+ try ap.parseInternal(&ta);
+ try std.testing.expectEqualStrings("bar", pos.value.?);
+ try std.testing.expectEqualStrings("defaultbar", pos2.value.?);
+}
+
+test "help" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var pos1: Positional = .{
+ .name = "foo",
+ .description = "A positional argument",
+ };
+ try ap.addPositional(&pos1);
+ var pos2: Positional = .{
+ .name = "foo2",
+ .description = "A second positional argument, with a default",
+ .default = "defaultbar",
+ };
+ try ap.addPositional(&pos2);
+ var flag1: Flag = .{
+ .long = "myflag",
+ .short = "m",
+ .hasarg = true,
+ .description = "My flag with an argument",
+ };
+ try ap.addFlag(&flag1);
+ var flag2: Flag = .{
+ .long = "twoflag",
+ .short = "t",
+ .hasarg = true,
+ .description = "My flag with an argument and a default",
+ .defaultargvalue = "twoflagdefault",
+ };
+ try ap.addFlag(&flag2);
+ var flag3: Flag = .{
+ .long = "threeflag",
+ .short = "tf",
+ .hasarg = false,
+ .description = "My flag with no argument",
+ };
+ try ap.addFlag(&flag3);
+
+ var out = std.ArrayList(u8).init(a);
+ defer out.deinit();
+ try ap.help(out.writer());
+ const expected =
+ \\Usage: Myprog [foo] [foo2]
+ \\my program to do things
+ \\ foo: A positional argument
+ \\ foo2: A second positional argument, with a default (Default: defaultbar)
+ \\
+ \\Flags:
+ \\ -m,--myflag=ARG My flag with an argument
+ \\ -t,--twoflag=ARG My flag with an argument and a default (Default: twoflagdefault)
+ \\ -tf,--threeflag My flag with no argument
+ \\ -h,--help Show this help
+ \\
+ ;
+ try std.testing.expectEqualStrings(expected, out.items);
+}
+
+test "helpwanted" {
+ var ap = ArgParse.init(std.testing.allocator, "foo", "foo");
+ defer ap.deinit();
+ var ta: TestArgs = .{ .args = &.{ "prog", "--help" } };
+ try std.testing.expectError(error.HelpWanted, ap.parseInternal(&ta));
+ var ta2: TestArgs = .{ .args = &.{ "prog", "-h" } };
+ try std.testing.expectError(error.HelpWanted, ap.parseInternal(&ta2));
+}
diff --git a/src/main.zig b/src/main.zig
@@ -0,0 +1,255 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const ArgParse = @import("./argparse.zig");
+const minisign = @import("minisign");
+
+pub fn main() !void {
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+ defer _ = gpa.deinit();
+ const a = gpa.allocator();
+
+ const path = try std.process.getEnvVarOwned(a, "PATH");
+ defer a.free(path);
+
+ const home = try std.process.getEnvVarOwned(a, "HOME");
+ defer a.free(home);
+ const defaultInstallDir = try std.fs.path.join(a, &.{ home, ".local", "bin" });
+ defer a.free(defaultInstallDir);
+ const defaultCacheDir = try std.fs.path.join(a, &.{ home, ".zigvm" });
+ defer a.free(defaultCacheDir);
+
+ // Argument parsing...
+ var ap = ArgParse.init(a, "zigvm", "Manage installed zig versions");
+ defer ap.deinit();
+ var versionArg: ArgParse.Positional = .{
+ .name = "version",
+ .description = "Version of zig to download",
+ .default = "master",
+ };
+ try ap.addPositional(&versionArg);
+ var installDirFlag: ArgParse.Flag = .{
+ .long = "install-dir",
+ .short = "i",
+ .description = "zig installation directory",
+ .hasarg = true,
+ .defaultargvalue = defaultInstallDir,
+ };
+ try ap.addFlag(&installDirFlag);
+ var cacheDirFlag: ArgParse.Flag = .{
+ .long = "cache-dir",
+ .short = "c",
+ .description = "zigvm cache directory",
+ .hasarg = true,
+ .defaultargvalue = defaultCacheDir,
+ };
+ try ap.addFlag(&cacheDirFlag);
+ if (!try ap.parseOrHelp()) {
+ return;
+ }
+ const installDirPath = installDirFlag.argvalue orelse return error.MissingArg;
+ const cacheDirPath = cacheDirFlag.argvalue orelse return error.MissingArg;
+ const version = versionArg.value orelse return error.MissingArg;
+
+ // Check the install dir is present in PATH, warn if it isn't
+ var toks = std.mem.splitScalar(u8, path, ':');
+ while (toks.next()) |nxt| {
+ if (std.mem.eql(u8, nxt, installDirPath)) {
+ break;
+ }
+ } else {
+ std.log.warn("Zig installation directory {s} not found in PATH {s}", .{ installDirPath, path });
+ }
+
+ const tuple = @tagName(builtin.target.cpu.arch) ++ "-" ++ @tagName(builtin.target.os.tag);
+ std.log.info("system: {s}", .{tuple});
+
+ // Ensure the cacheDir is present
+ var cacheDir: std.fs.Dir = undefined;
+ if (std.fs.openDirAbsolute(cacheDirPath, .{ .iterate = true })) |cd| {
+ cacheDir = cd;
+ } else |_| {
+ try std.fs.makeDirAbsolute(cacheDirPath);
+ cacheDir = try std.fs.openDirAbsolute(cacheDirPath, .{ .iterate = true });
+ }
+
+ var indexFile = try cacheDir.createFile("index.json", .{ .read = true, .truncate = false });
+ defer indexFile.close();
+
+ var client = std.http.Client{ .allocator = a };
+ defer client.deinit();
+ var cwb = ClientWithBuffer{
+ .client = &client,
+ };
+ std.log.info("refreshing index", .{});
+ refreshIndex(&cwb, &indexFile) catch |e| {
+ const ifs = try indexFile.stat();
+ if (ifs.size == 0) {
+ return error.FailedToDownloadIndex;
+ } else {
+ std.log.warn("Unable to refresh index: {}", .{e});
+ }
+ };
+
+ // Parse the index
+ var rdr = std.json.reader(a, indexFile.reader());
+ defer rdr.deinit();
+ var doc = try std.json.parseFromTokenSource(std.json.Value, a, &rdr, .{});
+ defer doc.deinit();
+ const versionDoc: std.json.Value = doc.value.object.get(version) orelse return error.ZigVersionNotFound;
+ const versionString = if (versionDoc.object.get("version")) |versionObj| versionObj.string else version;
+ std.log.info("version {s} mapped to {s}", .{ version, versionString });
+
+ // // Fetch the version if necessary
+ const systemVersion = versionDoc.object.get(tuple) orelse return error.SystemNotFound;
+ const tarballUrlObj = systemVersion.object.get("tarball") orelse return error.InvalidIndex;
+ const tarballHashObj = systemVersion.object.get("shasum") orelse return error.InvalidIndex;
+ const tarballHash = tarballHashObj.string;
+ const tarballUrl = tarballUrlObj.string;
+ std.log.info("tarballUrl: {s}", .{tarballUrl});
+ if (!std.mem.endsWith(u8, tarballUrl, ".tar.xz")) {
+ return error.UnsupportedFileFormat;
+ }
+
+ const tarballUri = try std.Uri.parse(tarballUrl);
+ const tarballUriPath = try std.fmt.allocPrint(a, "{path}", .{tarballUri.path});
+ defer a.free(tarballUriPath);
+ const tarballBasename = std.fs.path.basename(tarballUriPath);
+ std.log.info("basename: {s}", .{tarballBasename});
+ const tarballPath = try std.fs.path.join(a, &.{ cacheDirPath, tarballBasename });
+ defer a.free(tarballPath);
+ const tarballMinisigUrl = try std.fmt.allocPrint(a, "{s}.minisig", .{tarballUrl});
+ defer a.free(tarballMinisigUrl);
+ const tarballMinisigFilePath = try std.fmt.allocPrint(a, "{s}.minisig", .{tarballPath});
+ defer a.free(tarballMinisigFilePath);
+
+ var tarball = try std.fs.createFileAbsolute(tarballPath, .{ .read = true, .truncate = false });
+ var tarballStat = try tarball.stat();
+
+ if (tarballStat.size != 0) {
+ hashAndSigCheck(a, tarball, tarballHash, tarballMinisigFilePath) catch {
+ try tarball.setEndPos(0);
+ tarballStat.size = 0;
+ };
+ }
+
+ if (tarballStat.size == 0) {
+ {
+ std.log.info("downloading from {s} to {s}", .{ tarballUrl, tarballPath });
+ try downloadToFile(&cwb, tarballUrl, tarball);
+ try tarball.seekTo(0);
+
+ const minisigFile = try std.fs.createFileAbsolute(tarballMinisigFilePath, .{});
+ defer minisigFile.close();
+ try downloadToFile(&cwb, tarballMinisigUrl, minisigFile);
+ }
+
+ hashAndSigCheck(a, tarball, tarballHash, tarballMinisigFilePath) catch {
+ return error.InvalidDownload;
+ };
+ }
+
+ const extractedTarballDir = tarballBasename[0 .. tarballBasename.len - ".tar.xz".len];
+ cacheDir.access(extractedTarballDir, .{}) catch |e| switch (e) {
+ error.FileNotFound => {
+ std.log.info("extracting: {s}", .{tarballPath});
+ try tarball.seekTo(0);
+ var xr = try std.compress.xz.decompress(a, tarball.reader());
+ defer xr.deinit();
+ try std.tar.pipeToFileSystem(cacheDir, xr.reader(), .{});
+ },
+ else => {
+ return e;
+ },
+ };
+
+ // then symlink the selected dir to 'current'
+ const currentDirName = "current";
+ cacheDir.deleteFile(currentDirName) catch |e| switch (e) {
+ error.FileNotFound => {},
+ else => return e,
+ };
+ try cacheDir.symLink(extractedTarballDir, currentDirName, .{ .is_directory = true });
+
+ // then symlink the zig executable in 'current' to installDir
+ var installDir = try std.fs.openDirAbsolute(installDirPath, .{});
+ defer installDir.close();
+ const currentZigPath = try std.fs.path.join(a, &.{ cacheDirPath, currentDirName, "zig" });
+ defer a.free(currentZigPath);
+ installDir.deleteFile("zig") catch |e| switch (e) {
+ error.FileNotFound => {},
+ else => return e,
+ };
+ try installDir.symLink(currentZigPath, "zig", .{});
+ // EZ PZ
+ std.log.info("done!", .{});
+}
+
+const ClientWithBuffer = struct {
+ client: *std.http.Client,
+ buffer: [std.mem.page_size]u8 = .{0} ** std.mem.page_size,
+
+ fn open(self: *ClientWithBuffer, method: std.http.Method, uri: std.Uri) !std.http.Client.Request {
+ return try self.client.open(method, uri, .{ .server_header_buffer = &self.buffer });
+ }
+};
+
+fn hashAndSigCheck(a: std.mem.Allocator, file: std.fs.File, sha256: []const u8, minisigFilePath: []const u8) !void {
+ try sha256Check(file, sha256);
+ // minisig is leaky, use an arena
+ var aa = std.heap.ArenaAllocator.init(a);
+ defer aa.deinit();
+ const pk = minisign.PublicKey.decodeFromBase64("RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U") catch @panic("unexpected publickey");
+ var sig = try minisign.Signature.fromFile(aa.allocator(), minisigFilePath);
+ defer sig.deinit();
+ try file.seekTo(0);
+ try pk.verifyFile(aa.allocator(), file, sig, true);
+}
+
+fn sha256Check(file: std.fs.File, sha256: []const u8) !void {
+ try file.seekTo(0);
+ var hash = std.crypto.hash.sha2.Sha256.init(.{});
+ try pump(file.reader(), hash.writer());
+ const actual = hash.finalResult();
+ const actualHex = std.fmt.bytesToHex(actual, .lower);
+
+ if (!std.mem.eql(u8, sha256, &actualHex)) {
+ std.log.warn("hash mismatch exp {s} act {s}", .{ sha256, &actualHex });
+ return error.HashMismatch;
+ }
+}
+
+fn downloadToFile(cwb: *ClientWithBuffer, url: []const u8, file: std.fs.File) !void {
+ var req = try cwb.open(.GET, try std.Uri.parse(url));
+ defer req.deinit();
+ try req.send();
+ try req.wait();
+ if (req.response.status != .ok) {
+ return error.HttpStatusError;
+ }
+ try file.seekTo(0);
+ try pump(req.reader(), file.writer());
+}
+
+fn refreshIndex(client: *ClientWithBuffer, file: *std.fs.File) !void {
+ const uri = std.Uri.parse("https://ziglang.org/download/index.json") catch unreachable;
+ var req = try client.open(.GET, uri);
+ defer req.deinit();
+ try req.send();
+ try req.wait();
+ if (req.response.status != .ok) {
+ std.log.err("http status {}", .{req.response.status});
+ return error.IndexFetchHttpStatusError;
+ }
+ try file.setEndPos(0);
+ try pump(&req, file.writer());
+ try file.seekTo(0);
+}
+
+fn pump(reader: anytype, writer: anytype) !void {
+ var buf = [_]u8{0} ** std.mem.page_size;
+ while (true) {
+ const sz = try reader.read(&buf);
+ if (sz == 0) break;
+ try writer.writeAll(buf[0..sz]);
+ }
+}