diff options
| author | t <t@tjp.lol> | 2026-07-01 15:36:20 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-07-01 15:37:04 -0600 |
| commit | 800b2c4b6115845f6bf15d90484b3e80e81a606f (patch) | |
| tree | 914981ad5c49e8941280d015923b7c3143463d64 /src/luarocks_runtime.zig | |
| parent | ef7c37c3423ceb896f20676525307f15d6e927b4 (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.zig | 122 |
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); |
