//! 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/agent/` — the "system" extension/tool tree, /// populated at bootstrap from files embedded into the panto /// binary. Searched after user/project layers for tools and /// extensions; project shadows user shadows system. agent_dir: []u8, /// `$PANTO_HOME/rocks/lua-/` — the versioned tree. tree: []u8, /// `/include/` — where Lua headers are staged. include_dir: []u8, /// `/share/lua//` — pure-Lua rocks. share_lua_dir: []u8, /// `/lib/lua//` — C rocks (.so/.dylib). lib_lua_dir: []u8, /// `/lib/luarocks/rocks-/` — luarocks's rock metadata /// per the standard tree layout. rocks_metadata_dir: []u8, /// `/etc/luarocks/` — luarocks's own config dir, where the /// per-version `config-.lua` lives. We set `SYSCONFDIR` to /// this when constructing `luarocks.core.hardcoded`. sysconfdir: []u8, /// `/etc/luarocks/config-.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.agent_dir); 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); const agent_dir = try std.fs.path.join(allocator, &.{ home, "agent" }); errdefer allocator.free(agent_dir); // `/rocks/lua-` 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, .agent_dir = agent_dir, .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.agent_dir); 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)); }