Skip to content

Commit b3428e8

Browse files
committed
add persistent install-dir setting
1 parent a9b48e4 commit b3428e8

File tree

4 files changed

+235
-18
lines changed

4 files changed

+235
-18
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,10 @@ zigup run <version> <args>...
4343

4444
# How the compilers are managed
4545

46-
zigup stores each compiler in a global "install directory" in a versioned subdirectory. On posix systems the "install directory" is `$HOME/.local/share/zigup` (or `$XDG_DATA_HOME/zigup`; see below) and on windows the install directory will be a directory named "zig" in the same directory as the "zigup.exe".
46+
zigup stores each compiler in a global "install directory" in a versioned subdirectory. Run `zigup get-install-dir` to see what this PATH is on your system. You can change this default with `zigup set-install-dir PATH`.
4747

4848
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.
4949

50-
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.
5150
# Building
5251

5352
Run `zig build` to build, `zig build test` to test and install with:

build.zig

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,47 @@ fn addTests(
261261
},
262262
});
263263

264+
tests.addWithClean(.{
265+
.name = "test-get-install-dir",
266+
.argv = &.{"get-install-dir"},
267+
});
268+
tests.addWithClean(.{
269+
.name = "test-get-install-dir2",
270+
.argv = &.{ "--install-dir", "/a/fake/install/dir", "get-install-dir" },
271+
.checks = &.{
272+
.{ .expect_stdout_exact = "/a/fake/install/dir\n" },
273+
},
274+
});
275+
tests.addWithClean(.{
276+
.name = "test-set-install-dir-relative",
277+
.argv = &.{ "set-install-dir", "foo/bar" },
278+
.checks = &.{
279+
.{ .expect_stderr_match = "error: set-install-dir requires an absolute path" },
280+
},
281+
});
282+
283+
{
284+
// just has to be an absolute path that exists
285+
const install_dir = b.build_root.path.?;
286+
const with_install_dir = tests.add(.{
287+
.name = "test-set-install-dir",
288+
.argv = &.{ "set-install-dir", install_dir },
289+
});
290+
tests.addWithClean(.{
291+
.name = "test-get-install-dir3",
292+
.argv = &.{"get-install-dir"},
293+
.env = .{ .dir = with_install_dir },
294+
.checks = &.{
295+
.{ .expect_stdout_exact = b.fmt("{s}\n", .{install_dir}) },
296+
},
297+
});
298+
tests.addWithClean(.{
299+
.name = "test-revert-install-dir",
300+
.argv = &.{"set-install-dir"},
301+
.env = .{ .dir = with_install_dir },
302+
});
303+
}
304+
264305
tests.addWithClean(.{
265306
.name = "test-no-default",
266307
.argv = &.{"default"},

runtest.zig

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,20 @@ pub fn main() !void {
4545
// try file.writer().print("this file marks this directory as the output for test: {s}\n", .{test_name});
4646
// }
4747

48+
const appdata = try std.fs.path.join(arena, &.{ out_env_dir, "appdata" });
4849
const path_link = try std.fs.path.join(arena, &.{ out_env_dir, "zig" ++ exe_ext });
4950
const install_dir = try std.fs.path.join(arena, &.{ out_env_dir, "install" });
51+
const install_dir_parsed = parseInstallDir(install_dir);
52+
53+
const install_dir_setting_path = try std.fs.path.join(arena, &.{ appdata, "install-dir" });
54+
defer arena.free(install_dir_setting_path);
5055

5156
if (std.mem.eql(u8, in_env_dir, "--no-input-environment")) {
5257
try std.fs.cwd().makeDir(install_dir);
58+
try std.fs.cwd().makeDir(appdata);
59+
var file = try std.fs.cwd().createFile(install_dir_setting_path, .{});
60+
defer file.close();
61+
try file.writer().writeAll(install_dir);
5362
} else {
5463
var shared_sibling_state: SharedSiblingState = .{};
5564
try copyEnvDir(
@@ -61,6 +70,25 @@ pub fn main() !void {
6170
.{ .with_compilers = with_compilers },
6271
&shared_sibling_state,
6372
);
73+
74+
// we need to patch the old install dir
75+
const input_install_dir = blk: {
76+
var file = try std.fs.cwd().openFile(install_dir_setting_path, .{});
77+
defer file.close();
78+
break :blk try file.readToEndAlloc(arena, std.math.maxInt(usize));
79+
};
80+
defer arena.free(input_install_dir);
81+
82+
if (std.mem.startsWith(u8, install_dir, input_install_dir)) {
83+
// the install dir has been customized, we'll keep it
84+
} else {
85+
// otherwise, just write our new default install directory
86+
const input_install_dir_parsed = parseInstallDir(input_install_dir);
87+
std.debug.assert(std.mem.eql(u8, install_dir_parsed.cache_o, input_install_dir_parsed.cache_o));
88+
var file = try std.fs.cwd().createFile(install_dir_setting_path, .{});
89+
defer file.close();
90+
try file.writer().writeAll(install_dir);
91+
}
6492
}
6593

6694
var maybe_second_bin_dir: ?[]const u8 = null;
@@ -92,12 +120,20 @@ pub fn main() !void {
92120

93121
var argv = std.ArrayList([]const u8).init(arena);
94122
try argv.append(zigup_exe);
123+
try argv.append("--appdata");
124+
try argv.append(appdata);
95125
try argv.append("--path-link");
96126
try argv.append(path_link);
97-
try argv.append("--install-dir");
98-
try argv.append(install_dir);
99127
try argv.appendSlice(zigup_args);
100128

129+
if (true) {
130+
try std.io.getStdErr().writer().writeAll("runtest exec: ");
131+
for (argv.items) |arg| {
132+
try std.io.getStdErr().writer().print(" {s}", .{arg});
133+
}
134+
try std.io.getStdErr().writer().writeAll("\n");
135+
}
136+
101137
var child = std.process.Child.init(argv.items, arena);
102138

103139
if (add_path) {
@@ -156,6 +192,31 @@ pub fn main() !void {
156192
}
157193
}
158194

195+
const ParsedInstallDir = struct {
196+
test_name: []const u8,
197+
hash: []const u8,
198+
cache_o: []const u8,
199+
};
200+
fn badInstallDir(install_dir: []const u8, reason: []const u8) noreturn {
201+
std.debug.panic("invalid install directory '{s}': {s}", .{ install_dir, reason });
202+
}
203+
fn parseInstallDir(install_dir: []const u8) ParsedInstallDir {
204+
{
205+
const name = std.fs.path.basename(install_dir);
206+
if (!std.mem.eql(u8, name, "install"))
207+
badInstallDir(install_dir, "did not end with 'install'");
208+
}
209+
const test_dir = std.fs.path.dirname(install_dir) orelse badInstallDir(install_dir, "missing test dir");
210+
const test_name = std.fs.path.basename(test_dir);
211+
const cache_dir = std.fs.path.dirname(test_dir) orelse badInstallDir(install_dir, "missing cache/hash dir");
212+
const hash = std.fs.path.basename(cache_dir);
213+
return .{
214+
.test_name = test_name,
215+
.hash = hash,
216+
.cache_o = std.fs.path.dirname(cache_dir) orelse badInstallDir(install_dir, "missing cache o dir"),
217+
};
218+
}
219+
159220
fn containsCompiler(compilers: []const u8, compiler: []const u8) bool {
160221
var it = std.mem.splitScalar(u8, compilers, ',');
161222
while (it.next()) |c| {

zigup.zig

Lines changed: 130 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const url_platform = os ++ "-" ++ arch;
2626
const json_platform = arch ++ "-" ++ os;
2727
const archive_ext = if (builtin.os.tag == .windows) "zip" else "tar.xz";
2828

29+
var global_override_appdata: ?[]const u8 = null; // only used for testing
2930
var global_optional_install_dir: ?[]const u8 = null;
3031
var global_optional_path_link: ?[]const u8 = null;
3132

@@ -131,7 +132,7 @@ fn ignoreHttpCallback(request: []const u8) void {
131132
_ = request;
132133
}
133134

134-
fn allocInstallDirStringXdg(allocator: Allocator) ![]const u8 {
135+
fn allocInstallDirStringXdg(allocator: Allocator) error{AlreadyReported}![]const u8 {
135136
// see https://specifications.freedesktop.org/basedir-spec/latest/#variables
136137
// try $XDG_DATA_HOME/zigup first
137138
xdg_var: {
@@ -141,7 +142,7 @@ fn allocInstallDirStringXdg(allocator: Allocator) ![]const u8 {
141142
std.log.err("$XDG_DATA_HOME environment variable '{s}' is not an absolute path", .{xdg_data_home});
142143
return error.AlreadyReported;
143144
}
144-
return std.fs.path.join(allocator, &[_][]const u8{ xdg_data_home, "zigup" });
145+
return std.fs.path.join(allocator, &[_][]const u8{ xdg_data_home, "zigup" }) catch |e| oom(e);
145146
}
146147
// .. then fallback to $HOME/.local/share/zigup
147148
const home = std.posix.getenv("HOME") orelse {
@@ -152,21 +153,89 @@ fn allocInstallDirStringXdg(allocator: Allocator) ![]const u8 {
152153
std.log.err("$HOME environment variable '{s}' is not an absolute path", .{home});
153154
return error.AlreadyReported;
154155
}
155-
return std.fs.path.join(allocator, &[_][]const u8{ home, ".local", "share", "zigup" });
156+
return std.fs.path.join(allocator, &[_][]const u8{ home, ".local", "share", "zigup" }) catch |e| oom(e);
156157
}
157158

158-
fn allocInstallDirString(allocator: Allocator) ![]const u8 {
159-
// TODO: maybe support ZIG_INSTALL_DIR environment variable?
160-
// TODO: maybe support a file on the filesystem to configure install dir?
159+
fn getSettingsDir(allocator: Allocator) ?[]const u8 {
160+
const appdata: ?[]const u8 = std.fs.getAppDataDir(allocator, "zigup") catch |err| switch (err) {
161+
error.OutOfMemory => |e| oom(e),
162+
error.AppDataDirUnavailable => null,
163+
};
164+
// just used for testing, but note we still test getting the builtin appdata dir either way
165+
if (global_override_appdata) |appdata_override| {
166+
if (appdata) |a| allocator.free(a);
167+
return allocator.dupe(u8, appdata_override) catch |e| oom(e);
168+
}
169+
return appdata;
170+
}
171+
172+
fn readInstallDir(allocator: Allocator) !?[]const u8 {
173+
const settings_dir_path = getSettingsDir(allocator) orelse return null;
174+
defer allocator.free(settings_dir_path);
175+
const setting_path = std.fs.path.join(allocator, &.{ settings_dir_path, "install-dir" }) catch |e| oom(e);
176+
defer allocator.free(setting_path);
177+
var file = std.fs.cwd().openFile(setting_path, .{}) catch |err| switch (err) {
178+
error.FileNotFound => return null,
179+
else => |e| {
180+
std.log.err("open '{s}' failed with {s}", .{ setting_path, @errorName(e) });
181+
return error.AlreadyReported;
182+
},
183+
};
184+
defer file.close();
185+
return file.readToEndAlloc(allocator, 9999) catch |err| {
186+
std.log.err("read install dir from '{s}' failed with {s}", .{ setting_path, @errorName(err) });
187+
return error.AlreadyReported;
188+
};
189+
}
190+
191+
fn saveInstallDir(allocator: Allocator, maybe_dir: ?[]const u8) !void {
192+
const settings_dir_path = getSettingsDir(allocator) orelse {
193+
std.log.err("cannot save install dir, unable to find a suitable settings directory", .{});
194+
return error.AlreadyReported;
195+
};
196+
defer allocator.free(settings_dir_path);
197+
const setting_path = std.fs.path.join(allocator, &.{ settings_dir_path, "install-dir" }) catch |e| oom(e);
198+
defer allocator.free(setting_path);
199+
if (maybe_dir) |d| {
200+
{
201+
const file = try std.fs.cwd().createFile(setting_path, .{});
202+
defer file.close();
203+
try file.writer().writeAll(d);
204+
}
205+
206+
// sanity check, read it back
207+
const readback = (try readInstallDir(allocator)) orelse {
208+
std.log.err("unable to readback install-dir after saving it", .{});
209+
return error.AlreadyReported;
210+
};
211+
defer allocator.free(readback);
212+
if (!std.mem.eql(u8, readback, d)) {
213+
std.log.err("saved install dir readback mismatch\nwrote: '{s}'\nread : '{s}'\n", .{ d, readback });
214+
return error.AlreadyReported;
215+
}
216+
} else {
217+
std.fs.cwd().deleteFile(setting_path) catch |err| switch (err) {
218+
error.FileNotFound => {},
219+
else => |e| return e,
220+
};
221+
}
222+
}
223+
224+
fn allocInstallDirString(allocator: Allocator) error{AlreadyReported}![]const u8 {
225+
if (try readInstallDir(allocator)) |d| return d;
161226
if (builtin.os.tag == .windows) {
162-
const self_exe_dir = try std.fs.selfExeDirPathAlloc(allocator);
227+
const self_exe_dir = std.fs.selfExeDirPathAlloc(allocator) catch |e| {
228+
std.log.err("failed to get exe dir path with {s}", .{@errorName(e)});
229+
return error.AlreadyReported;
230+
};
163231
defer allocator.free(self_exe_dir);
164-
return std.fs.path.join(allocator, &.{ self_exe_dir, "zig" });
232+
return std.fs.path.join(allocator, &.{ self_exe_dir, "zig" }) catch |e| oom(e);
165233
}
166234
return allocInstallDirStringXdg(allocator);
167235
}
168236
const GetInstallDirOptions = struct {
169237
create: bool,
238+
log: bool = true,
170239
};
171240
fn getInstallDir(allocator: Allocator, options: GetInstallDirOptions) ![]const u8 {
172241
var optional_dir_to_free_on_error: ?[]const u8 = null;
@@ -178,7 +247,9 @@ fn getInstallDir(allocator: Allocator, options: GetInstallDirOptions) ![]const u
178247
break :init optional_dir_to_free_on_error.?;
179248
};
180249
std.debug.assert(std.fs.path.isAbsolute(install_dir));
181-
loginfo("install directory '{s}'", .{install_dir});
250+
if (options.log) {
251+
loginfo("install directory '{s}'", .{install_dir});
252+
}
182253
if (options.create) {
183254
loggyMakePath(install_dir) catch |e| switch (e) {
184255
error.PathAlreadyExists => {},
@@ -205,8 +276,12 @@ fn toAbsolute(allocator: Allocator, path: []const u8) ![]u8 {
205276
return std.fs.path.join(allocator, &[_][]const u8{ cwd, path });
206277
}
207278

208-
fn help() void {
209-
std.io.getStdErr().writeAll(
279+
fn help(allocator: Allocator) !void {
280+
const default_install_dir = allocInstallDirString(allocator) catch |err| switch (err) {
281+
error.AlreadyReported => "unknown (see error printed above)",
282+
};
283+
284+
try std.io.getStdErr().writer().print(
210285
\\Download and manage zig compilers.
211286
\\
212287
\\Common Usage:
@@ -220,12 +295,16 @@ fn help() void {
220295
\\ zigup keep VERSION mark a compiler to be kept during clean
221296
\\ zigup run VERSION ARGS... run the given VERSION of the compiler with the given ARGS...
222297
\\
298+
\\ zigup get-install-dir prints the install directory to stdout
299+
\\ zigup set-install-dir [PATH] set the default install directory, no PATH reverts to the builtin default
300+
\\
223301
\\Uncommon Usage:
224302
\\
225303
\\ zigup fetch-index download and print the download index json
226304
\\
227305
\\Common Options:
228306
\\ --install-dir DIR override the default install location
307+
\\ default: {s}
229308
\\ --path-link PATH path to the `zig` symlink that points to the default compiler
230309
\\ this will typically be a file path within a PATH directory so
231310
\\ that the user can just run `zig`
@@ -234,7 +313,9 @@ fn help() void {
234313
++ " " ++ default_index_url ++
235314
\\
236315
\\
237-
) catch unreachable;
316+
,
317+
.{default_install_dir},
318+
);
238319
}
239320

240321
fn getCmdOpt(args: [][:0]u8, i: *usize) ![]const u8 {
@@ -287,8 +368,11 @@ pub fn main2() !u8 {
287368
} else if (std.mem.eql(u8, "--index", arg)) {
288369
index_url = try getCmdOpt(args, &i);
289370
} else if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) {
290-
help();
371+
try help(allocator);
291372
return 0;
373+
} else if (std.mem.eql(u8, "--appdata", arg)) {
374+
// NOTE: this is a private option just used for testing
375+
global_override_appdata = try getCmdOpt(args, &i);
292376
} else {
293377
if (newlen == 0 and std.mem.eql(u8, "run", arg)) {
294378
return try runCompiler(allocator, args[i + 1 ..]);
@@ -300,9 +384,41 @@ pub fn main2() !u8 {
300384
args = args[0..newlen];
301385
}
302386
if (args.len == 0) {
303-
help();
387+
try help(allocator);
304388
return 1;
305389
}
390+
if (std.mem.eql(u8, "get-install-dir", args[0])) {
391+
if (args.len != 1) {
392+
std.log.err("get-install-dir does not accept any cmdline arguments", .{});
393+
return 1;
394+
}
395+
const install_dir = getInstallDir(allocator, .{ .create = false, .log = false }) catch |err| switch (err) {
396+
error.AlreadyReported => return 1,
397+
else => |e| return e,
398+
};
399+
try std.io.getStdOut().writer().writeAll(install_dir);
400+
try std.io.getStdOut().writer().writeAll("\n");
401+
return 0;
402+
}
403+
if (std.mem.eql(u8, "set-install-dir", args[0])) {
404+
const set_args = args[1..];
405+
switch (set_args.len) {
406+
0 => try saveInstallDir(allocator, null),
407+
1 => {
408+
const path = set_args[0];
409+
if (!std.fs.path.isAbsolute(path)) {
410+
std.log.err("set-install-dir requires an absolute path", .{});
411+
return 1;
412+
}
413+
try saveInstallDir(allocator, path);
414+
},
415+
else => |set_arg_count| {
416+
std.log.err("set-install-dir requires 0 or 1 cmdline arg but got {}", .{set_arg_count});
417+
return 1;
418+
},
419+
}
420+
return 0;
421+
}
306422
if (std.mem.eql(u8, "fetch-index", args[0])) {
307423
if (args.len != 1) {
308424
std.log.err("'index' command requires 0 arguments but got {d}", .{args.len - 1});

0 commit comments

Comments
 (0)