@@ -26,6 +26,7 @@ const url_platform = os ++ "-" ++ arch;
2626const json_platform = arch ++ "-" ++ os ;
2727const archive_ext = if (builtin .os .tag == .windows ) "zip" else "tar.xz" ;
2828
29+ var global_override_appdata : ? []const u8 = null ; // only used for testing
2930var global_optional_install_dir : ? []const u8 = null ;
3031var 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\n wrote: '{s}'\n read : '{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}
168236const GetInstallDirOptions = struct {
169237 create : bool ,
238+ log : bool = true ,
170239};
171240fn 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
240321fn 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