summaryrefslogtreecommitdiff
path: root/src/luarocks_runtime.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-07-01 15:36:20 -0600
committert <t@tjp.lol>2026-07-01 15:37:04 -0600
commit800b2c4b6115845f6bf15d90484b3e80e81a606f (patch)
tree914981ad5c49e8941280d015923b7c3143463d64 /src/luarocks_runtime.zig
parentef7c37c3423ceb896f20676525307f15d6e927b4 (diff)
Load extensions via unified policy, entry/activate, rocks and paths
Replace the split [tools]/[extensions] config sections and the two-stage load gate with a single model: - [extensions] is now one policy resolved across all layers. Rules from every layer are kept (not clobbered) and resolved by last-match-wins after sorting by (layer, glob specificity, deny-last), so a base layer can carve one name out of an otherwise-denied group and a higher layer's broad rule still wins. Default is allow; whitelist via deny=["**"]. - Registration is deferred: a source returns an entry {name, activate} (or a list, or the sugar tool table), is always eval'd side-effect-free, and only permitted names get activate()d. Identity is the declared name, not the filename. Collapses the old pre/post-load two-stage gate. - The loader runs two passes (eval -> shadow -> filter -> activate) with precedence project>user>base and, within a layer, rocks<paths<dir. - extensions.paths adds extra scan dirs; extensions.rocks loads luarocks packages as extension sources (require-as-entries). Startup installs only missing rocks (no per-launch network hit); panto update force- (re)installs every configured rock. deny is a feature toggle, not a security boundary: rocks is where registry code enters, so the pin list is the trust boundary. Rock integrity (first-party GPG signing + --verify) is designed but not yet wired; until then rocks pins which version, not which bytes. Breaking: [tools] is removed; extensions must return entries. Built-in tools and the wc example keep working via the sugar tool form.
Diffstat (limited to 'src/luarocks_runtime.zig')
-rw-r--r--src/luarocks_runtime.zig122
1 files changed, 105 insertions, 17 deletions
diff --git a/src/luarocks_runtime.zig b/src/luarocks_runtime.zig
index 1505b8f..96b2e37 100644
--- a/src/luarocks_runtime.zig
+++ b/src/luarocks_runtime.zig
@@ -916,9 +916,20 @@ fn batteryInstalled(
layout: panto_home.Layout,
battery: manifest.Battery,
) !bool {
+ return rockMetaExists(allocator, io, layout, battery.name, battery.version);
+}
+
+/// Whether `<tree>/lib/luarocks/rocks-<short>/<name>/<version>/` exists.
+fn rockMetaExists(
+ allocator: Allocator,
+ io: Io,
+ layout: panto_home.Layout,
+ name: []const u8,
+ version: []const u8,
+) !bool {
const subpath = try std.fs.path.join(
allocator,
- &.{ layout.rocks_metadata_dir, battery.name, battery.version },
+ &.{ layout.rocks_metadata_dir, name, version },
);
defer allocator.free(subpath);
@@ -955,10 +966,92 @@ fn installBattery(
L: *c.lua_State,
battery: manifest.Battery,
) !void {
- // We wrap the embedded driver in a function that overrides
- // `os.exit` for the duration of the call. luarocks's `die()`
- // ultimately calls `os.exit(1)`, which would terminate panto
- // entirely; we want to catch the failure as a regular error.
+ _ = allocator; // no scratch needed
+ try runLuarocksDriver(L, &.{ "install", battery.name, battery.version }, battery.name);
+}
+
+/// Install a user rock from a luarocks dependency spec (e.g.
+/// `"panto-agent 1.4.2-1"`), running `luarocks install <spec-tokens>` in
+/// process against `rt`'s `lua_State`. The spec is passed through verbatim,
+/// split on whitespace into the positional `install` arguments luarocks
+/// expects (`<name> [<version>]`). Idempotent: luarocks skips a rock that is
+/// already installed at the requested version.
+pub fn installRock(rt: *LuarocksRuntime, allocator: Allocator, spec: []const u8) !void {
+ var args: std.array_list.Managed([]const u8) = .init(allocator);
+ defer args.deinit();
+ try args.append("install");
+ var it = std.mem.tokenizeAny(u8, spec, " \t");
+ while (it.next()) |tok| try args.append(tok);
+ if (args.items.len < 2) return error.LuarocksInstallFailed; // no rock name
+ try runLuarocksDriver(rt.L, args.items, spec);
+}
+
+/// Like `installRock`, but avoids a per-startup network hit: it skips the
+/// install when the local tree already has something matching the spec.
+/// - exact pin (`name version-revision`, no operators): skip if that exact
+/// version is installed.
+/// - bare name or range/operators: skip if the rock name has *any* version
+/// installed. Upgrading within a range is deferred to `panto update`.
+/// A spec whose rock is absent (new pin / new rock) always installs once.
+/// Returns true if an install actually ran.
+pub fn installRockIfMissing(
+ rt: *LuarocksRuntime,
+ allocator: Allocator,
+ io: Io,
+ spec: []const u8,
+) !bool {
+ var it = std.mem.tokenizeAny(u8, spec, " \t");
+ const name = it.next() orelse return error.LuarocksInstallFailed;
+ const version = it.next();
+ const rest = it.next();
+
+ const is_exact_pin = version != null and rest == null and
+ std.mem.indexOfAny(u8, version.?, "<>=~") == null;
+
+ const present = if (is_exact_pin)
+ try rockMetaExists(allocator, io, rt.layout, name, version.?)
+ else
+ try rockAnyVersionInstalled(allocator, io, rt.layout, name);
+
+ if (present) return false;
+ try installRock(rt, allocator, spec);
+ return true;
+}
+
+/// Whether `<tree>/lib/luarocks/rocks-<short>/<name>/` has any installed
+/// version subdirectory.
+fn rockAnyVersionInstalled(
+ allocator: Allocator,
+ io: Io,
+ layout: panto_home.Layout,
+ name: []const u8,
+) !bool {
+ const dirpath = try std.fs.path.join(allocator, &.{ layout.rocks_metadata_dir, name });
+ defer allocator.free(dirpath);
+
+ var dir = Io.Dir.cwd().openDir(io, dirpath, .{ .iterate = true }) catch |err| switch (err) {
+ error.FileNotFound, error.NotDir => return false,
+ else => return err,
+ };
+ defer dir.close(io);
+
+ var iter = dir.iterate();
+ while (try iter.next(io)) |entry| {
+ if (entry.name.len == 0 or entry.name[0] == '.') continue;
+ if (entry.kind == .directory) return true;
+ }
+ return false;
+}
+
+/// Run the embedded luarocks CLI driver in process with `cli_args`
+/// (e.g. `{"install", "<name>", "<version>"}`). `label` is used only for
+/// error messages. Overrides `os.exit` so a luarocks `die()` surfaces as a
+/// Zig error instead of terminating panto.
+fn runLuarocksDriver(
+ L: *c.lua_State,
+ cli_args: []const []const u8,
+ label: []const u8,
+) !void {
const driver = embedded_luarocks.luarocks_main;
const wrapper =
@@ -998,24 +1091,19 @@ fn installBattery(
}
}
- // Pass the driver source as the first arg, then the luarocks
- // CLI args. We use `lua_pushlstring` to push everything as
- // Lua strings (no NUL terminator needed, since lua_pushlstring
- // takes an explicit length).
+ // Pass the driver source as the first arg, then the luarocks CLI args.
+ // `lua_pushlstring` takes an explicit length (no NUL needed).
_ = c.lua_pushlstring(L, driver_slice.ptr, driver_slice.len);
- _ = c.lua_pushlstring(L, "install", 7);
- _ = c.lua_pushlstring(L, battery.name.ptr, battery.name.len);
- _ = c.lua_pushlstring(L, battery.version.ptr, battery.version.len);
-
- _ = allocator; // currently unused; reserved for future scratch needs
+ for (cli_args) |a| _ = c.lua_pushlstring(L, a.ptr, a.len);
- if (c.lua_pcallk(L, 4, 0, 0, 0, null) != 0) {
+ const nargs: c_int = @intCast(1 + cli_args.len);
+ if (c.lua_pcallk(L, nargs, 0, 0, 0, null) != 0) {
var len: usize = 0;
const msg = c.lua_tolstring(L, -1, &len);
if (msg != null) {
std.log.err(
- "panto: luarocks install of {s} {s} failed: {s}",
- .{ battery.name, battery.version, msg[0..len] },
+ "panto: luarocks install of '{s}' failed: {s}",
+ .{ label, msg[0..len] },
);
}
c.lua_settop(L, c.lua_gettop(L) - 1);