zigvm

zigvm: Zig Version Manager
git clone git://code.mfashby.net:/zigvm
Log | Files | Refs | README

commit d4c06a2ce092a5ebec66cefd6ba2aaccb6d88db9
Author: Martin Ashby <martin@ashbysoft.com>
Date:   Thu, 23 May 2024 21:27:50 +0100

Initial

Diffstat:
A.gitignore | 2++
Abuild.zig | 38++++++++++++++++++++++++++++++++++++++
Abuild.zig.zon | 17+++++++++++++++++
Asrc/argparse.zig | 508+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/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]); + } +}