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 }