zigvm

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

main.zig (12039B)


      1 const std = @import("std");
      2 const builtin = @import("builtin");
      3 const ArgParse = @import("./argparse.zig");
      4 const minisign = @import("minisign");
      5 
      6 pub fn main() !void {
      7     var gpa = std.heap.GeneralPurposeAllocator(.{}){};
      8     defer _ = gpa.deinit();
      9     const a = gpa.allocator();
     10 
     11     const path = try std.process.getEnvVarOwned(a, "PATH");
     12     defer a.free(path);
     13 
     14     const home = try std.process.getEnvVarOwned(a, "HOME");
     15     defer a.free(home);
     16     const defaultInstallDir = try std.fs.path.join(a, &.{ home, ".local", "bin" });
     17     defer a.free(defaultInstallDir);
     18     const defaultCacheDir = try std.fs.path.join(a, &.{ home, ".zigvm" });
     19     defer a.free(defaultCacheDir);
     20 
     21     // Argument parsing...
     22     var ap = ArgParse.init(a, "zigvm", "Manage installed zig versions");
     23     defer ap.deinit();
     24     var versionArg: ArgParse.Positional = .{
     25         .name = "version",
     26         .description = "Version of zig to download",
     27         .default = "master",
     28     };
     29     try ap.addPositional(&versionArg);
     30     var installDirFlag: ArgParse.Flag = .{
     31         .long = "install-dir",
     32         .short = "i",
     33         .description = "zig installation directory",
     34         .hasarg = true,
     35         .defaultargvalue = defaultInstallDir,
     36     };
     37     try ap.addFlag(&installDirFlag);
     38     var cacheDirFlag: ArgParse.Flag = .{
     39         .long = "cache-dir",
     40         .short = "c",
     41         .description = "zigvm cache directory",
     42         .hasarg = true,
     43         .defaultargvalue = defaultCacheDir,
     44     };
     45     try ap.addFlag(&cacheDirFlag);
     46     var listFlag: ArgParse.Flag = .{
     47         .long = "list",
     48         .short = "l",
     49         .description = "List available zig versions",
     50         .hasarg = false,
     51     };
     52     try ap.addFlag(&listFlag);
     53     if (!try ap.parseOrHelp()) {
     54         return;
     55     }
     56     const installDirPath = installDirFlag.argvalue orelse return error.MissingArg;
     57     const cacheDirPath = cacheDirFlag.argvalue orelse return error.MissingArg;
     58     const version = versionArg.value orelse return error.MissingArg;
     59     const currentDirName = "current";
     60 
     61     // Check the install dir is present in PATH, warn if it isn't
     62     var toks = std.mem.splitScalar(u8, path, ':');
     63     while (toks.next()) |nxt| {
     64         if (std.mem.eql(u8, nxt, installDirPath)) {
     65             break;
     66         }
     67     } else {
     68         std.log.warn("Zig installation directory {s} not found in PATH {s}", .{ installDirPath, path });
     69     }
     70 
     71     const tuple = @tagName(builtin.target.cpu.arch) ++ "-" ++ @tagName(builtin.target.os.tag);
     72     std.log.info("system: {s}", .{tuple});
     73 
     74     // Ensure the cacheDir is present
     75     var cacheDir: std.fs.Dir = undefined;
     76     if (std.fs.openDirAbsolute(cacheDirPath, .{ .iterate = true })) |cd| {
     77         cacheDir = cd;
     78     } else |_| {
     79         try std.fs.makeDirAbsolute(cacheDirPath);
     80         cacheDir = try std.fs.openDirAbsolute(cacheDirPath, .{ .iterate = true });
     81     }
     82 
     83     var indexFile = try cacheDir.createFile("index.json", .{ .read = true, .truncate = false });
     84     defer indexFile.close();
     85 
     86     var client = std.http.Client{ .allocator = a };
     87     defer client.deinit();
     88     var cwb = ClientWithBuffer{
     89         .client = &client,
     90     };
     91     std.log.info("refreshing index", .{});
     92     refreshIndex(&cwb, &indexFile) catch |e| {
     93         const ifs = try indexFile.stat();
     94         if (ifs.size == 0) {
     95             return error.FailedToDownloadIndex;
     96         } else {
     97             std.log.warn("Unable to refresh index: {}", .{e});
     98         }
     99     };
    100 
    101     // Parse the index
    102     var rdr = std.json.reader(a, indexFile.reader());
    103     defer rdr.deinit();
    104     var doc = try std.json.parseFromTokenSource(std.json.Value, a, &rdr, .{});
    105     defer doc.deinit();
    106 
    107     if (listFlag.waspresent) {
    108         // List the files present in .zigvm, store in a hashmap
    109         var hm: std.StringHashMapUnmanaged(void) = .{};
    110         defer {
    111             var i = hm.iterator();
    112             while (i.next()) |*e| {
    113                 a.free(e.key_ptr.*);
    114             }
    115             hm.deinit(a);
    116         }
    117         var it = cacheDir.iterate();
    118         while (try it.next()) |e| {
    119             try hm.put(a, try a.dupe(u8, e.name), undefined);
    120         }
    121         // Check what the 'current' directory is symlinked to
    122         const currentLink = try cacheDir.realpathAlloc(a, currentDirName);
    123         defer a.free(currentLink);
    124         const currentLinkBasename = std.fs.path.basename(currentLink);
    125         const currentTarball = try std.fmt.allocPrint(a, "{s}.tar.xz", .{currentLinkBasename});
    126         defer a.free(currentTarball);
    127         const wtr = std.io.getStdOut().writer();
    128         var vit = doc.value.object.iterator();
    129         while (vit.next()) |e| {
    130             const versionObj = e.value_ptr.object;
    131             const versionObjForArch = versionObj.get(tuple) orelse continue;
    132             const tarballUrl = versionObjForArch.object.get("tarball").?.string;
    133             const tarballName = try baseNameFromUrl(a, tarballUrl);
    134             defer a.free(tarballName);
    135             const isPresent = hm.contains(tarballName);
    136             const isCurrent = std.mem.eql(u8, tarballName, currentTarball);
    137             try std.fmt.format(wtr, "version: {s}", .{e.key_ptr.*});
    138             if (versionObj.get("version")) |vs| {
    139                 try std.fmt.format(wtr, " ({s})", .{vs.string});
    140             }
    141             if (isPresent) {
    142                 try wtr.writeAll(", downloaded");
    143             }
    144             if (isCurrent) {
    145                 try wtr.writeAll(", current");
    146             }
    147             try wtr.writeByte('\n');
    148         }
    149         return;
    150     }
    151 
    152     const versionDoc: std.json.Value = doc.value.object.get(version) orelse return error.ZigVersionNotFound;
    153     const versionString = if (versionDoc.object.get("version")) |versionObj| versionObj.string else version;
    154     std.log.info("version {s} mapped to {s}", .{ version, versionString });
    155 
    156     // Fetch the version if necessary
    157     const systemVersion = versionDoc.object.get(tuple) orelse return error.SystemNotFound;
    158     const tarballUrlObj = systemVersion.object.get("tarball") orelse return error.InvalidIndex;
    159     const tarballHashObj = systemVersion.object.get("shasum") orelse return error.InvalidIndex;
    160     const tarballHash = tarballHashObj.string;
    161     const tarballUrl = tarballUrlObj.string;
    162     std.log.info("tarballUrl: {s}", .{tarballUrl});
    163     if (!std.mem.endsWith(u8, tarballUrl, ".tar.xz")) {
    164         return error.UnsupportedFileFormat;
    165     }
    166 
    167     const tarballBasename = try baseNameFromUrl(a, tarballUrl);
    168     std.log.info("basename: {s}", .{tarballBasename});
    169     const tarballPath = try std.fs.path.join(a, &.{ cacheDirPath, tarballBasename });
    170     defer a.free(tarballPath);
    171     const tarballMinisigUrl = try std.fmt.allocPrint(a, "{s}.minisig", .{tarballUrl});
    172     defer a.free(tarballMinisigUrl);
    173     const tarballMinisigFilePath = try std.fmt.allocPrint(a, "{s}.minisig", .{tarballPath});
    174     defer a.free(tarballMinisigFilePath);
    175 
    176     var tarball = try std.fs.createFileAbsolute(tarballPath, .{ .read = true, .truncate = false });
    177     var tarballStat = try tarball.stat();
    178 
    179     if (tarballStat.size != 0) {
    180         hashAndSigCheck(a, tarball, tarballHash, tarballMinisigFilePath) catch {
    181             try tarball.setEndPos(0);
    182             tarballStat.size = 0;
    183         };
    184     }
    185 
    186     if (tarballStat.size == 0) {
    187         {
    188             std.log.info("downloading from {s} to {s}", .{ tarballUrl, tarballPath });
    189             try downloadToFile(&cwb, tarballUrl, tarball);
    190             try tarball.seekTo(0);
    191 
    192             const minisigFile = try std.fs.createFileAbsolute(tarballMinisigFilePath, .{});
    193             defer minisigFile.close();
    194             try downloadToFile(&cwb, tarballMinisigUrl, minisigFile);
    195         }
    196 
    197         hashAndSigCheck(a, tarball, tarballHash, tarballMinisigFilePath) catch {
    198             return error.InvalidDownload;
    199         };
    200     }
    201 
    202     const extractedTarballDir = tarballBasename[0 .. tarballBasename.len - ".tar.xz".len];
    203     cacheDir.access(extractedTarballDir, .{}) catch |e| switch (e) {
    204         error.FileNotFound => {
    205             std.log.info("extracting: {s}", .{tarballPath});
    206             try tarball.seekTo(0);
    207             var xr = try std.compress.xz.decompress(a, tarball.reader());
    208             defer xr.deinit();
    209             try std.tar.pipeToFileSystem(cacheDir, xr.reader(), .{});
    210         },
    211         else => {
    212             return e;
    213         },
    214     };
    215 
    216     // then symlink the selected dir to 'current'
    217 
    218     cacheDir.deleteFile(currentDirName) catch |e| switch (e) {
    219         error.FileNotFound => {},
    220         else => return e,
    221     };
    222     try cacheDir.symLink(extractedTarballDir, currentDirName, .{ .is_directory = true });
    223 
    224     // then symlink the zig executable in 'current' to installDir
    225     var installDir = try std.fs.openDirAbsolute(installDirPath, .{});
    226     defer installDir.close();
    227     const currentZigPath = try std.fs.path.join(a, &.{ cacheDirPath, currentDirName, "zig" });
    228     defer a.free(currentZigPath);
    229     installDir.deleteFile("zig") catch |e| switch (e) {
    230         error.FileNotFound => {},
    231         else => return e,
    232     };
    233     try installDir.symLink(currentZigPath, "zig", .{});
    234     // EZ PZ
    235     std.log.info("done!", .{});
    236 }
    237 
    238 // Caller owns the result and must free it with a.
    239 fn baseNameFromUrl(a: std.mem.Allocator, url: []const u8) ![]const u8 {
    240     const tarballUri = try std.Uri.parse(url);
    241     const tarballUriPath = try std.fmt.allocPrint(a, "{path}", .{tarballUri.path});
    242     defer a.free(tarballUriPath);
    243     const tarballBasename = std.fs.path.basename(tarballUriPath);
    244     return try a.dupe(u8, tarballBasename);
    245 }
    246 
    247 const ClientWithBuffer = struct {
    248     client: *std.http.Client,
    249     buffer: [std.mem.page_size]u8 = .{0} ** std.mem.page_size,
    250 
    251     fn open(self: *ClientWithBuffer, method: std.http.Method, uri: std.Uri) !std.http.Client.Request {
    252         return try self.client.open(method, uri, .{ .server_header_buffer = &self.buffer });
    253     }
    254 };
    255 
    256 fn hashAndSigCheck(a: std.mem.Allocator, file: std.fs.File, sha256: []const u8, minisigFilePath: []const u8) !void {
    257     try sha256Check(file, sha256);
    258     // minisig is leaky, use an arena
    259     var aa = std.heap.ArenaAllocator.init(a);
    260     defer aa.deinit();
    261     const pk = minisign.PublicKey.decodeFromBase64("RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U") catch @panic("unexpected publickey");
    262     var sig = try minisign.Signature.fromFile(aa.allocator(), minisigFilePath);
    263     defer sig.deinit();
    264     try file.seekTo(0);
    265     try pk.verifyFile(aa.allocator(), file, sig, true);
    266 }
    267 
    268 fn sha256Check(file: std.fs.File, sha256: []const u8) !void {
    269     try file.seekTo(0);
    270     var hash = std.crypto.hash.sha2.Sha256.init(.{});
    271     try pump(file.reader(), hash.writer());
    272     const actual = hash.finalResult();
    273     const actualHex = std.fmt.bytesToHex(actual, .lower);
    274 
    275     if (!std.mem.eql(u8, sha256, &actualHex)) {
    276         std.log.warn("hash mismatch exp {s} act {s}", .{ sha256, &actualHex });
    277         return error.HashMismatch;
    278     }
    279 }
    280 
    281 fn downloadToFile(cwb: *ClientWithBuffer, url: []const u8, file: std.fs.File) !void {
    282     var req = try cwb.open(.GET, try std.Uri.parse(url));
    283     defer req.deinit();
    284     try req.send();
    285     try req.wait();
    286     if (req.response.status != .ok) {
    287         return error.HttpStatusError;
    288     }
    289     try file.seekTo(0);
    290     try pump(req.reader(), file.writer());
    291 }
    292 
    293 fn refreshIndex(client: *ClientWithBuffer, file: *std.fs.File) !void {
    294     const uri = std.Uri.parse("https://ziglang.org/download/index.json") catch unreachable;
    295     var req = try client.open(.GET, uri);
    296     defer req.deinit();
    297     try req.send();
    298     try req.wait();
    299     if (req.response.status != .ok) {
    300         std.log.err("http status {}", .{req.response.status});
    301         return error.IndexFetchHttpStatusError;
    302     }
    303     try file.setEndPos(0);
    304     try pump(&req, file.writer());
    305     try file.seekTo(0);
    306 }
    307 
    308 fn pump(reader: anytype, writer: anytype) !void {
    309     var buf = [_]u8{0} ** std.mem.page_size;
    310     while (true) {
    311         const sz = try reader.read(&buf);
    312         if (sz == 0) break;
    313         try writer.writeAll(buf[0..sz]);
    314     }
    315 }