diff options
Diffstat (limited to 'src/panto_home.zig')
| -rw-r--r-- | src/panto_home.zig | 206 |
1 files changed, 206 insertions, 0 deletions
diff --git a/src/panto_home.zig b/src/panto_home.zig new file mode 100644 index 0000000..b7c1799 --- /dev/null +++ b/src/panto_home.zig @@ -0,0 +1,206 @@ +//! Filesystem layout resolution for `$PANTO_HOME` and the per-Lua +//! version rocks tree. +//! +//! $PANTO_HOME = $XDG_DATA_HOME/panto +//! (or $HOME/.local/share/panto if XDG_DATA_HOME unset) +//! +//! $PANTO_HOME/ +//! rocks/ +//! lua-5.4.7/ ← current tree +//! include/ ← Lua headers staged by bootstrap +//! share/lua/5.4/ ← installed pure-Lua rocks +//! lib/lua/5.4/ ← installed C rocks (.so/.dylib) +//! lib/luarocks/rocks-5.4/ ← luarocks's own rock metadata +//! etc/luarocks/ ← luarocks config-5.4.lua +//! +//! See Q3 of LUA_MAKEOVER.md for the rationale behind the versioned +//! subdirectory. We never write to the tree of a different Lua version +//! — a panto upgrade that bumps Lua creates a fresh tree alongside the +//! old one, leaving the old one in place for rollback. + +const std = @import("std"); +const manifest = @import("manifest.zig"); + +const Allocator = std.mem.Allocator; +const Io = std.Io; + +/// Resolved set of filesystem paths panto uses for its rocks tree. All +/// fields are owned by the same allocator passed to `resolve`. +pub const Layout = struct { + allocator: Allocator, + /// `$PANTO_HOME` itself. + home: []u8, + /// `$PANTO_HOME/rocks/lua-<lua_version>/` — the versioned tree. + tree: []u8, + /// `<tree>/include/` — where Lua headers are staged. + include_dir: []u8, + /// `<tree>/share/lua/<short>/` — pure-Lua rocks. + share_lua_dir: []u8, + /// `<tree>/lib/lua/<short>/` — C rocks (.so/.dylib). + lib_lua_dir: []u8, + /// `<tree>/lib/luarocks/rocks-<short>/` — luarocks's rock metadata + /// per the standard tree layout. + rocks_metadata_dir: []u8, + /// `<tree>/etc/luarocks/` — luarocks's own config dir, where the + /// per-version `config-<short>.lua` lives. We set `SYSCONFDIR` to + /// this when constructing `luarocks.core.hardcoded`. + sysconfdir: []u8, + /// `<tree>/etc/luarocks/config-<short>.lua` — the path of the config + /// file we materialize at bootstrap. + config_file: []u8, + + pub fn deinit(self: Layout) void { + const a = self.allocator; + a.free(self.home); + a.free(self.tree); + a.free(self.include_dir); + a.free(self.share_lua_dir); + a.free(self.lib_lua_dir); + a.free(self.rocks_metadata_dir); + a.free(self.sysconfdir); + a.free(self.config_file); + } +}; + +/// Resolve every path the runtime cares about. Environment-driven: +/// - `PANTO_HOME` (explicit override) +/// - `XDG_DATA_HOME` (XDG default) +/// - `HOME` (fallback) +/// +/// Returns `error.NoHomeDirectory` if none of those are available and +/// no `PANTO_HOME` was set. +pub fn resolve( + allocator: Allocator, + environ_map: *const std.process.Environ.Map, +) !Layout { + const home = try resolveHome(allocator, environ_map); + errdefer allocator.free(home); + + // `<home>/rocks/lua-<lua_version>` + const tree_subdir = try std.fmt.allocPrint( + allocator, + "lua-{s}", + .{manifest.lua_version}, + ); + defer allocator.free(tree_subdir); + const tree = try std.fs.path.join(allocator, &.{ home, "rocks", tree_subdir }); + errdefer allocator.free(tree); + + const short = manifest.lua_short_version; + + const include_dir = try std.fs.path.join(allocator, &.{ tree, "include" }); + errdefer allocator.free(include_dir); + const share_lua_dir = try std.fs.path.join(allocator, &.{ tree, "share", "lua", short }); + errdefer allocator.free(share_lua_dir); + const lib_lua_dir = try std.fs.path.join(allocator, &.{ tree, "lib", "lua", short }); + errdefer allocator.free(lib_lua_dir); + const rocks_subdir = try std.fmt.allocPrint(allocator, "rocks-{s}", .{short}); + defer allocator.free(rocks_subdir); + const rocks_metadata_dir = try std.fs.path.join( + allocator, + &.{ tree, "lib", "luarocks", rocks_subdir }, + ); + errdefer allocator.free(rocks_metadata_dir); + const sysconfdir = try std.fs.path.join(allocator, &.{ tree, "etc", "luarocks" }); + errdefer allocator.free(sysconfdir); + const config_basename = try std.fmt.allocPrint(allocator, "config-{s}.lua", .{short}); + defer allocator.free(config_basename); + const config_file = try std.fs.path.join(allocator, &.{ sysconfdir, config_basename }); + errdefer allocator.free(config_file); + + return .{ + .allocator = allocator, + .home = home, + .tree = tree, + .include_dir = include_dir, + .share_lua_dir = share_lua_dir, + .lib_lua_dir = lib_lua_dir, + .rocks_metadata_dir = rocks_metadata_dir, + .sysconfdir = sysconfdir, + .config_file = config_file, + }; +} + +/// Resolve `$PANTO_HOME` honoring overrides in the documented order. +/// Returns owned bytes. +fn resolveHome( + allocator: Allocator, + environ_map: *const std.process.Environ.Map, +) ![]u8 { + if (environ_map.get("PANTO_HOME")) |explicit| { + return allocator.dupe(u8, explicit); + } + if (environ_map.get("XDG_DATA_HOME")) |xdg| { + return std.fs.path.join(allocator, &.{ xdg, "panto" }); + } + if (environ_map.get("HOME")) |hh| { + return std.fs.path.join(allocator, &.{ hh, ".local", "share", "panto" }); + } + return error.NoHomeDirectory; +} + +/// Create every directory referenced by `layout`, recursively. Idempotent +/// — existing directories are left alone. +pub fn ensureDirsExist(layout: Layout, io: Io) !void { + try makePathRecursive(io, layout.home); + try makePathRecursive(io, layout.tree); + try makePathRecursive(io, layout.include_dir); + try makePathRecursive(io, layout.share_lua_dir); + try makePathRecursive(io, layout.lib_lua_dir); + try makePathRecursive(io, layout.rocks_metadata_dir); + try makePathRecursive(io, layout.sysconfdir); +} + +fn makePathRecursive(io: Io, path: []const u8) !void { + Io.Dir.cwd().createDirPath(io, path) catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => return err, + }; +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +const testing = std.testing; + +test "resolve: PANTO_HOME explicit override wins" { + var env: std.process.Environ.Map = .init(testing.allocator); + defer env.deinit(); + try env.put("PANTO_HOME", "/tmp/some/home"); + try env.put("XDG_DATA_HOME", "/should/be/ignored"); + + var layout = try resolve(testing.allocator, &env); + defer layout.deinit(); + + try testing.expectEqualStrings("/tmp/some/home", layout.home); + try testing.expect(std.mem.indexOf(u8, layout.tree, "/tmp/some/home/rocks/lua-") != null); + try testing.expect(std.mem.endsWith(u8, layout.share_lua_dir, "/share/lua/" ++ manifest.lua_short_version)); +} + +test "resolve: XDG_DATA_HOME is honored before HOME" { + var env: std.process.Environ.Map = .init(testing.allocator); + defer env.deinit(); + try env.put("XDG_DATA_HOME", "/x/data"); + try env.put("HOME", "/h"); + + var layout = try resolve(testing.allocator, &env); + defer layout.deinit(); + try testing.expectEqualStrings("/x/data/panto", layout.home); +} + +test "resolve: HOME fallback used if neither override is present" { + var env: std.process.Environ.Map = .init(testing.allocator); + defer env.deinit(); + try env.put("HOME", "/home/user"); + + var layout = try resolve(testing.allocator, &env); + defer layout.deinit(); + try testing.expectEqualStrings("/home/user/.local/share/panto", layout.home); +} + +test "resolve: error when no environment hint at all" { + var env: std.process.Environ.Map = .init(testing.allocator); + defer env.deinit(); + try testing.expectError(error.NoHomeDirectory, resolve(testing.allocator, &env)); +} |
