From 576891dc2ec4d917932a4c396471d4bbbad90c8e Mon Sep 17 00:00:00 2001 From: T Date: Wed, 27 May 2026 08:04:04 -0600 Subject: 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 --- build.zig | 251 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 230 insertions(+), 21 deletions(-) (limited to 'build.zig') diff --git a/build.zig b/build.zig index e675859..5bf5d44 100644 --- a/build.zig +++ b/build.zig @@ -1,5 +1,9 @@ const std = @import("std"); +const lua_version = "5.4.7"; +const lua_short_version = "5.4"; +const luarocks_version = "3.13.0"; + pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); @@ -9,10 +13,31 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); - // Fetch upstream Lua 5.4.7 (source-only tarball from lua.org). - // Reproducibility comes from the content-addressed hash in build.zig.zon. + // Fetch upstream Lua source (used both for our static library and + // staged at runtime as the `include/` headers under $PANTO_HOME). + // Reproducibility comes from the content-addressed hash in + // build.zig.zon. const lua_src = b.dependency("lua_src", .{}); - const lua = buildLua(b, target, optimize, lua_src); + const lua_anchor_path = generateLuaAnchor(b, lua_src); + + // Fetch luarocks source. We don't compile any of it — it's pure + // Lua. The codegen step below produces a `embedded_luarocks.zig` + // module that `@embedFile`s every script so the runtime can serve + // them as `require("luarocks.*")` results without disk I/O. + const luarocks_src = b.dependency("luarocks_src", .{}); + const luarocks_embed_path = generateLuarocksEmbed(b, luarocks_src); + + // Same trick for the Lua headers: bundle them so first-run + // bootstrap can stage them under $PANTO_HOME/rocks/lua-X.Y.Z/include/ + // for luarocks to find when compiling C rocks. + const lua_headers_embed_path = generateLuaHeadersEmbed(b, lua_src); + + // Constants module: makes Zig know the Lua and luarocks versions + // without duplicating literals everywhere. + const versions_mod = b.addOptions(); + versions_mod.addOption([]const u8, "lua_version", lua_version); + versions_mod.addOption([]const u8, "lua_short_version", lua_short_version); + versions_mod.addOption([]const u8, "luarocks_version", luarocks_version); // CLI executable const exe_mod = b.createModule(.{ @@ -22,13 +47,42 @@ pub fn build(b: *std.Build) void { .link_libc = true, }); exe_mod.addImport("panto", panto_lib_dep.module("panto")); - exe_mod.linkLibrary(lua); + exe_mod.addImport("versions", versions_mod.createModule()); + exe_mod.addImport("embedded_luarocks", b.createModule(.{ + .root_source_file = luarocks_embed_path, + })); + exe_mod.addImport("embedded_lua_headers", b.createModule(.{ + .root_source_file = lua_headers_embed_path, + })); + // Link Lua. We can't use a static library here because the + // standard archive-linking behavior drops object files whose + // symbols nothing in our code references — e.g. `lua_settable`, + // which luv.so needs at runtime via `dlopen` but neither panto + // itself nor the embedded lua.c repl ever call directly. Instead, + // compile the Lua sources directly *into* the executable's own + // module so every object lands in the binary. `rdynamic = true` + // below ensures the symbols are exported into the dynamic symbol + // table for `dlopen`'d rocks to resolve. + addLuaSources(exe_mod, lua_src, lua_anchor_path); exe_mod.addIncludePath(lua_src.path("src")); + // Compile lua.c (the upstream standalone interpreter, ~600 lines) + // as a separate static library so we can route the `panto lua` + // subcommand into its main() without it conflicting with our own + // main(). + const lua_repl = buildLuaRepl(b, target, optimize, lua_src); + exe_mod.linkLibrary(lua_repl); + const exe = b.addExecutable(.{ .name = "panto", .root_module = exe_mod, }); + // C rocks (luv.so and anything else loaded via dlopen) need + // Lua's C API symbols to be findable in the running process's + // symbol table. Without `-rdynamic`, the linker strips them as + // unused-from-outside; `dlopen` then fails at load time with + // "symbol not found in flat namespace '_lua_settable'" etc. + exe.rdynamic = true; b.installArtifact(exe); @@ -41,7 +95,7 @@ pub fn build(b: *std.Build) void { const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); - // CLI unit tests (minimal — most tests live in libpanto) + // CLI unit tests const test_mod = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, @@ -49,8 +103,16 @@ pub fn build(b: *std.Build) void { .link_libc = true, }); test_mod.addImport("panto", panto_lib_dep.module("panto")); - test_mod.linkLibrary(lua); + test_mod.addImport("versions", versions_mod.createModule()); + test_mod.addImport("embedded_luarocks", b.createModule(.{ + .root_source_file = luarocks_embed_path, + })); + test_mod.addImport("embedded_lua_headers", b.createModule(.{ + .root_source_file = lua_headers_embed_path, + })); + addLuaSources(test_mod, lua_src, lua_anchor_path); test_mod.addIncludePath(lua_src.path("src")); + test_mod.linkLibrary(lua_repl); const unit_tests = b.addTest(.{ .name = "panto-tests", @@ -66,16 +128,20 @@ pub fn build(b: *std.Build) void { test_step.dependOn(&lib_test_step.step); } -/// Compile Lua 5.4.7 as a static library against the upstream source tarball. -/// Excludes `lua.c` (interpreter REPL) and `luac.c` (bytecode compiler) — we -/// only want the embedded library. -fn buildLua( +/// Compile a thin wrapper around the upstream `lua.c` standalone +/// interpreter that exposes `pmain` for panto's `lua` subcommand to +/// invoke against a pre-configured `lua_State`. +/// +/// We rename `main` to `lua_unused_main` so the symbol doesn't collide +/// with panto's `main` (the wrapper never references it; the linker +/// just needs a place for it to land). +fn buildLuaRepl( b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, lua_src: *std.Build.Dependency, ) *std.Build.Step.Compile { - const lua_mod = b.createModule(.{ + const mod = b.createModule(.{ .target = target, .optimize = optimize, .link_libc = true, @@ -84,33 +150,176 @@ fn buildLua( const cflags = [_][]const u8{ "-std=gnu99", "-Wall", - "-Wextra", "-Wno-unused-parameter", + "-Wno-unused-function", + // Rename upstream `main` so it doesn't collide with panto's. + // We never call it; the wrapper invokes `pmain` directly. + "-Dmain=lua_unused_main", }; - lua_mod.addCSourceFiles(.{ + mod.addCSourceFile(.{ + .file = b.path("build/panto_lua_repl.c"), + .flags = &cflags, + }); + // The wrapper `#include`s lua.c by name; we add lua_src/src to + // the search path so `#include "lua.c"` resolves. + mod.addIncludePath(lua_src.path("src")); + addLuaPlatformMacros(mod, target); + + return b.addLibrary(.{ + .name = "lua-repl", + .linkage = .static, + .root_module = mod, + }); +} + +/// Add all the Lua 5.4 core + standard-library C sources directly to +/// `mod`. Compared to building a separate static library and linking, +/// this guarantees the linker doesn't drop "unused" objects — we need +/// every Lua API symbol present in the final binary so C rocks loaded +/// via `dlopen` can resolve them against the host process's symbol +/// table. +fn addLuaSources( + mod: *std.Build.Module, + lua_src: *std.Build.Dependency, + lua_anchor_path: std.Build.LazyPath, +) void { + const cflags = [_][]const u8{ + "-std=gnu99", + "-Wall", + "-Wextra", + "-Wno-unused-parameter", + }; + mod.addCSourceFiles(.{ .root = lua_src.path("src"), .files = &lua_files, .flags = &cflags, }); + // Tiny extra TU (codegen'd at build time): takes the address of + // every Lua API function into a `__attribute__((used))` array. + // This forces the linker to keep every API function in the binary + // even under ReleaseFast's LTO and gc-sections, regardless of + // whether our own code references them. C rocks loaded via + // `dlopen` (luv.so etc.) need every public Lua symbol resolvable + // in the host process's symbol table; combined with + // `rdynamic = true` on the executable, this gets them all into + // the dynamic symbol table. + mod.addCSourceFile(.{ + .file = lua_anchor_path, + .flags = &cflags, + }); + addLuaPlatformMacros(mod, mod.resolved_target.?); +} +fn addLuaPlatformMacros( + mod: *std.Build.Module, + target: std.Build.ResolvedTarget, +) void { // Tell Lua which platform features are available. The macros change // which `dlopen`/readline/etc. paths Lua compiles in. switch (target.result.os.tag) { - .macos => lua_mod.addCMacro("LUA_USE_MACOSX", ""), - .linux => lua_mod.addCMacro("LUA_USE_LINUX", ""), - .freebsd, .netbsd, .openbsd => lua_mod.addCMacro("LUA_USE_POSIX", ""), + .macos => mod.addCMacro("LUA_USE_MACOSX", ""), + .linux => mod.addCMacro("LUA_USE_LINUX", ""), + .freebsd, .netbsd, .openbsd => mod.addCMacro("LUA_USE_POSIX", ""), else => {}, } +} - return b.addLibrary(.{ - .name = "lua", - .linkage = .static, - .root_module = lua_mod, +/// Codegen step: walk the luarocks tarball, emit a Zig module that +/// exposes every Lua source as a `@embedFile`d entry plus a top-level +/// table mapping `require` paths (e.g. `"luarocks.core.cfg"`) to those +/// contents. The runtime installs this table into `package.preload` so +/// `require("luarocks.*")` resolves without touching disk. +/// +/// The generator emits `@embedFile("luarocks/<...>.lua")`-style paths. +/// We then materialize the luarocks source tree alongside the +/// generated Zig file via `addCopyDirectory`, so the embedded paths +/// resolve against the same root the module compiles from. (`@embedFile` +/// only allows reading files inside the module's import-path tree.) +/// +/// Two outputs are present in the resulting module: +/// - `pub const files: []const Entry` — { require_name, contents } +/// - `pub const luarocks_main` / `pub const luarocks_admin_main` +/// for the two `src/bin/` driver scripts. +fn generateLuaAnchor( + b: *std.Build, + lua_src: *std.Build.Dependency, +) std.Build.LazyPath { + const tool = b.addExecutable(.{ + .name = "gen-lua-anchor", + .root_module = b.createModule(.{ + .root_source_file = b.path("build/gen_lua_anchor.zig"), + .target = b.graph.host, + .optimize = .Debug, + }), + }); + + const run = b.addRunArtifact(tool); + run.addDirectoryArg(lua_src.path("src")); + return run.addOutputFileArg("lua_anchor.c"); +} + +fn generateLuarocksEmbed( + b: *std.Build, + luarocks_src: *std.Build.Dependency, +) std.Build.LazyPath { + const tool = b.addExecutable(.{ + .name = "gen-luarocks-embed", + .root_module = b.createModule(.{ + .root_source_file = b.path("build/gen_luarocks_embed.zig"), + .target = b.graph.host, + .optimize = .Debug, + }), }); + + const run = b.addRunArtifact(tool); + // Pass a marker arg the tool will treat as the "embed-relative" + // subpath — every `@embedFile` reference uses this prefix so + // they resolve against the same directory the generated zig file + // lives in (the one we set up next via addWriteFiles). + run.addArg("luarocks_src"); + run.addDirectoryArg(luarocks_src.path("src")); + const gen_file = run.addOutputFileArg("embedded_luarocks.zig"); + + // Stage the generated zig file next to a copy of the luarocks src + // tree, so `@embedFile("luarocks_src/...")` resolves. + const wf = b.addWriteFiles(); + const out_zig = wf.addCopyFile(gen_file, "embedded_luarocks.zig"); + _ = wf.addCopyDirectory(luarocks_src.path("src"), "luarocks_src", .{}); + return out_zig; +} + +/// Codegen step: emit a Zig module that exposes every Lua public header +/// from the Lua source tarball as embedded bytes. The bootstrap stages +/// these under `$PANTO_HOME/rocks/lua-X.Y.Z/include/` so luarocks can +/// compile C rocks against them. +fn generateLuaHeadersEmbed( + b: *std.Build, + lua_src: *std.Build.Dependency, +) std.Build.LazyPath { + const tool = b.addExecutable(.{ + .name = "gen-lua-headers-embed", + .root_module = b.createModule(.{ + .root_source_file = b.path("build/gen_lua_headers_embed.zig"), + .target = b.graph.host, + .optimize = .Debug, + }), + }); + + const run = b.addRunArtifact(tool); + // Marker subpath: each emitted `@embedFile` reference is + // `lua_src/
`, and we stage the headers under that name. + run.addArg("lua_src"); + run.addDirectoryArg(lua_src.path("src")); + const gen_file = run.addOutputFileArg("embedded_lua_headers.zig"); + + const wf = b.addWriteFiles(); + const out_zig = wf.addCopyFile(gen_file, "embedded_lua_headers.zig"); + _ = wf.addCopyDirectory(lua_src.path("src"), "lua_src", .{}); + return out_zig; } -// Lua 5.4.7 core VM + standard library. Excludes lua.c (interpreter entry +// Lua 5.4.x core VM + standard library. Excludes lua.c (interpreter entry // point) and luac.c (compiler entry point). const lua_files = [_][]const u8{ // Core VM -- cgit v1.3