zigup.zig 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. const std = @import("std");
  2. const builtin = std.builtin;
  3. const mem = std.mem;
  4. const ArrayList = std.ArrayList;
  5. const Allocator = mem.Allocator;
  6. const ziget = @import("ziget");
  7. var globalOptionalInstallDir : ?[]const u8 = null;
  8. var globalOptionalPathLink : ?[]const u8 = null;
  9. fn find_zigs(allocator: *Allocator) !?[][]u8 {
  10. const ziglist = std.ArrayList([]u8).init(allocator);
  11. // don't worry about free for now, this is a short lived program
  12. if (builtin.os.tag == .windows) {
  13. @panic("windows not implemented");
  14. //const result = try runGetOutput(allocator, .{"where", "-a", "zig"});
  15. } else {
  16. const whichResult = try cmdlinetool.runGetOutput(allocator, .{"which", "zig"});
  17. if (runutil.runFailed(&whichResult)) {
  18. return null;
  19. }
  20. if (whichResult.stderr.len > 0) {
  21. std.debug.warn("which command failed with:\n{}\n", .{whichResult.stderr});
  22. std.os.exit(1);
  23. }
  24. std.debug.warn("which output:\n{}\n", .{whichResult.stdout});
  25. {var i = std.mem.split(whichResult.stdout, "\n"); while (i.next()) |dir| {
  26. std.debug.warn("path '{}'\n", .{dir});
  27. }}
  28. }
  29. @panic("not impl");
  30. }
  31. fn download(allocator: *Allocator, url: []const u8, writer: anytype) !void {
  32. var downloadOptions = ziget.request.DownloadOptions {
  33. .flags = 0,
  34. .allocator = allocator,
  35. .maxRedirects = 10,
  36. .forwardBufferSize = 4096,
  37. .maxHttpResponseHeaders = 8192,
  38. .onHttpRequest = ignoreHttpCallback,
  39. .onHttpResponse = ignoreHttpCallback,
  40. };
  41. var downloadState = ziget.request.DownloadState.init();
  42. try ziget.request.download(
  43. ziget.url.parseUrl(url) catch unreachable,
  44. writer,
  45. downloadOptions,
  46. &downloadState,
  47. );
  48. }
  49. fn downloadToFileAbsolute(allocator: *Allocator, url: []const u8, fileAbsolute: []const u8) !void {
  50. const file = try std.fs.createFileAbsolute(fileAbsolute, .{});
  51. defer file.close();
  52. try download(allocator, url, file.outStream());
  53. }
  54. fn downloadToString(allocator: *Allocator, url: []const u8) ![]u8 {
  55. var responseArrayList = try ArrayList(u8).initCapacity(allocator, 20 * 1024); // 20 KB (modify if response is expected to be bigger)
  56. errdefer responseArrayList.deinit();
  57. try download(allocator, url, responseArrayList.outStream());
  58. return responseArrayList.toOwnedSlice();
  59. }
  60. fn ignoreHttpCallback(request: []const u8) void { }
  61. fn makeInstallDirString(allocator: *Allocator) ![]const u8 {
  62. // TODO: maybe support ZIG_INSTALL_DIR environment variable?
  63. // TODO: maybe support a file on the filesystem to configure install dir?
  64. const home = std.os.getenv("HOME") orelse {
  65. std.debug.warn("Error: cannot find install directory, $HOME environment variable is not set\n", .{});
  66. return error.MissingHomeEnvironmentVariable;
  67. };
  68. if (!std.fs.path.isAbsolute(home)) {
  69. std.debug.warn("Error: $HOME environment variable '{}' is not an absolute path\n", .{home});
  70. return error.BadHomeEnvironmentVariable;
  71. }
  72. return std.fs.path.join(allocator, &[_][]const u8 {home, "zig"});
  73. }
  74. fn getAndCreateInstallDir(allocator: *Allocator) ![]const u8 {
  75. var optionalDirToFreeOnError : ?[]const u8 = null;
  76. errdefer if (optionalDirToFreeOnError) |dir| allocator.free(dir);
  77. const installDir = init: {
  78. if (globalOptionalInstallDir) |dir| break :init dir;
  79. optionalDirToFreeOnError = try makeInstallDirString(allocator);
  80. break :init optionalDirToFreeOnError.?;
  81. };
  82. std.debug.assert(std.fs.path.isAbsolute(installDir));
  83. std.debug.warn("install directory '{}'\n", .{installDir});
  84. loggyMakeDirAbsolute(installDir) catch |e| switch (e) {
  85. error.PathAlreadyExists => {},
  86. else => return e,
  87. };
  88. return installDir;
  89. }
  90. fn makeZigPathLinkString(allocator: *Allocator) ![]const u8 {
  91. if (globalOptionalPathLink) |path| return path;
  92. // for now we're just going to hardcode the path to $HOME/bin/zig
  93. const home = std.os.getenv("HOME") orelse {
  94. std.debug.warn("Error: cannot find install directory, $HOME environment variable is not set\n", .{});
  95. return error.MissingHomeEnvironmentVariable;
  96. };
  97. return try std.fs.path.join(allocator, &[_][]const u8 {home, "bin", "zig"});
  98. }
  99. // TODO: this should be in standard lib
  100. fn toAbsolute(allocator: *Allocator, path: []const u8) ![] u8 {
  101. std.debug.assert(!std.fs.path.isAbsolute(path));
  102. const cwd = try std.process.getCwdAlloc(allocator);
  103. defer allocator.free(cwd);
  104. return std.fs.path.join(allocator, &[_][]const u8 {cwd, path});
  105. }
  106. fn help() void {
  107. std.io.getStdErr().writeAll(
  108. \\Download and manage zig compilers.
  109. \\
  110. \\Common Usage:
  111. \\
  112. \\ zigup VERSION download and set VERSION compiler as default
  113. \\ zigup fetch VERSION download VERSION compiler
  114. \\ zigup default [VERSION] get or set the default compiler
  115. \\
  116. \\Uncommon Usage:
  117. \\
  118. \\ zigup fetch-index download and print the download index json
  119. \\
  120. \\Common Options:
  121. \\ --install-dir DIR override the default install location
  122. \\ --path-link PATH path to the `zig` symlink that points to the default compiler
  123. \\ this will typically be a file path within a PATH directory so
  124. \\ that the user can just run `zig`
  125. ) catch unreachable;
  126. }
  127. fn getCmdOpt(args: [][]const u8, i: *usize) ![]const u8 {
  128. i.* += 1;
  129. if (i.* == args.len) {
  130. std.debug.warn("Error: option '{}' requires an argument\n", .{args[i.* - 1]});
  131. return error.AlreadyReported;
  132. }
  133. return args[i.*];
  134. }
  135. pub fn main() !u8 {
  136. return main2() catch |e| switch (e) {
  137. error.AlreadyReported => return 1,
  138. else => return e,
  139. };
  140. }
  141. pub fn main2() !u8 {
  142. var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
  143. const allocator = &arena.allocator;
  144. const argsArray = try std.process.argsAlloc(allocator);
  145. // no need to free, os will do it
  146. //defer std.process.argsFree(allocator, argsArray);
  147. var args = if (argsArray.len == 0) argsArray else argsArray[1..];
  148. //
  149. // parse common options
  150. //
  151. {
  152. var i : usize = 0;
  153. var newlen : usize = 0;
  154. while (i < args.len) : (i += 1) {
  155. const arg = args[i];
  156. if (std.mem.eql(u8, "--install-dir", arg)) {
  157. globalOptionalInstallDir = try getCmdOpt(args, &i);
  158. if (!std.fs.path.isAbsolute(globalOptionalInstallDir.?)) {
  159. globalOptionalInstallDir = try toAbsolute(allocator, globalOptionalInstallDir.?);
  160. }
  161. } else if (std.mem.eql(u8, "--path-link", arg)) {
  162. globalOptionalPathLink = try getCmdOpt(args, &i);
  163. if (!std.fs.path.isAbsolute(globalOptionalPathLink.?)) {
  164. globalOptionalPathLink = try toAbsolute(allocator, globalOptionalPathLink.?);
  165. }
  166. } else {
  167. args[newlen] = args[i];
  168. newlen += 1;
  169. }
  170. }
  171. args = args[0..newlen];
  172. }
  173. if (args.len == 0) {
  174. help();
  175. return 1;
  176. }
  177. if (std.mem.eql(u8, "fetch-index", args[0])) {
  178. if (args.len != 1) {
  179. std.debug.warn("Error: 'index' command requires 0 arguments but got {}\n", .{args.len - 1});
  180. return 1;
  181. }
  182. var downloadIndex = try fetchDownloadIndex(allocator);
  183. defer downloadIndex.deinit(allocator);
  184. try std.io.getStdOut().writeAll(downloadIndex.text);
  185. return 0;
  186. }
  187. if (std.mem.eql(u8, "fetch", args[0])) {
  188. if (args.len != 2) {
  189. std.debug.warn("Error: 'fetch' command requires 1 argument but got {}\n", .{args.len - 1});
  190. return 1;
  191. }
  192. try fetchCompiler(allocator, args[1], .leaveDefault);
  193. return 0;
  194. }
  195. if (std.mem.eql(u8, "list", args[0])) {
  196. if (args.len != 1) {
  197. std.debug.warn("Error: 'list' command requires 0 arguments but got {}\n", .{args.len - 1});
  198. return 1;
  199. }
  200. try listCompilers(allocator);
  201. return 0;
  202. }
  203. if (std.mem.eql(u8, "default", args[0])) {
  204. if (args.len == 1) {
  205. try printDefaultCompiler(allocator);
  206. return 0;
  207. }
  208. if (args.len == 2) {
  209. const versionString = args[1];
  210. const installDir = try getAndCreateInstallDir(allocator);
  211. defer allocator.free(installDir);
  212. const compilerDir = try std.fs.path.join(allocator, &[_][]const u8 {installDir, versionString});
  213. defer allocator.free(compilerDir);
  214. if (std.mem.eql(u8, versionString, "latest")) {
  215. @panic("set default to latest not implemented");
  216. } else {
  217. try setDefaultCompiler(allocator, compilerDir);
  218. }
  219. return 0;
  220. }
  221. std.debug.warn("Error: 'default' command requires 1 or 2 arguments but got {}\n", .{args.len - 1});
  222. return 1;
  223. }
  224. if (args.len == 1) {
  225. try fetchCompiler(allocator, args[0], .setDefault);
  226. return 0;
  227. }
  228. const command = args[0];
  229. args = args[1..];
  230. std.debug.warn("command not impl '{}'\n", .{command});
  231. return 1;
  232. //const optionalInstallPath = try find_zigs(allocator);
  233. }
  234. const SetDefault = enum { setDefault, leaveDefault };
  235. fn fetchCompiler(allocator: *Allocator, versionArg: []const u8, setDefault: SetDefault) !void {
  236. const installDir = try getAndCreateInstallDir(allocator);
  237. defer allocator.free(installDir);
  238. var optionalDownloadIndex : ?DownloadIndex = null;
  239. // This is causing an LLVM error
  240. //defer if (optionalDownloadIndex) |_| optionalDownloadIndex.?.deinit(allocator);
  241. // Also I would rather do this, but it doesn't work because of const issues
  242. //defer if (optionalDownloadIndex) |downloadIndex| downloadIndex.deinit(allocator);
  243. const VersionUrl = struct { version: []const u8, url: []const u8 };
  244. const latest = std.mem.eql(u8, versionArg, "latest");
  245. const versionUrl = blk: {
  246. if (!latest)
  247. break :blk VersionUrl { .version = versionArg, .url = try getDefaultUrl(allocator, versionArg) };
  248. optionalDownloadIndex = try fetchDownloadIndex(allocator);
  249. const master = optionalDownloadIndex.?.json.root.Object.get("master").?;
  250. const compilerVersion = master.Object.get("version").?.String;
  251. const masterLinux = master.Object.get("x86_64-linux").?;
  252. const masterLinuxTarball = masterLinux.Object.get("tarball").?.String;
  253. break :blk VersionUrl { .version = compilerVersion, .url = masterLinuxTarball };
  254. };
  255. const compilerDir = try std.fs.path.join(allocator, &[_][]const u8 {installDir, versionUrl.version});
  256. defer allocator.free(compilerDir);
  257. try installCompiler(allocator, compilerDir, versionUrl.url);
  258. if (latest) {
  259. const latestSymlink = try std.fs.path.join(allocator, &[_][]const u8 {installDir, "latest"});
  260. defer allocator.free(latestSymlink);
  261. _ = try loggyUpdateSymlink(versionUrl.version, latestSymlink, .{.is_directory=true});
  262. }
  263. if (setDefault == .setDefault) {
  264. try setDefaultCompiler(allocator, compilerDir);
  265. }
  266. }
  267. const downloadIndexUrl = "https://ziglang.org/download/index.json";
  268. const DownloadIndex = struct {
  269. text: []u8,
  270. json: std.json.ValueTree,
  271. pub fn deinit(self: *DownloadIndex, allocator: *Allocator) void {
  272. self.json.deinit();
  273. allocator.free(self.text);
  274. }
  275. };
  276. fn fetchDownloadIndex(allocator: *Allocator) !DownloadIndex {
  277. const text = downloadToString(allocator, downloadIndexUrl) catch |e| switch (e) {
  278. else => {
  279. std.debug.warn("failed to download '{}': {}\n", .{downloadIndexUrl, e});
  280. return e;
  281. },
  282. };
  283. errdefer allocator.free(text);
  284. var json = init: {
  285. var parser = std.json.Parser.init(allocator, false);
  286. defer parser.deinit();
  287. break :init try parser.parse(text);
  288. };
  289. errdefer json.deinit();
  290. return DownloadIndex { .text = text, .json = json };
  291. }
  292. fn loggyMakeDirAbsolute(dirAbsolute: []const u8) !void {
  293. if (builtin.os.tag == .windows) {
  294. std.debug.warn("mkdir \"{}\"\n", .{dirAbsolute});
  295. } else {
  296. std.debug.warn("mkdir '{}'\n", .{dirAbsolute});
  297. }
  298. try std.fs.makeDirAbsolute(dirAbsolute);
  299. }
  300. fn loggyDeleteTreeAbsolute(dirAbsolute: []const u8) !void {
  301. if (builtin.os.tag == .windows) {
  302. std.debug.warn("rd /s /q \"{}\"\n", .{dirAbsolute});
  303. } else {
  304. std.debug.warn("rm -rf '{}'\n", .{dirAbsolute});
  305. }
  306. try std.fs.deleteTreeAbsolute(dirAbsolute);
  307. }
  308. pub fn loggyRenameAbsolute(old_path: []const u8, new_path: []const u8) !void {
  309. std.debug.warn("mv '{}' '{}'\n", .{old_path, new_path});
  310. try std.fs.renameAbsolute(old_path, new_path);
  311. }
  312. pub fn loggySymlinkAbsolute(target_path: []const u8, sym_link_path: []const u8, flags: std.fs.SymLinkFlags) !void {
  313. std.debug.warn("ln -s '{}' '{}'\n", .{target_path, sym_link_path});
  314. // NOTE: can't use symLinkAbsolute because it requires target_path to be absolute but we don't want that
  315. // not sure if it is a bug in the standard lib or not
  316. //try std.fs.symLinkAbsolute(target_path, sym_link_path, flags);
  317. try std.os.symlink(target_path, sym_link_path);
  318. }
  319. /// returns: true if the symlink was updated, false if it was already set to the given `target_path`
  320. pub fn loggyUpdateSymlink(target_path: []const u8, sym_link_path: []const u8, flags: std.fs.SymLinkFlags) !bool {
  321. var current_target_path_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
  322. if (std.fs.readLinkAbsolute(sym_link_path, &current_target_path_buffer)) |current_target_path| {
  323. if (std.mem.eql(u8, target_path, current_target_path)) {
  324. std.debug.warn("symlink '{}' already points to '{}'\n", .{sym_link_path, target_path});
  325. return false; // already up-to-date
  326. }
  327. try std.os.unlink(sym_link_path);
  328. } else |e| switch (e) {
  329. error.FileNotFound => {},
  330. else => return e,
  331. }
  332. try loggySymlinkAbsolute(target_path, sym_link_path, flags);
  333. return true; // updated
  334. }
  335. // TODO: this should be in std lib somewhere
  336. fn existsAbsolute(absolutePath: []const u8) !bool {
  337. std.fs.cwd().access(absolutePath, .{}) catch |e| switch (e) {
  338. error.FileNotFound => return false,
  339. error.PermissionDenied => return e,
  340. error.InputOutput => return e,
  341. error.SystemResources => return e,
  342. error.SymLinkLoop => return e,
  343. error.FileBusy => return e,
  344. error.Unexpected => unreachable,
  345. error.InvalidUtf8 => unreachable,
  346. error.ReadOnlyFileSystem => unreachable,
  347. error.NameTooLong => unreachable,
  348. error.BadPathName => unreachable,
  349. };
  350. return true;
  351. }
  352. fn listCompilers(allocator: *Allocator) !void {
  353. const installDirString = try makeInstallDirString(allocator);
  354. defer allocator.free(installDirString);
  355. var installDir = std.fs.cwd().openDir(installDirString, .{.iterate=true}) catch |e| switch (e) {
  356. error.FileNotFound => return,
  357. else => return e,
  358. };
  359. defer installDir.close();
  360. const stdout = std.io.getStdOut().writer();
  361. {var it = installDir.iterate(); while (try it.next()) |entry| {
  362. if (entry.kind != .Directory)
  363. continue;
  364. if (std.mem.endsWith(u8, entry.name, ".installing"))
  365. continue;
  366. try stdout.print("{}\n", .{entry.name});
  367. }}
  368. }
  369. fn printDefaultCompiler(allocator: *Allocator) !void {
  370. const pathLink = try makeZigPathLinkString(allocator);
  371. defer allocator.free(pathLink);
  372. var targetPathBuffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
  373. if (std.fs.readLinkAbsolute(pathLink, &targetPathBuffer)) |targetPath| {
  374. std.debug.warn("{}", .{
  375. std.fs.path.basename(std.fs.path.dirname(std.fs.path.dirname(targetPath).?).?)});
  376. } else |e| switch (e) {
  377. error.FileNotFound => {
  378. std.debug.warn("<no-default>", .{});
  379. },
  380. else => return e,
  381. }
  382. }
  383. fn setDefaultCompiler(allocator: *Allocator, compilerDir: []const u8) !void {
  384. const pathLink = try makeZigPathLinkString(allocator);
  385. defer allocator.free(pathLink);
  386. const linkTarget = try std.fs.path.join(allocator, &[_][]const u8 {compilerDir, "files", "zig"});
  387. defer allocator.free(linkTarget);
  388. if (builtin.os.tag == .windows) {
  389. // TODO: create zig.bat file
  390. @panic("setDefaultCompiler not implemented in Windows");
  391. } else {
  392. _ = try loggyUpdateSymlink(linkTarget, pathLink, .{});
  393. }
  394. }
  395. fn getDefaultUrl(allocator: *Allocator, compilerVersion: []const u8) ![]const u8 {
  396. return try std.fmt.allocPrint(allocator, "https://ziglang.org/download/{}/zig-linux-x86_64-{}.tar.xz",
  397. .{compilerVersion, compilerVersion});
  398. }
  399. fn installCompiler(allocator: *Allocator, compilerDir: []const u8, url: []const u8) !void {
  400. if (try existsAbsolute(compilerDir)) {
  401. std.debug.warn("compiler '{}' already installed\n", .{compilerDir});
  402. return;
  403. }
  404. const installingDir = try std.mem.concat(allocator, u8, &[_][]const u8 {compilerDir, ".installing"});
  405. defer allocator.free(installingDir);
  406. try loggyDeleteTreeAbsolute(installingDir);
  407. try loggyMakeDirAbsolute(installingDir);
  408. const archiveBasename = std.fs.path.basename(url);
  409. var archiveRootDir : []const u8 = undefined;
  410. // download and extract archive
  411. {
  412. const archiveAbsolute = try std.fs.path.join(allocator, &[_][]const u8 {installingDir, archiveBasename});
  413. defer allocator.free(archiveAbsolute);
  414. std.debug.warn("downloading '{}' to '{}'\n", .{url, archiveAbsolute});
  415. downloadToFileAbsolute(allocator, url, archiveAbsolute) catch |e| switch (e) {
  416. error.HttpNon200StatusCode => {
  417. // TODO: more information would be good
  418. std.debug.warn("HTTP request failed (TODO: improve ziget library to get better error)\n", .{});
  419. return error.AlreadyReported;
  420. },
  421. else => return e,
  422. };
  423. if (std.mem.endsWith(u8, archiveBasename, ".tar.xz")) {
  424. archiveRootDir = archiveBasename[0.. archiveBasename.len - ".tar.xz".len];
  425. _ = try run(allocator, &[_][]const u8 {"tar", "xf", archiveAbsolute, "-C", installingDir});
  426. } else {
  427. std.debug.warn("Error: unknown archive extension '{}'\n", .{archiveBasename});
  428. return error.UnknownArchiveExtension;
  429. }
  430. try loggyDeleteTreeAbsolute(archiveAbsolute);
  431. }
  432. {
  433. const extractedDir = try std.fs.path.join(allocator, &[_][]const u8 {installingDir, archiveRootDir});
  434. defer allocator.free(extractedDir);
  435. const normalizedDir = try std.fs.path.join(allocator, &[_][]const u8 {installingDir, "files"});
  436. defer allocator.free(normalizedDir);
  437. try loggyRenameAbsolute(extractedDir, normalizedDir);
  438. }
  439. // TODO: write date information (so users can sort compilers by date)
  440. // finish installation by renaming the install dir
  441. try loggyRenameAbsolute(installingDir, compilerDir);
  442. }
  443. pub fn run(allocator: *std.mem.Allocator, argv: []const []const u8) !std.ChildProcess.Term {
  444. try logRun(allocator, argv);
  445. var proc = try std.ChildProcess.init(argv, allocator);
  446. defer proc.deinit();
  447. return proc.spawnAndWait();
  448. }
  449. fn logRun(allocator: *std.mem.Allocator, argv: []const []const u8) !void {
  450. var buffer = try allocator.alloc(u8, getCommandStringLength(argv));
  451. defer allocator.free(buffer);
  452. var prefix = false;
  453. var offset : usize = 0;
  454. for (argv) |arg| {
  455. if (prefix) {
  456. buffer[offset] = ' ';
  457. offset += 1;
  458. } else {
  459. prefix = true;
  460. }
  461. std.mem.copy(u8, buffer[offset..offset + arg.len], arg);
  462. offset += arg.len;
  463. }
  464. std.debug.assert(offset == buffer.len);
  465. std.debug.warn("[RUN] {}\n", .{buffer});
  466. }
  467. pub fn getCommandStringLength(argv: []const []const u8) usize {
  468. var len : usize = 0;
  469. var prefixLength : u8 = 0;
  470. for (argv) |arg| {
  471. len += prefixLength + arg.len;
  472. prefixLength = 1;
  473. }
  474. return len;
  475. }
  476. pub fn appendCommandString(appender: *appendlib.Appender(u8), argv: []const []const u8) void {
  477. var prefix : []const u8 = "";
  478. for (argv) |arg| {
  479. appender.appendSlice(prefix);
  480. appender.appendSlice(arg);
  481. prefix = " ";
  482. }
  483. }