From 7eaf2076208b9ee1f7c30f4c4586447657aa340a Mon Sep 17 00:00:00 2001 From: vent Date: Sat, 8 Feb 2025 13:48:19 +0000 Subject: [PATCH 1/8] Create and hook up Config struct This struct contains the configuration for Zigup. A global object `config` is initialized from command-line arguments and from reading in a ZON file. Its ensured that command-line arguments always overwrite ZON file values, and if neither are set then the default value is used. A global (`zigup.config`) is used for storing the final configuration because its pretty universal and gets used all over the place. Its expected that this value gets initialized once, before we start doing any serious work, and is never mutated again. Some shuffling around of the parsing was needed to be done, notably the check for the `run` command was moved after configuration is initialized. The `getInstallDir` and `makeZigPathLinkString` have been moved and renamed as functions within Config, and functions using those instead read from `zigup.config` directly. This has an added benefit of removing some allocations. --- zigup.zig | 287 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 161 insertions(+), 126 deletions(-) diff --git a/zigup.zig b/zigup.zig index 64e80fb..e3252ce 100644 --- a/zigup.zig +++ b/zigup.zig @@ -26,9 +26,6 @@ const url_platform = os ++ "-" ++ arch; const json_platform = arch ++ "-" ++ os; const archive_ext = if (builtin.os.tag == .windows) "zip" else "tar.xz"; -var global_optional_install_dir: ?[]const u8 = null; -var global_optional_path_link: ?[]const u8 = null; - var global_enable_log = true; fn loginfo(comptime fmt: []const u8, args: anytype) void { if (global_enable_log) { @@ -131,70 +128,18 @@ fn ignoreHttpCallback(request: []const u8) void { _ = request; } -fn allocInstallDirStringXdg(allocator: Allocator) ![]const u8 { - // see https://specifications.freedesktop.org/basedir-spec/latest/#variables - // try $XDG_DATA_HOME/zigup first - xdg_var: { - const xdg_data_home = std.posix.getenv("XDG_DATA_HOME") orelse break :xdg_var; - if (xdg_data_home.len == 0) break :xdg_var; - if (!std.fs.path.isAbsolute(xdg_data_home)) { - std.log.err("$XDG_DATA_HOME environment variable '{s}' is not an absolute path", .{xdg_data_home}); - return error.AlreadyReported; - } - return std.fs.path.join(allocator, &[_][]const u8{ xdg_data_home, "zigup" }); - } - // .. then fallback to $HOME/.local/share/zigup - const home = std.posix.getenv("HOME") orelse { - std.log.err("cannot find install directory, neither $HOME nor $XDG_DATA_HOME environment variables are set", .{}); - return error.AlreadyReported; +fn getHomeDir() ![]const u8 { + const home_dir = std.posix.getenv("HOME") orelse { + std.log.err("cannot find install directory, $HOME environment variable is not set", .{}); + return error.MissingHomeEnvironmentVariable; }; - if (!std.fs.path.isAbsolute(home)) { - std.log.err("$HOME environment variable '{s}' is not an absolute path", .{home}); - return error.AlreadyReported; - } - return std.fs.path.join(allocator, &[_][]const u8{ home, ".local", "share", "zigup" }); -} -fn allocInstallDirString(allocator: Allocator) ![]const u8 { - // TODO: maybe support ZIG_INSTALL_DIR environment variable? - // TODO: maybe support a file on the filesystem to configure install dir? - if (builtin.os.tag == .windows) { - const self_exe_dir = try std.fs.selfExeDirPathAlloc(allocator); - defer allocator.free(self_exe_dir); - return std.fs.path.join(allocator, &.{ self_exe_dir, "zig" }); + if (!std.fs.path.isAbsolute(home_dir)) { + std.log.err("$HOME environment variable '{s}' is not an absolute path", .{home_dir}); + return error.BadHomeEnvironmentVariable; } - return allocInstallDirStringXdg(allocator); -} -const GetInstallDirOptions = struct { - create: bool, -}; -fn getInstallDir(allocator: Allocator, options: GetInstallDirOptions) ![]const u8 { - var optional_dir_to_free_on_error: ?[]const u8 = null; - errdefer if (optional_dir_to_free_on_error) |dir| allocator.free(dir); - - const install_dir = init: { - if (global_optional_install_dir) |dir| break :init dir; - optional_dir_to_free_on_error = try allocInstallDirString(allocator); - break :init optional_dir_to_free_on_error.?; - }; - std.debug.assert(std.fs.path.isAbsolute(install_dir)); - loginfo("install directory '{s}'", .{install_dir}); - if (options.create) { - loggyMakePath(install_dir) catch |e| switch (e) { - error.PathAlreadyExists => {}, - else => return e, - }; - } - return install_dir; -} - -fn makeZigPathLinkString(allocator: Allocator) ![]const u8 { - if (global_optional_path_link) |path| return path; - const zigup_dir = try std.fs.selfExeDirPathAlloc(allocator); - defer allocator.free(zigup_dir); - - return try std.fs.path.join(allocator, &[_][]const u8{ zigup_dir, comptime "zig" ++ builtin.target.exeFileExt() }); + return home_dir; } // TODO: this should be in standard lib @@ -231,7 +176,7 @@ fn help() void { \\ that the user can just run `zig` \\ --index override the default index URL that zig versions/URLs are fetched from. \\ default: - ++ " " ++ default_index_url ++ + ++ " " ++ Config.default_index_url ++ \\ \\ ) catch unreachable; @@ -267,7 +212,11 @@ pub fn main2() !u8 { var args = if (args_array.len == 0) args_array else args_array[1..]; // parse common options - var index_url: []const u8 = default_index_url; + var config_args: Config = .{ + .install_dir = null, + .path_link = null, + .index = null, + }; { var i: usize = 0; @@ -275,40 +224,55 @@ pub fn main2() !u8 { while (i < args.len) : (i += 1) { const arg = args[i]; if (std.mem.eql(u8, "--install-dir", arg)) { - global_optional_install_dir = try getCmdOpt(args, &i); - if (!std.fs.path.isAbsolute(global_optional_install_dir.?)) { - global_optional_install_dir = try toAbsolute(allocator, global_optional_install_dir.?); - } + config_args.install_dir = try getCmdOpt(args, &i); } else if (std.mem.eql(u8, "--path-link", arg)) { - global_optional_path_link = try getCmdOpt(args, &i); - if (!std.fs.path.isAbsolute(global_optional_path_link.?)) { - global_optional_path_link = try toAbsolute(allocator, global_optional_path_link.?); - } + config_args.path_link = try getCmdOpt(args, &i); } else if (std.mem.eql(u8, "--index", arg)) { - index_url = try getCmdOpt(args, &i); + config_args.index = try getCmdOpt(args, &i); } else if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) { help(); return 0; } else { - if (newlen == 0 and std.mem.eql(u8, "run", arg)) { - return try runCompiler(allocator, args[i + 1 ..]); - } args[newlen] = args[i]; newlen += 1; } } args = args[0..newlen]; } + + const config_zon = try Config.initFromZon(allocator); + + // Order of precedence: CLI -> ZON -> Defaults + config = .{ + .install_dir = config_args.install_dir orelse + config_zon.install_dir orelse + try Config.allocDefaultInstallDir(allocator), + + .path_link = config_args.path_link orelse + config_zon.path_link orelse + try Config.allocDefaultPathLinkString(allocator), + + .index = config_args.index orelse + config_zon.index orelse + try allocator.dupe(u8, Config.default_index_url), + }; + try config.ensureValid(allocator); + if (args.len == 0) { help(); return 1; } + + if (std.mem.eql(u8, "run", args[0])) { + return try runCompiler(allocator, args[1..]); + } + if (std.mem.eql(u8, "fetch-index", args[0])) { if (args.len != 1) { std.log.err("'index' command requires 0 arguments but got {d}", .{args.len - 1}); return 1; } - var download_index = try fetchDownloadIndex(allocator, index_url); + var download_index = try fetchDownloadIndex(allocator, config.index.?); defer download_index.deinit(allocator); try std.io.getStdOut().writeAll(download_index.text); return 0; @@ -318,7 +282,7 @@ pub fn main2() !u8 { std.log.err("'fetch' command requires 1 argument but got {d}", .{args.len - 1}); return 1; } - try fetchCompiler(allocator, index_url, args[1], .leave_default); + try fetchCompiler(allocator, args[1], .leave_default); return 0; } if (std.mem.eql(u8, "clean", args[0])) { @@ -337,7 +301,7 @@ pub fn main2() !u8 { std.log.err("'keep' command requires 1 argument but got {d}", .{args.len - 1}); return 1; } - try keepCompiler(allocator, args[1]); + try keepCompiler(args[1]); return 0; } if (std.mem.eql(u8, "list", args[0])) { @@ -345,7 +309,7 @@ pub fn main2() !u8 { std.log.err("'list' command requires 0 arguments but got {d}", .{args.len - 1}); return 1; } - try listCompilers(allocator); + try listCompilers(); return 0; } if (std.mem.eql(u8, "default", args[0])) { @@ -355,14 +319,13 @@ pub fn main2() !u8 { } if (args.len == 2) { const version_string = args[1]; - const install_dir_string = try getInstallDir(allocator, .{ .create = true }); - defer allocator.free(install_dir_string); + try ensureDirExists(config.install_dir.?); const resolved_version_string = init_resolved: { if (!std.mem.eql(u8, version_string, "master")) break :init_resolved version_string; const optional_master_dir: ?[]const u8 = blk: { - var install_dir = std.fs.openDirAbsolute(install_dir_string, .{ .iterate = true }) catch |e| switch (e) { + var install_dir = std.fs.openDirAbsolute(config.install_dir.?, .{ .iterate = true }) catch |e| switch (e) { error.FileNotFound => break :blk null, else => return e, }; @@ -375,7 +338,7 @@ pub fn main2() !u8 { return 1; }; }; - const compiler_dir = try std.fs.path.join(allocator, &[_][]const u8{ install_dir_string, resolved_version_string }); + const compiler_dir = try std.fs.path.join(allocator, &[_][]const u8{ config.install_dir.?, resolved_version_string }); defer allocator.free(compiler_dir); try setDefaultCompiler(allocator, compiler_dir, .verify_existence); return 0; @@ -384,7 +347,7 @@ pub fn main2() !u8 { return 1; } if (args.len == 1) { - try fetchCompiler(allocator, index_url, args[0], .set_default); + try fetchCompiler(allocator, args[0], .set_default); return 0; } const command = args[0]; @@ -395,6 +358,95 @@ pub fn main2() !u8 { //const optionalInstallPath = try find_zigs(allocator); } +var config: Config = undefined; // Global config: Initialized on startup, after which no fields will be null +const Config = struct { + install_dir: ?[]const u8 = null, + path_link: ?[]const u8 = null, + index: ?[]const u8 = null, + + const default_index_url = "https://ziglang.org/download/index.json"; + + // Read configuration from a ZON file, if it exists. + fn initFromZon(allocator: Allocator) !Config { + // Read ZON config + const zon_path = try defaultZonPath(allocator); + const fd = std.fs.cwd().openFile(zon_path, .{ .mode = .read_only }) catch |err| { + switch (err) { + error.FileNotFound => return .{ + .install_dir = null, + .path_link = null, + .index = null, + }, + else => return err, + } + }; + const src = try fd.readToEndAllocOptions(allocator, 2048, null, 1, 0); + defer allocator.free(src); + var status = std.mem.zeroes(std.zon.parse.Status); + const zon = std.zon.parse.fromSlice(Config, allocator, src, &status, .{}) catch { + std.debug.print("{any}\n", .{status}); + return error.fail_parse; + }; + defer status.deinit(allocator); + + return zon; + } + + fn ensureValid(conf: *Config, allocator: Allocator) !void { + // Ensure we use absolute paths + if (!std.fs.path.isAbsolute(conf.install_dir.?)) + conf.install_dir = try toAbsolute(allocator, conf.install_dir.?); + if (!std.fs.path.isAbsolute(conf.path_link.?)) + conf.path_link = try toAbsolute(allocator, conf.path_link.?); + } + + fn allocDefaultInstallDir(allocator: Allocator) ![]const u8 { + if (builtin.os.tag == .windows) { + const self_exe_dir = try std.fs.selfExeDirPathAlloc(allocator); + defer allocator.free(self_exe_dir); + return std.fs.path.join(allocator, &.{ self_exe_dir, "zig" }); + } + + // see https://specifications.freedesktop.org/basedir-spec/latest/#variables + // try $XDG_DATA_HOME/zigup first + xdg_var: { + const xdg_data_home = std.posix.getenv("XDG_DATA_HOME") orelse break :xdg_var; + if (xdg_data_home.len == 0) break :xdg_var; + if (!std.fs.path.isAbsolute(xdg_data_home)) { + std.log.err("$XDG_DATA_HOME environment variable '{s}' is not an absolute path", .{xdg_data_home}); + return error.BadXdgDataHomeEnvironmentVariable; + } + return std.fs.path.join(allocator, &[_][]const u8{ xdg_data_home, "zigup" }); + } + // .. then fallback to $HOME/.local/share/zigup + return std.fs.path.join(allocator, &[_][]const u8{ try getHomeDir(), ".local", "share", "zigup" }); + } + + fn allocDefaultPathLinkString(allocator: Allocator) ![]const u8 { + const zigup_dir = try std.fs.selfExeDirPathAlloc(allocator); + defer allocator.free(zigup_dir); + + return try std.fs.path.join( + allocator, + &[_][]const u8{ zigup_dir, comptime "zig" ++ builtin.target.exeFileExt() }, + ); + } + + fn defaultZonPath(allocator: Allocator) ![]const u8 { + return switch (builtin.os.tag) { + .linux => std.fs.path.join(allocator, &[_][]const u8{ try getHomeDir(), ".config/zigup.zon" }), + else => @compileError("Not supported yet"), // TODO: Complete list + }; + } +}; + +fn ensureDirExists(path: []const u8) !void { + loggyMakePath(path) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return e, + }; +} + pub fn runCompiler(allocator: Allocator, args: []const []const u8) !u8 { // disable log so we don't add extra output to whatever the compiler will output global_enable_log = false; @@ -403,10 +455,9 @@ pub fn runCompiler(allocator: Allocator, args: []const []const u8) !u8 { return 1; } const version_string = args[0]; - const install_dir_string = try getInstallDir(allocator, .{ .create = true }); - defer allocator.free(install_dir_string); + try ensureDirExists(config.install_dir.?); - const compiler_dir = try std.fs.path.join(allocator, &[_][]const u8{ install_dir_string, version_string }); + const compiler_dir = try std.fs.path.join(allocator, &[_][]const u8{ config.install_dir.?, version_string }); defer allocator.free(compiler_dir); if (!try existsAbsolute(compiler_dir)) { std.log.err("compiler '{s}' does not exist, fetch it first with: zigup fetch {0s}", .{version_string}); @@ -433,13 +484,9 @@ const SetDefault = enum { set_default, leave_default }; fn fetchCompiler( allocator: Allocator, - index_url: []const u8, version_arg: []const u8, set_default: SetDefault, ) !void { - const install_dir = try getInstallDir(allocator, .{ .create = true }); - defer allocator.free(install_dir); - var optional_download_index: ?DownloadIndex = null; // This is causing an LLVM error //defer if (optionalDownloadIndex) |_| optionalDownloadIndex.?.deinit(allocator); @@ -453,20 +500,21 @@ fn fetchCompiler( const is_master = std.mem.eql(u8, version_arg, "master"); const version_url = blk: { // For default index_url we can build the url so we avoid downloading the index - if (!is_master and std.mem.eql(u8, default_index_url, index_url)) + if (!is_master and std.mem.eql(u8, Config.default_index_url, config.index.?)) break :blk VersionUrl{ .version = version_arg, .url = try getDefaultUrl(allocator, version_arg) }; - optional_download_index = try fetchDownloadIndex(allocator, index_url); + optional_download_index = try fetchDownloadIndex(allocator, config.index.?); const master = optional_download_index.?.json.value.object.get(version_arg).?; const compiler_version = master.object.get("version").?.string; const master_linux = master.object.get(json_platform).?; const master_linux_tarball = master_linux.object.get("tarball").?.string; break :blk VersionUrl{ .version = compiler_version, .url = master_linux_tarball }; }; - const compiler_dir = try std.fs.path.join(allocator, &[_][]const u8{ install_dir, version_url.version }); + const compiler_dir = try std.fs.path.join(allocator, &[_][]const u8{ config.install_dir.?, version_url.version }); defer allocator.free(compiler_dir); + try ensureDirExists(config.install_dir.?); try installCompiler(allocator, compiler_dir, version_url.url); if (is_master) { - const master_symlink = try std.fs.path.join(allocator, &[_][]const u8{ install_dir, "master" }); + const master_symlink = try std.fs.path.join(allocator, &[_][]const u8{ config.install_dir.?, "master" }); defer allocator.free(master_symlink); if (builtin.os.tag == .windows) { var file = try std.fs.createFileAbsolute(master_symlink, .{}); @@ -481,8 +529,6 @@ fn fetchCompiler( } } -const default_index_url = "https://ziglang.org/download/index.json"; - const DownloadIndex = struct { text: []u8, json: std.json.Parsed(std.json.Value), @@ -587,11 +633,8 @@ fn existsAbsolute(absolutePath: []const u8) !bool { return true; } -fn listCompilers(allocator: Allocator) !void { - const install_dir_string = try getInstallDir(allocator, .{ .create = false }); - defer allocator.free(install_dir_string); - - var install_dir = std.fs.openDirAbsolute(install_dir_string, .{ .iterate = true }) catch |e| switch (e) { +fn listCompilers() !void { + var install_dir = std.fs.openDirAbsolute(config.install_dir.?, .{ .iterate = true }) catch |e| switch (e) { error.FileNotFound => return, else => return e, }; @@ -610,11 +653,10 @@ fn listCompilers(allocator: Allocator) !void { } } -fn keepCompiler(allocator: Allocator, compiler_version: []const u8) !void { - const install_dir_string = try getInstallDir(allocator, .{ .create = true }); - defer allocator.free(install_dir_string); +fn keepCompiler(compiler_version: []const u8) !void { + try ensureDirExists(config.install_dir.?); - var install_dir = try std.fs.openDirAbsolute(install_dir_string, .{ .iterate = true }); + var install_dir = try std.fs.openDirAbsolute(config.install_dir.?, .{ .iterate = true }); defer install_dir.close(); var compiler_dir = install_dir.openDir(compiler_version, .{}) catch |e| switch (e) { @@ -626,17 +668,16 @@ fn keepCompiler(allocator: Allocator, compiler_version: []const u8) !void { }; var keep_fd = try compiler_dir.createFile("keep", .{}); keep_fd.close(); - loginfo("created '{s}{c}{s}{c}{s}'", .{ install_dir_string, std.fs.path.sep, compiler_version, std.fs.path.sep, "keep" }); + loginfo("created '{s}{c}{s}{c}{s}'", .{ config.install_dir.?, std.fs.path.sep, compiler_version, std.fs.path.sep, "keep" }); } fn cleanCompilers(allocator: Allocator, compiler_name_opt: ?[]const u8) !void { - const install_dir_string = try getInstallDir(allocator, .{ .create = true }); - defer allocator.free(install_dir_string); + try ensureDirExists(config.install_dir.?); // getting the current compiler const default_comp_opt = try getDefaultCompiler(allocator); defer if (default_comp_opt) |default_compiler| allocator.free(default_compiler); - var install_dir = std.fs.openDirAbsolute(install_dir_string, .{ .iterate = true }) catch |e| switch (e) { + var install_dir = std.fs.openDirAbsolute(config.install_dir.?, .{ .iterate = true }) catch |e| switch (e) { error.FileNotFound => return, else => return e, }; @@ -648,7 +689,7 @@ fn cleanCompilers(allocator: Allocator, compiler_name_opt: ?[]const u8) !void { std.log.err("cannot clean '{s}' ({s})", .{ compiler_name, reason }); return error.AlreadyReported; } - loginfo("deleting '{s}{c}{s}'", .{ install_dir_string, std.fs.path.sep, compiler_name }); + loginfo("deleting '{s}{c}{s}'", .{ config.install_dir.?, std.fs.path.sep, compiler_name }); try fixdeletetree.deleteTree(install_dir, compiler_name); } else { var it = install_dir.iterate(); @@ -671,17 +712,14 @@ fn cleanCompilers(allocator: Allocator, compiler_name_opt: ?[]const u8) !void { else => return e, } } - loginfo("deleting '{s}{c}{s}'", .{ install_dir_string, std.fs.path.sep, entry.name }); + loginfo("deleting '{s}{c}{s}'", .{ config.install_dir.?, std.fs.path.sep, entry.name }); try fixdeletetree.deleteTree(install_dir, entry.name); } } } fn readDefaultCompiler(allocator: Allocator, buffer: *[std.fs.max_path_bytes + 1]u8) !?[]const u8 { - const path_link = try makeZigPathLinkString(allocator); - defer allocator.free(path_link); - if (builtin.os.tag == .windows) { - var file = std.fs.openFileAbsolute(path_link, .{}) catch |e| switch (e) { + var file = std.fs.openFileAbsolute(config.path_link.?, .{}) catch |e| switch (e) { error.FileNotFound => return null, else => return e, }; @@ -689,14 +727,14 @@ fn readDefaultCompiler(allocator: Allocator, buffer: *[std.fs.max_path_bytes + 1 try file.seekTo(win32exelink.exe_offset); const len = try file.readAll(buffer); if (len != buffer.len) { - std.log.err("path link file '{s}' is too small", .{path_link}); + std.log.err("path link file '{s}' is too small", .{config.path_link.?}); return error.AlreadyReported; } const target_exe = std.mem.sliceTo(buffer, 0); return try allocator.dupe(u8, targetPathToVersion(target_exe)); } - const target_path = std.fs.readLinkAbsolute(path_link, buffer[0..std.fs.max_path_bytes]) catch |e| switch (e) { + const target_path = std.fs.readLinkAbsolute(config.path_link.?, buffer[0..std.fs.max_path_bytes]) catch |e| switch (e) { error.FileNotFound => return null, else => return e, }; @@ -766,21 +804,18 @@ fn setDefaultCompiler(allocator: Allocator, compiler_dir: []const u8, exist_veri }, } - const path_link = try makeZigPathLinkString(allocator); - defer allocator.free(path_link); - const link_target = try std.fs.path.join( allocator, &[_][]const u8{ compiler_dir, "files", comptime "zig" ++ builtin.target.exeFileExt() }, ); defer allocator.free(link_target); if (builtin.os.tag == .windows) { - try createExeLink(link_target, path_link); + try createExeLink(link_target, config.path_link.?); } else { - _ = try loggyUpdateSymlink(link_target, path_link, .{}); + _ = try loggyUpdateSymlink(link_target, config.path_link.?, .{}); } - try verifyPathLink(allocator, path_link); + try verifyPathLink(allocator, config.path_link.?); } /// Verify that path_link will work. It verifies that `path_link` is From 011e814803da45764a216a05360b4bc4c1429400 Mon Sep 17 00:00:00 2001 From: vent Date: Sat, 8 Feb 2025 14:35:18 +0000 Subject: [PATCH 2/8] Improve error messages from ZON parsing --- zigup.zig | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/zigup.zig b/zigup.zig index e3252ce..24726d0 100644 --- a/zigup.zig +++ b/zigup.zig @@ -383,9 +383,10 @@ const Config = struct { const src = try fd.readToEndAllocOptions(allocator, 2048, null, 1, 0); defer allocator.free(src); var status = std.mem.zeroes(std.zon.parse.Status); - const zon = std.zon.parse.fromSlice(Config, allocator, src, &status, .{}) catch { - std.debug.print("{any}\n", .{status}); - return error.fail_parse; + const zon = std.zon.parse.fromSlice(Config, allocator, src, &status, .{}) catch |e| { + if (e == error.ParseZon) + std.debug.print("Error in configuration file '{s}':\n{}\n", .{ zon_path, status }); + return e; }; defer status.deinit(allocator); From 7decc0c96b63c3a1d6674956bb980f17ef295359 Mon Sep 17 00:00:00 2001 From: vent Date: Sat, 8 Feb 2025 15:54:44 +0000 Subject: [PATCH 3/8] Fix tilde paths Tilde paths (e.g. '~/.local/bin') were not expanding properly. --- zigup.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/zigup.zig b/zigup.zig index 24726d0..e965232 100644 --- a/zigup.zig +++ b/zigup.zig @@ -145,6 +145,10 @@ fn getHomeDir() ![]const u8 { // TODO: this should be in standard lib fn toAbsolute(allocator: Allocator, path: []const u8) ![]u8 { std.debug.assert(!std.fs.path.isAbsolute(path)); + + if (path[0] == '~' and builtin.os.tag != .windows) + return std.fs.path.join(allocator, &[_][]const u8{ try getHomeDir(), path[1..] }); + const cwd = try std.process.getCwdAlloc(allocator); defer allocator.free(cwd); return std.fs.path.join(allocator, &[_][]const u8{ cwd, path }); From 63cfcff20701a969123f79ffe2239dd45462254c Mon Sep 17 00:00:00 2001 From: vent Date: Tue, 18 Mar 2025 14:52:52 +0000 Subject: [PATCH 4/8] Ensure all config fields are initialized --- zigup.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/zigup.zig b/zigup.zig index e965232..e486295 100644 --- a/zigup.zig +++ b/zigup.zig @@ -398,6 +398,10 @@ const Config = struct { } fn ensureValid(conf: *Config, allocator: Allocator) !void { + // All fields must be set by this time. + inline for (comptime std.meta.fieldNames(Config)) |field_name| + if (@field(config, field_name) == null) unreachable; + // Ensure we use absolute paths if (!std.fs.path.isAbsolute(conf.install_dir.?)) conf.install_dir = try toAbsolute(allocator, conf.install_dir.?); From c1bbc9b46bd50fcee1e70053cc879585e5287816 Mon Sep 17 00:00:00 2001 From: vent Date: Sat, 8 Feb 2025 16:07:53 +0000 Subject: [PATCH 5/8] Protect against empty strings in configuration --- zigup.zig | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/zigup.zig b/zigup.zig index e486295..50c9e2a 100644 --- a/zigup.zig +++ b/zigup.zig @@ -260,7 +260,17 @@ pub fn main2() !u8 { config_zon.index orelse try allocator.dupe(u8, Config.default_index_url), }; - try config.ensureValid(allocator); + config.ensureValid(allocator) catch |e| { + std.debug.print("Configuration error ({})", .{e}); + switch (e) { + error.EmptyInstallDir => std.debug.print(": install_dir cannot be an empty string", .{}), + error.EmptyPathLink => std.debug.print(": path_link cannot be an empty string", .{}), + error.EmptyIndex => std.debug.print(": index cannot be an empty string", .{}), + else => return e, + } + std.debug.print("\n", .{}); + return e; + }; if (args.len == 0) { help(); @@ -402,6 +412,10 @@ const Config = struct { inline for (comptime std.meta.fieldNames(Config)) |field_name| if (@field(config, field_name) == null) unreachable; + if (config.install_dir.?.len == 0) return error.EmptyInstallDir; + if (config.path_link.?.len == 0) return error.EmptyPathLink; + if (config.index.?.len == 0) return error.EmptyIndex; + // Ensure we use absolute paths if (!std.fs.path.isAbsolute(conf.install_dir.?)) conf.install_dir = try toAbsolute(allocator, conf.install_dir.?); From 0cac6816dd861e8692c9464f7076f728e3d421dd Mon Sep 17 00:00:00 2001 From: vent Date: Sat, 8 Feb 2025 16:33:40 +0000 Subject: [PATCH 6/8] Strip trailing path separators This fixes a couple visual bugs in printing, where some paths will have a double path separator if one was left trailing in configuration. --- zigup.zig | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/zigup.zig b/zigup.zig index 50c9e2a..9113261 100644 --- a/zigup.zig +++ b/zigup.zig @@ -421,6 +421,16 @@ const Config = struct { conf.install_dir = try toAbsolute(allocator, conf.install_dir.?); if (!std.fs.path.isAbsolute(conf.path_link.?)) conf.path_link = try toAbsolute(allocator, conf.path_link.?); + + // Strip trailing path separators + inplaceStripTrailingPathSep(&conf.install_dir.?); + inplaceStripTrailingPathSep(&conf.path_link.?); + } + + fn inplaceStripTrailingPathSep(path: *[]const u8) void { + const len = path.*.len; + if (path.*[len - 1] == std.fs.path.sep) + path.* = path.*[0 .. len - 1]; } fn allocDefaultInstallDir(allocator: Allocator) ![]const u8 { From 58126cfd59d072a8da6a882a1e32b85b6b70f1ae Mon Sep 17 00:00:00 2001 From: vent Date: Fri, 14 Mar 2025 11:37:54 +0000 Subject: [PATCH 7/8] Use APPDATA directory for zon file on Windows --- zigup.zig | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/zigup.zig b/zigup.zig index 9113261..ada426d 100644 --- a/zigup.zig +++ b/zigup.zig @@ -466,9 +466,17 @@ const Config = struct { } fn defaultZonPath(allocator: Allocator) ![]const u8 { + const zon_fname = "zigup.zon"; return switch (builtin.os.tag) { - .linux => std.fs.path.join(allocator, &[_][]const u8{ try getHomeDir(), ".config/zigup.zon" }), - else => @compileError("Not supported yet"), // TODO: Complete list + .windows => std.fs.path.join(allocator, &[_][]const u8{ + try std.fs.getAppDataDir(allocator, "zigup"), + zon_fname, + }), + else => std.fs.path.join(allocator, &[_][]const u8{ + try getHomeDir(), + ".config", + zon_fname, + }), }; } }; From 0e571ab0995b225765392e8f7c3a9e8b26cdd94f Mon Sep 17 00:00:00 2001 From: vent Date: Sat, 8 Feb 2025 15:40:37 +0000 Subject: [PATCH 8/8] Add configuration info to README.md --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 383cc21..a321416 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,24 @@ zigup stores each compiler in a global "install directory" in a versioned subdir zigup makes the zig program available by creating an entry in a directory that occurs in the `PATH` environment variable. On posix systems this entry is a symlink to one of the `zig` executables in the install directory. On windows this is an executable that forwards invocations to one of the `zig` executables in the install directory. -Both the "install directory" and "path link" are configurable through command-line options `--install-dir` and `--path-link` respectively. On posix systems the default "install directory" follows the [XDG basedir spec](https://specifications.freedesktop.org/basedir-spec/latest/#variables), ie. `$XDG_DATA_HOME/zigup` or `$HOME/.local/share/zigup` if `XDG_DATA_HOME` environment variable is empty or undefined. +Both the "install directory" and "path link" are configurable through command-line options `--install-dir` and `--path-link` respectively, as well as in your configuration file. On posix systems the default "install directory" follows the [XDG basedir spec](https://specifications.freedesktop.org/basedir-spec/latest/#variables), ie. `$XDG_DATA_HOME/zigup` or `$HOME/.local/share/zigup` if `XDG_DATA_HOME` environment variable is empty or undefined. + +# Configuration + +zigup can be configured via file or command-line, command-line takes precedence. + +The configuration file is in ZON (Zig Object Notation) format. The default path for this configuration file is `~/.config/zigup.zon` for posix, and `C:\Users\\AppData\Local\zigup\zigup.zon` for Windows. + +Any fields not set in your configuration file will use default values. + +An example configuration is as follows: +```zig +.{ + .install_dir = "/opt/zigup/", + .path_link = "~/.local/bin/zig", +} +``` + # Building Run `zig build` to build, `zig build test` to test and install with: