diff options
| author | T <t@tjp.lol> | 2026-05-27 08:04:04 -0600 |
|---|---|---|
| committer | T <t@tjp.lol> | 2026-05-27 11:46:52 -0600 |
| commit | 576891dc2ec4d917932a4c396471d4bbbad90c8e (patch) | |
| tree | 0662d629cf15a2e9cbb51353f6d3abe6d2c6edb5 /src/subcommand.zig | |
| parent | b72a405534d6be019573ee0a806014e2713fe55e (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/subcommand.zig')
| -rw-r--r-- | src/subcommand.zig | 231 |
1 files changed, 231 insertions, 0 deletions
diff --git a/src/subcommand.zig b/src/subcommand.zig new file mode 100644 index 0000000..97eb977 --- /dev/null +++ b/src/subcommand.zig @@ -0,0 +1,231 @@ +//! Subcommand dispatch for the `panto` CLI. +//! +//! Routes argv[1] to one of: +//! - `lua` — drop into the embedded standalone Lua interpreter +//! (panto's `lua.c` build), with luarocks's runtime +//! bootstrap completed first so `require("luarocks.*")` +//! and the configured rocks tree work the same as in +//! the agent process. +//! - `bootstrap` — run the luarocks runtime bootstrap pipeline only; +//! exit before entering any agent loop. Lets users +//! do first-run setup on a fresh machine without +//! starting a chat session. +//! - anything else (or absent) — fall through to the agent REPL. +//! +//! Both `lua` and `bootstrap` end up calling `luarocks_runtime.bootstrap` +//! before doing their thing. The agent path does the same; the only +//! difference is whether the agent loop runs afterward. + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Io = std.Io; + +const lua_bridge = @import("lua_bridge.zig"); +const luarocks_runtime = @import("luarocks_runtime.zig"); +const self_exe = @import("self_exe.zig"); + +const c = lua_bridge.c; + +pub const Action = enum { + /// Continue with the default agent REPL. + agent, + /// Bootstrap is already done; the dispatcher consumed the subcommand. + /// `main` should exit immediately. + done, +}; + +/// Inspect `argv[1]`, run the appropriate subcommand, and return what +/// the caller should do next. On `.agent`, the dispatcher leaves argv +/// untouched and `main` continues as before. On `.done`, the caller +/// must return promptly (the subcommand has already produced output). +/// +/// `panto_executable_path` is the absolute path of the running panto +/// binary, used both to wire up the embedded luarocks `LUA` variable +/// and to `exec` ourselves where needed. +pub fn dispatch( + allocator: Allocator, + io: Io, + environ_map: *const std.process.Environ.Map, + args: std.process.Args, + panto_executable_path: []const u8, +) !Action { + var it = args.iterate(); + defer it.deinit(); + _ = it.next(); // argv[0] + + const sub = it.next() orelse return .agent; + + if (std.mem.eql(u8, sub, "lua")) { + try runLuaSubcommand(allocator, io, environ_map, args, panto_executable_path); + return .done; + } + if (std.mem.eql(u8, sub, "bootstrap")) { + var force = false; + while (it.next()) |flag| { + if (std.mem.eql(u8, flag, "--force")) { + force = true; + } else { + std.log.err("panto bootstrap: unknown flag '{s}'", .{flag}); + return error.UnknownFlag; + } + } + try runBootstrapSubcommand(allocator, io, environ_map, panto_executable_path, .{ .force = force }); + return .done; + } + return .agent; +} + +pub const BootstrapOptions = struct { + /// Wipe the per-Lua-version tree before reinstalling everything. + /// Surfaced as `panto bootstrap --force`. Equivalent to deleting + /// `$PANTO_HOME/rocks/lua-X.Y.Z/` by hand and then running + /// `panto bootstrap`. + force: bool = false, +}; + +// --------------------------------------------------------------------------- +// `panto lua` +// --------------------------------------------------------------------------- + +extern "c" fn panto_lua_pmain(L: *c.lua_State, argc: c_int, argv: [*]?[*:0]u8) c_int; + +/// Drop into the embedded Lua standalone interpreter, with the +/// luarocks runtime bootstrap completed so `require("luarocks.*")` +/// and rocks installed under `$PANTO_HOME` are visible. +/// +/// argv is rewritten so the interpreter sees `lua [...args]` rather +/// than `panto lua [...args]` — matching upstream behavior. The first +/// argument visible to `pmain` is the program name; this matters for +/// `arg[0]` and error reporting. +fn runLuaSubcommand( + allocator: Allocator, + io: Io, + environ_map: *const std.process.Environ.Map, + args: std.process.Args, + panto_executable_path: []const u8, +) !void { + // Build a fresh lua_State that we own, configure it like luarocks + // expects, then hand it to `pmain`. + const L = c.luaL_newstate() orelse return error.LuaInitFailed; + defer c.lua_close(L); + + // Run bootstrap against this state. This installs the embedded + // searcher, configures package.path/cpath, and stages on-disk + // resources. We deliberately do NOT call `luaL_openlibs` here — + // `pmain` does that itself, and we want exactly the upstream + // ordering for everything that runs inside the REPL. + // + // The searcher install only requires `package.searchers` to be + // present; the stock libs ship it. We open libs once here just + // to satisfy that, then pmain's own `luaL_openlibs` is idempotent. + c.luaL_openlibs(L); + const rt = try luarocks_runtime.bootstrap( + allocator, + io, + environ_map, + L, + panto_executable_path, + ); + defer rt.deinit(); + + // Re-create the argv the standalone interpreter expects. argv[0] + // is the program name; argv[1..] are the user's args. + var raw_args = args.iterate(); + defer raw_args.deinit(); + _ = raw_args.next(); // panto + _ = raw_args.next(); // lua + + var argv_list: std.array_list.Managed([:0]u8) = .init(allocator); + defer { + for (argv_list.items) |s| allocator.free(s); + argv_list.deinit(); + } + + // Program name first. + try argv_list.append(try allocator.dupeZ(u8, "lua")); + while (raw_args.next()) |a| { + try argv_list.append(try allocator.dupeZ(u8, a)); + } + + // Build a `[*]?[*:0]u8` argv pointer array. lua.c expects a + // NULL-terminated array (it uses `argv[i]` indexed access through + // argc; the trailing NULL is conventional for C `main`). + var argv_c: std.array_list.Managed(?[*:0]u8) = .init(allocator); + defer argv_c.deinit(); + for (argv_list.items) |s| { + try argv_c.append(s.ptr); + } + try argv_c.append(null); + + const exit_code = panto_lua_pmain(L, @intCast(argv_list.items.len), argv_c.items.ptr); + if (exit_code != 0) std.process.exit(@intCast(exit_code)); +} + +// --------------------------------------------------------------------------- +// `panto bootstrap` +// --------------------------------------------------------------------------- + +/// Run the luarocks bootstrap and exit. Useful for first-run setup on +/// a clean machine (downloads + compiles batteries, stages headers, +/// materializes config) and for CI/scripted installs. +/// +/// Idempotent: subsequent invocations no-op fast, unless `force` was +/// passed — then the entire per-Lua-version tree is wiped before the +/// regular bootstrap pipeline runs. +fn runBootstrapSubcommand( + allocator: Allocator, + io: Io, + environ_map: *const std.process.Environ.Map, + panto_executable_path: []const u8, + opts: BootstrapOptions, +) !void { + if (opts.force) { + try luarocks_runtime.wipeTree(allocator, io, environ_map); + } + + const L = c.luaL_newstate() orelse return error.LuaInitFailed; + defer c.lua_close(L); + c.luaL_openlibs(L); + + const rt = try luarocks_runtime.bootstrap( + allocator, + io, + environ_map, + L, + panto_executable_path, + ); + defer rt.deinit(); + + // Pleasant single-line confirmation. The interesting bits (rock + // installs etc.) print their own progress. + std.log.info( + "panto bootstrap: tree ready at {s}", + .{rt.layout.tree}, + ); +} + +// --------------------------------------------------------------------------- +// `panto lua` argv plumbing — sketched against the older Args API for +// reference (kept here so the design notes survive the implementation). +// --------------------------------------------------------------------------- +// +// Because we own the `lua_State` end-to-end, the subcommand can also +// expose extra panto-specific globals to user code (e.g. surface the +// resolved $PANTO_HOME) without disturbing upstream `lua.c` behavior. +// Step out of scope for the current makeover; add when needed. + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +const testing = std.testing; + +// Note: `dispatch` reads from the process's real argv, which isn't +// controllable from a unit test. The behavior is exercised by +// integration runs of the panto binary. We test the smaller pieces. +// +// Suppress dead-code warnings for `self_exe` (it's used by main, not +// by tests in this module). +test { + _ = self_exe; +} |
