summaryrefslogtreecommitdiff
path: root/src/panto_home.zig
diff options
context:
space:
mode:
authorT <t@tjp.lol>2026-05-27 08:04:04 -0600
committerT <t@tjp.lol>2026-05-27 11:46:52 -0600
commit576891dc2ec4d917932a4c396471d4bbbad90c8e (patch)
tree0662d629cf15a2e9cbb51353f6d3abe6d2c6edb5 /src/panto_home.zig
parentb72a405534d6be019573ee0a806014e2713fe55e (diff)
Finish the hard parts of the lua makeover
- bundle luarocks source in the panto binary - bootstrap process (intended for first `panto` run): - make ~/.local/share/panto/... - write out luarocks sources into it - run luarocks to install luv - new `panto bootstrap` command just runs the bootstrap - `panto bootstrap --force` removes everything and re-bootstraps - new `panto lua` command just runs panto's embedded lua
Diffstat (limited to 'src/panto_home.zig')
-rw-r--r--src/panto_home.zig206
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));
+}