1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
|
//! Filesystem layout resolution for the panto data home and the per-Lua
//! version rocks tree.
//!
//! data home = $XDG_DATA_HOME/panto
//! (or $HOME/.local/share/panto if XDG_DATA_HOME unset)
//!
//! data 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,
/// The panto data home.
home: []u8,
/// `<data home>/agent/` — the "base" 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 base.
agent_dir: []u8,
/// `<data home>/auth/` — persisted provider auth tokens, one
/// `<auth-name>.json` per OAuth session. Files are written owner-only;
/// treat them like passwords.
auth_dir: []u8,
/// `<data 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.agent_dir);
a.free(self.auth_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:
/// - `XDG_DATA_HOME` (XDG default)
/// - `HOME` (fallback)
///
/// Returns `error.NoHomeDirectory` if neither is available.
pub fn resolve(
allocator: Allocator,
environ_map: *const std.process.Environ.Map,
) !Layout {
const home = try homePath(allocator, environ_map);
errdefer allocator.free(home);
const agent_dir = try std.fs.path.join(allocator, &.{ home, "agent" });
errdefer allocator.free(agent_dir);
const auth_dir = try std.fs.path.join(allocator, &.{ home, "auth" });
errdefer allocator.free(auth_dir);
// `<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,
.agent_dir = agent_dir,
.auth_dir = auth_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 the panto data home. Returns owned bytes.
/// Returns owned bytes.
pub fn homePath(
allocator: Allocator,
environ_map: *const std.process.Environ.Map,
) ![]u8 {
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: 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));
}
|