os: add prependEnv(), like appendEnv() (#2983)

We can use this function in setupXdgDataDirs() to simplify the
XDG_DATA_DIRS environment variable code in a more standardized way.
This commit is contained in:
Mitchell Hashimoto
2024-12-16 12:24:29 -08:00
committed by GitHub
3 changed files with 94 additions and 25 deletions

View File

@ -34,6 +34,23 @@ pub fn appendEnvAlways(
}); });
} }
/// Prepend a value to an environment variable such as PATH.
/// The returned value is always allocated so it must be freed.
pub fn prependEnv(
alloc: Allocator,
current: []const u8,
value: []const u8,
) ![]u8 {
// If there is no prior value, we return it as-is
if (current.len == 0) return try alloc.dupe(u8, value);
return try std.fmt.allocPrint(alloc, "{s}{c}{s}", .{
value,
std.fs.path.delimiter,
current,
});
}
/// The result of getenv, with a shared deinit to properly handle allocation /// The result of getenv, with a shared deinit to properly handle allocation
/// on Windows. /// on Windows.
pub const GetEnvResult = struct { pub const GetEnvResult = struct {
@ -110,3 +127,25 @@ test "appendEnv existing" {
try testing.expectEqualStrings(result, "a:b:foo"); try testing.expectEqualStrings(result, "a:b:foo");
} }
} }
test "prependEnv empty" {
const testing = std.testing;
const alloc = testing.allocator;
const result = try prependEnv(alloc, "", "foo");
defer alloc.free(result);
try testing.expectEqualStrings(result, "foo");
}
test "prependEnv existing" {
const testing = std.testing;
const alloc = testing.allocator;
const result = try prependEnv(alloc, "a:b", "foo");
defer alloc.free(result);
if (builtin.os.tag == .windows) {
try testing.expectEqualStrings(result, "foo;a:b");
} else {
try testing.expectEqualStrings(result, "foo:a:b");
}
}

View File

@ -27,6 +27,7 @@ pub const CFReleaseThread = @import("cf_release_thread.zig");
pub const TempDir = @import("TempDir.zig"); pub const TempDir = @import("TempDir.zig");
pub const appendEnv = env.appendEnv; pub const appendEnv = env.appendEnv;
pub const appendEnvAlways = env.appendEnvAlways; pub const appendEnvAlways = env.appendEnvAlways;
pub const prependEnv = env.prependEnv;
pub const getenv = env.getenv; pub const getenv = env.getenv;
pub const setenv = env.setenv; pub const setenv = env.setenv;
pub const unsetenv = env.unsetenv; pub const unsetenv = env.unsetenv;

View File

@ -5,6 +5,7 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const EnvMap = std.process.EnvMap; const EnvMap = std.process.EnvMap;
const config = @import("../config.zig"); const config = @import("../config.zig");
const homedir = @import("../os/homedir.zig"); const homedir = @import("../os/homedir.zig");
const internal_os = @import("../os/main.zig");
const log = std.log.scoped(.shell_integration); const log = std.log.scoped(.shell_integration);
@ -435,8 +436,8 @@ test "bash: preserve ENV" {
/// Setup automatic shell integration for shells that include /// Setup automatic shell integration for shells that include
/// their modules from paths in `XDG_DATA_DIRS` env variable. /// their modules from paths in `XDG_DATA_DIRS` env variable.
/// ///
/// Path of shell-integration dir is prepended to `XDG_DATA_DIRS`. /// The shell-integration path is prepended to `XDG_DATA_DIRS`.
/// It is also saved in `GHOSTTY_SHELL_INTEGRATION_XDG_DIR` variable /// It is also saved in the `GHOSTTY_SHELL_INTEGRATION_XDG_DIR` variable
/// so that the shell can refer to it and safely remove this directory /// so that the shell can refer to it and safely remove this directory
/// from `XDG_DATA_DIRS` when integration is complete. /// from `XDG_DATA_DIRS` when integration is complete.
fn setupXdgDataDirs( fn setupXdgDataDirs(
@ -458,32 +459,60 @@ fn setupXdgDataDirs(
// so that our modifications don't interfere with other commands. // so that our modifications don't interfere with other commands.
try env.put("GHOSTTY_SHELL_INTEGRATION_XDG_DIR", integ_dir); try env.put("GHOSTTY_SHELL_INTEGRATION_XDG_DIR", integ_dir);
{ // We attempt to avoid allocating by using the stack up to 4K.
const xdg_data_dir_key = "XDG_DATA_DIRS"; // Max stack size is considerably larger on mac
// 4K is a reasonable size for this for most cases. However, env
// vars can be significantly larger so if we have to we fall
// back to a heap allocated value.
var stack_alloc_state = std.heap.stackFallback(4096, alloc_arena);
const stack_alloc = stack_alloc_state.get();
// We attempt to avoid allocating by using the stack up to 4K. // If no XDG_DATA_DIRS set use the default value as specified.
// Max stack size is considerably larger on macOS and Linux but // This ensures that the default directories aren't lost by setting
// 4K is a reasonable size for this for most cases. However, env // our desired integration dir directly. See #2711.
// vars can be significantly larger so if we have to we fall // <https://specifications.freedesktop.org/basedir-spec/0.6/#variables>
// back to a heap allocated value. const xdg_data_dirs_key = "XDG_DATA_DIRS";
var stack_alloc_state = std.heap.stackFallback(4096, alloc_arena); try env.put(
const stack_alloc = stack_alloc_state.get(); xdg_data_dirs_key,
try internal_os.prependEnv(
// If no XDG_DATA_DIRS set use the default value as specified. stack_alloc,
// This ensures that the default directories aren't lost by setting env.get(xdg_data_dirs_key) orelse "/usr/local/share:/usr/share",
// our desired integration dir directly. See #2711.
// <https://specifications.freedesktop.org/basedir-spec/0.6/#variables>
const old = env.get(xdg_data_dir_key) orelse "/usr/local/share:/usr/share";
const prepended = try std.fmt.allocPrint(stack_alloc, "{s}{c}{s}", .{
integ_dir, integ_dir,
std.fs.path.delimiter, ),
old, );
}); }
defer stack_alloc.free(prepended);
try env.put(xdg_data_dir_key, prepended); test "xdg: empty XDG_DATA_DIRS" {
} const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var env = EnvMap.init(alloc);
defer env.deinit();
try setupXdgDataDirs(alloc, ".", &env);
try testing.expectEqualStrings("./shell-integration", env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR").?);
try testing.expectEqualStrings("./shell-integration:/usr/local/share:/usr/share", env.get("XDG_DATA_DIRS").?);
}
test "xdg: existing XDG_DATA_DIRS" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var env = EnvMap.init(alloc);
defer env.deinit();
try env.put("XDG_DATA_DIRS", "/opt/share");
try setupXdgDataDirs(alloc, ".", &env);
try testing.expectEqualStrings("./shell-integration", env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR").?);
try testing.expectEqualStrings("./shell-integration:/opt/share", env.get("XDG_DATA_DIRS").?);
} }
/// Setup the zsh automatic shell integration. This works by setting /// Setup the zsh automatic shell integration. This works by setting