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 /build | |
| 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 'build')
| -rw-r--r-- | build/gen_lua_anchor.zig | 144 | ||||
| -rw-r--r-- | build/gen_lua_headers_embed.zig | 68 | ||||
| -rw-r--r-- | build/gen_luarocks_embed.zig | 157 | ||||
| -rw-r--r-- | build/panto_lua_repl.c | 49 |
4 files changed, 418 insertions, 0 deletions
diff --git a/build/gen_lua_anchor.zig b/build/gen_lua_anchor.zig new file mode 100644 index 0000000..1143c2c --- /dev/null +++ b/build/gen_lua_anchor.zig @@ -0,0 +1,144 @@ +//! Build-time codegen: scan Lua's `lua.h` and `lauxlib.h` for every +//! `LUA_API` / `LUALIB_API` function declaration, and emit a C source +//! file that takes the address of each into a single `__attribute__ +//! ((used))` array. +//! +//! Result: the linker can't drop those functions even under LTO + +//! gc-sections, because they're reachable from the surviving array. +//! Combined with `rdynamic = true` on the executable, every Lua API +//! function ends up in the dynamic symbol table for C rocks (luv.so +//! etc.) to resolve at `dlopen` time. +//! +//! Invoked from `build.zig`: +//! +//! gen-lua-anchor <lua-src-dir> <out-file> + +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + const arena = init.arena.allocator(); + const io = init.io; + + var args = init.minimal.args.iterate(); + defer args.deinit(); + _ = args.next(); + const src_dir = args.next() orelse return error.MissingSrcDir; + const out_path = args.next() orelse return error.MissingOutPath; + + var names: std.array_list.Managed([]const u8) = .init(arena); + try collectFrom(arena, io, src_dir, "lua.h", &names); + try collectFrom(arena, io, src_dir, "lauxlib.h", &names); + + // Deduplicate via a string set. + var seen: std.StringHashMapUnmanaged(void) = .empty; + try seen.ensureTotalCapacity(arena, @intCast(names.items.len * 2)); + var unique: std.array_list.Managed([]const u8) = .init(arena); + for (names.items) |n| { + const gop = seen.getOrPutAssumeCapacity(n); + if (!gop.found_existing) try unique.append(n); + } + std.mem.sort([]const u8, unique.items, {}, lessThanString); + + var out_file = try std.Io.Dir.cwd().createFile(io, out_path, .{}); + defer out_file.close(io); + var buf: [4096]u8 = undefined; + var writer = out_file.writer(io, &buf); + const w = &writer.interface; + + try w.writeAll( + \\/* Auto-generated by build/gen_lua_anchor.zig. Do not edit. + \\ * + \\ * Holds a reference to every Lua public API function so the + \\ * linker can't drop them under LTO + gc-sections. C rocks + \\ * loaded at runtime (luv.so etc.) resolve these symbols + \\ * against the host process's symbol table; combined with + \\ * rdynamic on the executable, this keeps the whole API + \\ * surface exported. + \\ */ + \\ + \\#include "lua.h" + \\#include "lauxlib.h" + \\ + \\__attribute__((used, visibility("default"))) + \\const void *const _panto_lua_api_anchor[] = { + \\ + ); + for (unique.items) |n| { + try w.print(" (const void *)&{s},\n", .{n}); + } + try w.writeAll("};\n"); + try w.flush(); +} + +fn lessThanString(_: void, a: []const u8, b: []const u8) bool { + return std.mem.lessThan(u8, a, b); +} + +fn collectFrom( + arena: std.mem.Allocator, + io: std.Io, + src_dir: []const u8, + filename: []const u8, + out: *std.array_list.Managed([]const u8), +) !void { + const path = try std.fs.path.join(arena, &.{ src_dir, filename }); + const contents = try std.Io.Dir.cwd().readFileAlloc(io, path, arena, .unlimited); + + // Walk line by line. When we hit a line starting with `LUA_API`, + // accumulate continuation lines until we see `);`. From the joined + // string, extract the function name \u2014 either parenthesized + // (`(lua_foo)`) or bare (`lua_foo(...)`). + var line_it = std.mem.splitScalar(u8, contents, '\n'); + while (line_it.next()) |first_line| { + const trimmed = std.mem.trimStart(u8, first_line, " \t"); + if (!std.mem.startsWith(u8, trimmed, "LUA_API") and + !std.mem.startsWith(u8, trimmed, "LUALIB_API")) continue; + + var joined: std.array_list.Managed(u8) = .init(arena); + try joined.appendSlice(first_line); + while (std.mem.indexOf(u8, joined.items, ");") == null) { + const next_line = line_it.next() orelse break; + try joined.append(' '); + try joined.appendSlice(next_line); + } + + if (extractName(joined.items)) |name| { + try out.append(try arena.dupe(u8, name)); + } + } +} + +/// Return the function-name portion of a Lua API declaration. Handles +/// both `LUA_API <ret> (lua_foo) (...)` and `LUA_API <ret> lua_foo(...)`. +fn extractName(decl: []const u8) ?[]const u8 { + // Find a `(name)` token first. + var i: usize = 0; + while (i + 1 < decl.len) : (i += 1) { + if (decl[i] != '(') continue; + const end = std.mem.indexOfScalar(u8, decl[i + 1 ..], ')') orelse return null; + const candidate = decl[i + 1 .. i + 1 + end]; + if (isApiName(candidate)) return candidate; + // Skip this candidate; try further on. + } + // Fall back: bare-name pattern (no parens around the name). + var j: usize = 0; + while (j < decl.len) : (j += 1) { + if (j + 4 > decl.len) break; + if (!std.mem.eql(u8, decl[j .. j + 4], "lua_") and + !std.mem.eql(u8, decl[j .. j + 5], "luaL_")) continue; + var end = j; + while (end < decl.len and (std.ascii.isAlphanumeric(decl[end]) or decl[end] == '_')) end += 1; + return decl[j..end]; + } + return null; +} + +fn isApiName(s: []const u8) bool { + if (s.len < 4) return false; + const ok_prefix = std.mem.startsWith(u8, s, "lua_") or std.mem.startsWith(u8, s, "luaL_"); + if (!ok_prefix) return false; + for (s) |ch| { + if (!std.ascii.isAlphanumeric(ch) and ch != '_') return false; + } + return true; +} diff --git a/build/gen_lua_headers_embed.zig b/build/gen_lua_headers_embed.zig new file mode 100644 index 0000000..1ac4004 --- /dev/null +++ b/build/gen_lua_headers_embed.zig @@ -0,0 +1,68 @@ +//! Build-time codegen: emit a Zig module exposing every Lua public +//! header from the Lua source tarball as embedded bytes. Bootstrap +//! writes these to `$PANTO_HOME/rocks/lua-X.Y.Z/include/` on first run +//! so luarocks can compile C rocks against them. +//! +//! Invoked from `build.zig`: +//! +//! gen-lua-headers-embed <lua-src-dir> <out-file> +//! +//! `<lua-src-dir>` is the `src/` directory of the Lua tarball. + +const std = @import("std"); + +const headers = [_][]const u8{ + "lua.h", + "luaconf.h", + "lualib.h", + "lauxlib.h", + // hpp is the C++-friendly include shim shipped upstream; some C + // rocks reference it. + "lua.hpp", +}; + +pub fn main(init: std.process.Init) !void { + const arena = init.arena.allocator(); + const io = init.io; + + var args = init.minimal.args.iterate(); + defer args.deinit(); + _ = args.next(); + const embed_prefix = args.next() orelse return error.MissingEmbedPrefix; + const src_dir = args.next() orelse return error.MissingSrcDir; + const out_path = args.next() orelse return error.MissingOutPath; + + var out_file = try std.Io.Dir.cwd().createFile(io, out_path, .{}); + defer out_file.close(io); + var buf: [4096]u8 = undefined; + var writer = out_file.writer(io, &buf); + const w = &writer.interface; + + try w.writeAll( + \\//! Auto-generated. Do not edit. See build/gen_lua_headers_embed.zig. + \\ + \\pub const Entry = struct { + \\ name: []const u8, + \\ contents: []const u8, + \\}; + \\ + \\pub const files: []const Entry = &.{ + \\ + ); + + for (headers) |name| { + const abs = try std.fs.path.join(arena, &.{ src_dir, name }); + // Verify the file exists; emit nothing for missing files (lua.hpp + // exists in current Lua releases but we don't want to hard-fail + // if the tarball ever drops it). + std.Io.Dir.cwd().access(io, abs, .{}) catch continue; + const embed_path = try std.fs.path.join(arena, &.{ embed_prefix, name }); + try w.print( + " .{{ .name = \"{s}\", .contents = @embedFile(\"{s}\") }},\n", + .{ name, embed_path }, + ); + } + + try w.writeAll("};\n"); + try w.flush(); +} diff --git a/build/gen_luarocks_embed.zig b/build/gen_luarocks_embed.zig new file mode 100644 index 0000000..3f85177 --- /dev/null +++ b/build/gen_luarocks_embed.zig @@ -0,0 +1,157 @@ +//! Build-time codegen: walk the luarocks `src/` tree and emit a Zig +//! module that exposes every Lua file as an `@embedFile` entry, with a +//! comptime table mapping `require`-style module names to contents. +//! +//! Invoked from `build.zig`: +//! +//! gen-luarocks-embed <src-dir> <out-file> +//! +//! `<src-dir>` is `luarocks_src/src` (contains `luarocks/`, `compat53/`, +//! and `bin/`). `<out-file>` is the Zig source to write. +//! +//! The generated file looks like: +//! +//! pub const Entry = struct { name: []const u8, contents: []const u8 }; +//! pub const files: []const Entry = &.{ +//! .{ .name = "luarocks.cmd", .contents = @embedFile("...") }, +//! ... +//! }; +//! pub const luarocks_main: []const u8 = @embedFile("..."); +//! pub const luarocks_admin_main: []const u8 = @embedFile("..."); +//! +//! The `@embedFile` paths are absolute paths into the Zig cache. That's +//! safe because the codegen step depends on the luarocks dependency, so +//! the cache entry exists by the time the generated source is compiled. + +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + const arena = init.arena.allocator(); + const io = init.io; + + var args = init.minimal.args.iterate(); + defer args.deinit(); + _ = args.next(); // skip program name + const embed_prefix = args.next() orelse return error.MissingEmbedPrefix; + const src_dir_arg = args.next() orelse return error.MissingSrcDir; + const out_path_arg = args.next() orelse return error.MissingOutPath; + + // Walk src/luarocks and src/compat53 collecting *.lua files; pick + // up src/bin/luarocks and src/bin/luarocks-admin separately. + var entries: std.array_list.Managed(Entry) = .init(arena); + try collect(arena, io, src_dir_arg, embed_prefix, "luarocks", &entries); + try collect(arena, io, src_dir_arg, embed_prefix, "compat53", &entries); + + // Sort for deterministic output. + std.mem.sort(Entry, entries.items, {}, Entry.lessThan); + + const bin_luarocks = try std.fs.path.join(arena, &.{ embed_prefix, "bin/luarocks" }); + const bin_luarocks_admin = try std.fs.path.join(arena, &.{ embed_prefix, "bin/luarocks-admin" }); + + var out_file = try std.Io.Dir.cwd().createFile(io, out_path_arg, .{}); + defer out_file.close(io); + var buf: [4096]u8 = undefined; + var writer = out_file.writer(io, &buf); + const w = &writer.interface; + + try w.writeAll( + \\//! Auto-generated. Do not edit. See build/gen_luarocks_embed.zig. + \\ + \\pub const Entry = struct { + \\ /// Lua `require` path, e.g. "luarocks.core.cfg". + \\ name: []const u8, + \\ /// File contents (Lua source). + \\ contents: []const u8, + \\}; + \\ + \\pub const files: []const Entry = &.{ + \\ + ); + + for (entries.items) |e| { + try w.print( + " .{{ .name = \"{s}\", .contents = @embedFile(\"{s}\") }},\n", + .{ e.require_name, e.embed_path }, + ); + } + + try w.writeAll("};\n\n"); + try w.print( + "pub const luarocks_main: []const u8 = @embedFile(\"{s}\");\n", + .{bin_luarocks}, + ); + try w.print( + "pub const luarocks_admin_main: []const u8 = @embedFile(\"{s}\");\n", + .{bin_luarocks_admin}, + ); + + try w.flush(); +} + +const Entry = struct { + require_name: []const u8, + embed_path: []const u8, + + fn lessThan(_: void, a: Entry, b: Entry) bool { + return std.mem.lessThan(u8, a.require_name, b.require_name); + } +}; + +/// Walk `<root>/<subdir>` recursively, collecting every `.lua` file. The +/// `require` name is computed as the path relative to `<root>` with `/` +/// replaced by `.` and `.lua` stripped, then `/init.lua` collapsed to +/// the parent directory. +fn collect( + arena: std.mem.Allocator, + io: std.Io, + root_abs: []const u8, + embed_prefix: []const u8, + subdir: []const u8, + out: *std.array_list.Managed(Entry), +) !void { + const start_abs = try std.fs.path.join(arena, &.{ root_abs, subdir }); + var dir = std.Io.Dir.cwd().openDir(io, start_abs, .{ .iterate = true }) catch |err| switch (err) { + error.FileNotFound => return, + else => return err, + }; + defer dir.close(io); + + var walker = try dir.walk(arena); + defer walker.deinit(); + + while (try walker.next(io)) |entry| { + if (entry.kind != .file) continue; + if (!std.mem.endsWith(u8, entry.basename, ".lua")) continue; + + // `embed_path` lives under the staged tree (`<embed_prefix>/...`) + // so `@embedFile` can resolve it relative to the generated zig. + const embed_path = try std.fs.path.join( + arena, + &.{ embed_prefix, subdir, entry.path }, + ); + const rel = try std.mem.concat(arena, u8, &.{ subdir, "/", entry.path }); + + // Compute `require` name: drop ".lua", convert `/` to `.`. + // + // We deliberately do NOT collapse `/init.lua` into the parent + // module name. Lua's stock path searcher checks both + // `foo.lua` and `foo/init.lua` when you `require("foo")`, so + // the on-disk layout would resolve fine without our help — + // and luarocks's own tree contains both `luarocks/cmd.lua` + // (the package) and `luarocks/cmd/init.lua` (the subcommand + // module named `luarocks.cmd.init`). Collapsing would alias + // them onto one name. The embedded searcher serves whichever + // name appears literally in the source tree; the standard + // searcher behavior emerges from cmd.lua being a sibling. + const stem = rel[0 .. rel.len - ".lua".len]; + const require_name = try arena.alloc(u8, stem.len); + for (stem, 0..) |ch, i| { + require_name[i] = if (ch == '/') '.' else ch; + } + + try out.append(.{ + .require_name = require_name, + .embed_path = embed_path, + }); + } +} diff --git a/build/panto_lua_repl.c b/build/panto_lua_repl.c new file mode 100644 index 0000000..ce99cb1 --- /dev/null +++ b/build/panto_lua_repl.c @@ -0,0 +1,49 @@ +/* + * panto's hook into the upstream `lua.c` standalone interpreter. + * + * We need to call into `pmain` (the body of the standalone REPL/driver) + * with a pre-configured `lua_State` — specifically, one where panto's + * embedded-luarocks searcher and configured `package.path` have already + * been installed. Upstream `pmain` is `static`, so we can't link to it + * from Zig directly. + * + * Trick: include `lua.c` verbatim with `static` redefined to empty, so + * every static gets external linkage. We rename `main` to a stub via + * macro so it doesn't conflict with panto's own `main`. Then we expose + * a single function, `panto_lua_pmain`, that runs `pmain` against a + * caller-provided state. + * + * `lua.c` also defines its own signal handlers via `setsignal` / + * `laction`. We leave those alone — they're useful in `panto lua` too, + * giving Ctrl-C the same behavior it has in upstream `lua`. + */ + +/* Remove `static` everywhere lua.c uses it so symbols are externally + * accessible. We undef `static` back to default for any system headers + * that follow, in case they care. */ +#define static +#include "lua.c" +#undef static + +/* Forward declarations of the (now non-static) symbols we use. */ +extern int pmain(lua_State *L); + +/* + * Trampoline: push (argc, argv) onto `L` and run `pmain` in protected + * mode, mirroring `lua.c::main`'s call pattern. Returns the exit code + * appropriate for `lua` (0 on success, 1 on failure). + * + * The caller is responsible for state lifecycle (lua_close, etc.) — + * we don't open or close `L` ourselves. This is what lets panto's + * subcommand install the embedded-luarocks searcher into `L` *before* + * calling us. + */ +int panto_lua_pmain(lua_State *L, int argc, char **argv) { + lua_pushcfunction(L, &pmain); + lua_pushinteger(L, argc); + lua_pushlightuserdata(L, argv); + int status = lua_pcall(L, 2, 1, 0); + int result = lua_toboolean(L, -1); + report(L, status); + return (result && status == LUA_OK) ? 0 : 1; +} |
