From b7eea9ab9ac54f2282ceea79da2a42ad4659cb7b Mon Sep 17 00:00:00 2001 From: Nick Hawke Date: Tue, 19 May 2026 21:51:43 -0400 Subject: [PATCH 1/4] Support setting ad-hoc version The ad hoc version setting provides a mechanism for running the zig binary outside a project dir without needing to explicitly specify a version. i.e. "zig fmt" instead of "zig 0.15.2 fmt". When inside a project dir, anyzig will still prefer the version specified in the build.zig[.zon] file. Also adds `zig any set-ad-hoc-version` command for setting the version. This uses a similar mechanism as the existing 'verbosity' setting for simplicity. Coming up with a more robust configuration system is outside the scope of this change. --- src/main.zig | 104 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 90 insertions(+), 14 deletions(-) diff --git a/src/main.zig b/src/main.zig index 6a80a93..cdb56bd 100644 --- a/src/main.zig +++ b/src/main.zig @@ -104,6 +104,35 @@ fn readVerbosityFile() union(enum) { ); } +fn readAdHocVersionFile() ?VersionSpecifier { + const app_data_dir = global.getAppDataDir() catch return null; + const ad_hoc_version_path = std.fs.path.join(global.arena, &.{ app_data_dir, "ad-hoc-version" }) catch |e| oom(e); + defer global.arena.free(ad_hoc_version_path); + const content: []u8 = read_file: { + const file = std.fs.cwd().openFile(ad_hoc_version_path, .{}) catch |err| switch (err) { + error.FileNotFound => return null, + else => |e| std.debug.panic("open '{s}' failed with {s}", .{ ad_hoc_version_path, @errorName(e) }), + }; + defer file.close(); + break :read_file file.readToEndAlloc(global.arena, std.math.maxInt(usize)) catch |err| std.debug.panic( + "read '{s}' failed with {s}", + .{ ad_hoc_version_path, @errorName(err) }, + ); + }; + defer global.arena.free(content); + const content_trimmed = std.mem.trimRight(u8, content, &std.ascii.whitespace); + if (VersionSpecifier.parse(content_trimmed)) |parsed_version| { + return parsed_version; + } + + std.debug.panic( + "file '{s}' had the following unexpected content:\n" ++ + "---\n{s}\n---\n" ++ + "we expect the content to be a valid semantic version (e.g. 'master', '0.14.0')", + .{ ad_hoc_version_path, content }, + ); +} + fn anyzigLog( comptime level: std.log.Level, comptime scope: @Type(.enum_literal), @@ -336,6 +365,9 @@ pub fn main() !void { break :blk options; }; + // Resolve configured ad-hoc version + const ad_hoc_version: ?VersionSpecifier = readAdHocVersionFile(); + const version_specifier: VersionSpecifier, const is_init = blk: { if (maybe_command) |command| { if (std.mem.startsWith(u8, command, "-") and !std.mem.eql(u8, command, "-h") and !std.mem.eql(u8, command, "--help")) { @@ -355,25 +387,39 @@ pub fn main() !void { } else break :blk_is_help false; }; + // manual version gets priority over ad hoc version if (manual_version) |version| break :blk .{ version, !is_help }; + if (ad_hoc_version) |version| break :blk .{ version, !is_help }; try std.io.getStdErr().writer().print( - "error: anyzig init requires a version, i.e. 'zig 0.13.0 {s}'\n", + "error: anyzig init requires a version, you can:\n" ++ + " 1. run 'zig 0.13.0 {s}'\n" ++ + " 2. set an ad-hoc version using 'zig any set-ad-hoc-version VERSION'\n", .{command}, ); std.process.exit(0xff); } if (std.mem.eql(u8, command, "any")) std.process.exit(try anyCommand(cmdline, cmdline_offset + 1)); } + + // 1. use manual version if specified if (manual_version) |version| break :blk .{ version, false }; - const build_root = try findBuildRoot(arena, build_root_options) orelse { - try std.io.getStdErr().writeAll( - "no build.zig to pull a zig version from, you can:\n" ++ - " 1. run '" ++ exe_str ++ " VERSION' to specify a version\n" ++ - " 2. run from a directory where a build.zig can be found\n", - ); - std.process.exit(0xff); - }; - break :blk .{ .{ .semantic = try determineSemanticVersion(arena, build_root) }, false }; + + // 2. use project version (note: we intentionally fail if in a + // project without a specified version) + if (try findBuildRoot(arena, build_root_options)) |build_root| { + break :blk .{ .{ .semantic = try determineSemanticVersion(arena, build_root) }, false }; + } + + // 3. fall back to ad hoc version when outside a project + if (ad_hoc_version) |version| break :blk .{ version, false }; + + try std.io.getStdErr().writeAll( + "no build.zig to pull a zig version from, you can:\n" ++ + " 1. run '" ++ exe_str ++ " VERSION' to specify a version\n" ++ + " 2. run from a directory where a build.zig can be found\n" ++ + " 3. set an ad-hoc version using 'zig any set-ad-hoc-version VERSION'\n", + ); + std.process.exit(0xff); }; const app_data_path = try std.fs.getAppDataDir(arena, "anyzig"); @@ -545,10 +591,12 @@ fn anyCommandUsage() !u8 { try std.io.getStdErr().writer().print( "any" ++ @tagName(build_options.exe) ++ " {s} from https://github.com/marler8997/anyzig\n" ++ "Here are the anyzig-specific subcommands:\n" ++ - " zig any set-verbosity LEVEL | sets the default system-wide verbosity\n" ++ - " | accepts 'warn' or 'debug'\n" ++ - " zig any version | print the version of anyzig to stdout\n" ++ - " zig any list-installed | list all versions of zig installed in the global cache\n", + " zig any set-verbosity LEVEL | sets the default system-wide verbosity\n" ++ + " | accepts 'warn' or 'debug'\n" ++ + " zig any set-ad-hoc-version VERSION | sets the version to use by default when outside a project directory\n" ++ + " | accepts a version specifier (e.g. 'master' or '0.15.2'\n" ++ + " zig any version | print the version of anyzig to stdout\n" ++ + " zig any list-installed | list all versions of zig installed in the global cache\n", .{@embedFile("version")}, ); return 0xff; @@ -594,6 +642,28 @@ fn anyCommand(cmdline: Cmdline, cmdline_offset: usize) !u8 { .loaded_from_file => |l| std.debug.assert(l == level), } return 0; + } else if (std.mem.eql(u8, command, "set-ad-hoc-version")) { + if (arg_offset >= cmdline.len()) errExit("missing VERSION", .{}); + if (arg_offset + 1 < cmdline.len()) errExit("too many cmdline args", .{}); + const version_str = cmdline.arg(arg_offset); + const version: VersionSpecifier = if (VersionSpecifier.parse(version_str)) |v| v else errExit("invalid VERSION '{s}'", .{version_str}); + { + const app_data_dir = try global.getAppDataDir(); + const version_path = std.fs.path.join(global.arena, &.{ app_data_dir, "ad-hoc-version" }) catch |e| oom(e); + defer global.arena.free(version_path); + if (std.fs.path.dirname(version_path)) |dir| { + try std.fs.cwd().makePath(dir); + } + const file = try std.fs.cwd().createFile(version_path, .{}); + defer file.close(); + try file.writer().print("{s}\n", .{version_str}); + } + if (readAdHocVersionFile()) |v| { + std.debug.assert(v.eql(version)); + } else { + @panic("no ad hoc version file after writing it?"); + } + return 0; } else if (std.mem.eql(u8, command, "list-installed")) { if (arg_offset < cmdline.len()) errExit("the 'list-installed' subcommand does not take any cmdline args", .{}); try listInstalled(); @@ -767,6 +837,12 @@ const VersionSpecifier = union(enum) { .zls => return null, }; } + pub fn eql(self: VersionSpecifier, other: VersionSpecifier) bool { + switch (self) { + .master => return other == .master, + .semantic => |s| return other == .semantic and s.eql(other.semantic), + } + } }; const arch = switch (builtin.cpu.arch) { From 6d6d763940ebf62c085ab5a633963be38bc9ecf0 Mon Sep 17 00:00:00 2001 From: Nick Hawke Date: Tue, 19 May 2026 23:19:20 -0400 Subject: [PATCH 2/4] Add tests for 'any set-ad-hoc-version' --- build.zig | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/build.zig b/build.zig index 5e6e56d..26021d8 100644 --- a/build.zig +++ b/build.zig @@ -199,7 +199,9 @@ fn addTests( const run = b.addRunArtifact(anyzig); run.setName("anyzig init (no version)"); run.addArg("init"); - run.expectStdErrEqual("error: anyzig init requires a version, i.e. 'zig 0.13.0 init'\n"); + run.expectStdErrEqual("error: anyzig init requires a version, you can:\n" ++ + " 1. run 'zig 0.13.0 init'\n" ++ + " 2. set an ad-hoc version using 'zig any set-ad-hoc-version VERSION'\n"); test_step.dependOn(&run.step); } @@ -244,6 +246,7 @@ fn addTests( ) }); t.run.addCheck(.{ .expect_stderr_match = "zig any version" }); t.run.addCheck(.{ .expect_stderr_match = "zig any set-verbosity" }); + t.run.addCheck(.{ .expect_stderr_match = "zig any set-ad-hoc-version" }); } { @@ -298,6 +301,48 @@ fn addTests( }); } + { + const t = test_factory.add(.{ + .name = "test-any-set-ad-hoc-version-none", + .input_dir = .no_input, + .options = .nosetup, + .args = &.{ "any", "set-ad-hoc-version" }, + }); + t.run.expectStdErrEqual("anyzig: error: missing VERSION\n"); + } + + { + const t = test_factory.add(.{ + .name = "test-any-set-ad-hoc-version-too-many", + .input_dir = .no_input, + .options = .nosetup, + .args = &.{ "any", "set-ad-hoc-version", "warn", "debug" }, + }); + t.run.expectStdErrEqual("anyzig: error: too many cmdline args\n"); + } + + { + const t = test_factory.add(.{ + .name = "test-any-set-ad-hoc-version-bad", + .input_dir = .no_input, + .options = .nosetup, + .args = &.{ "any", "set-ad-hoc-version", "whattheheck" }, + }); + t.run.expectStdErrEqual("anyzig: error: invalid VERSION 'whattheheck'\n"); + } + + { + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // TODO: override the appdata directory to run this test + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + _ = test_factory.add(.{ + .name = "test-any-set-ad-hoc-version-warn", + .input_dir = .no_input, + .options = .nosetup, + .args = &.{ "any", "set-ad-hoc-version", "0.14.0" }, + }); + } + _ = test_factory.add(.{ .name = "test-master-version", .input_dir = .no_input, From 128af92f3f8d29cacad01d968cd41ac2a2e7729b Mon Sep 17 00:00:00 2001 From: Nick Hawke Date: Wed, 20 May 2026 00:54:31 -0400 Subject: [PATCH 3/4] Skip set-ad-hoc-version test due to side effects This test sets the version, which is not hermetic and therefore causes future test runs to be incorrect. The right way to test this would be to configure a temporary appdata directory which can be cleaned up after each test. For now, the best solution is to just comment the test out. --- build.zig | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/build.zig b/build.zig index 26021d8..b65ded6 100644 --- a/build.zig +++ b/build.zig @@ -316,7 +316,7 @@ fn addTests( .name = "test-any-set-ad-hoc-version-too-many", .input_dir = .no_input, .options = .nosetup, - .args = &.{ "any", "set-ad-hoc-version", "warn", "debug" }, + .args = &.{ "any", "set-ad-hoc-version", "0.14.0", "0.15.2" }, }); t.run.expectStdErrEqual("anyzig: error: too many cmdline args\n"); } @@ -335,12 +335,12 @@ fn addTests( // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // TODO: override the appdata directory to run this test // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - _ = test_factory.add(.{ - .name = "test-any-set-ad-hoc-version-warn", - .input_dir = .no_input, - .options = .nosetup, - .args = &.{ "any", "set-ad-hoc-version", "0.14.0" }, - }); + // _ = test_factory.add(.{ + // .name = "test-any-set-ad-hoc-version-warn", + // .input_dir = .no_input, + // .options = .nosetup, + // .args = &.{ "any", "set-ad-hoc-version", "0.14.0" }, + // }); } _ = test_factory.add(.{ From 1802af47435b764723e5be896a134b55e4230c78 Mon Sep 17 00:00:00 2001 From: Nick Hawke Date: Wed, 20 May 2026 17:36:26 -0400 Subject: [PATCH 4/4] Only read ad-hoc version file if needed --- src/main.zig | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main.zig b/src/main.zig index cdb56bd..4e7231a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -365,9 +365,6 @@ pub fn main() !void { break :blk options; }; - // Resolve configured ad-hoc version - const ad_hoc_version: ?VersionSpecifier = readAdHocVersionFile(); - const version_specifier: VersionSpecifier, const is_init = blk: { if (maybe_command) |command| { if (std.mem.startsWith(u8, command, "-") and !std.mem.eql(u8, command, "-h") and !std.mem.eql(u8, command, "--help")) { @@ -389,7 +386,7 @@ pub fn main() !void { // manual version gets priority over ad hoc version if (manual_version) |version| break :blk .{ version, !is_help }; - if (ad_hoc_version) |version| break :blk .{ version, !is_help }; + if (readAdHocVersionFile()) |version| break :blk .{ version, !is_help }; try std.io.getStdErr().writer().print( "error: anyzig init requires a version, you can:\n" ++ " 1. run 'zig 0.13.0 {s}'\n" ++ @@ -411,7 +408,7 @@ pub fn main() !void { } // 3. fall back to ad hoc version when outside a project - if (ad_hoc_version) |version| break :blk .{ version, false }; + if (readAdHocVersionFile()) |version| break :blk .{ version, false }; try std.io.getStdErr().writeAll( "no build.zig to pull a zig version from, you can:\n" ++