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(.{}); const panto_lib_dep = b.dependency("panto", .{ .target = target, .optimize = optimize, }); // The native Lua C-module (`libpanto-lua`). We embed its compiled // `panto.so` into the panto binary and stage it onto the embedded // VM's `package.cpath` at bootstrap, so `require('panto')` resolves // to the native agent/stream module on a cold, network-less machine // (the guaranteed initial module load — see // docs/step19-cli-panto-module-plan.md). const panto_lua_dep = b.dependency("panto_lua", .{ .target = target, .optimize = optimize, }); const panto_so_embed_path = generatePantoSoEmbed(b, panto_lua_dep); // TOML parser (used by the CLI for ~/.config/panto/models.toml). // We disable the upstream's optional thread-pool dep — we only need // sequential parsing of a small config file. const toml_dep = b.dependency("toml", .{ .target = target, .optimize = optimize, .@"thread-pool" = false, }); // Markdown parser for TUI rendering. MD4C is a small, permissively // licensed C CommonMark parser; Zig owns only terminal rendering. const md4c_dep = b.dependency("md4c", .{}); // Fetch upstream Lua source (used both for our static library and // staged at runtime as the `include/` headers under the data home). // Reproducibility comes from the content-addressed hash in // build.zig.zon. const lua_src = b.dependency("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 /rocks/lua-X.Y.Z/include/ // for luarocks to find when compiling C rocks. const lua_headers_embed_path = generateLuaHeadersEmbed(b, lua_src); // And the in-repo `agent/` tree: bundled into the binary and // staged at /agent/ on first run. This is panto's // "base" extension/tool layer (read/write/edit/bash etc.). const agent_embed_path = generateAgentEmbed(b); // Constants module: makes Zig know the Lua and luarocks versions // without duplicating literals everywhere. const versions_mod = b.addOptions(); versions_mod.addOption([]const u8, "panto_version", @import("build.zig.zon").version); 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); // Absolute repo root, for tests that need to locate source-tree // assets (e.g. `agent/tools/*.lua`) regardless of the test // runner's cwd. versions_mod.addOption([]const u8, "repo_root", b.build_root.path orelse "."); // CLI executable const exe_mod = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, .link_libc = true, }); exe_mod.addImport("panto", panto_lib_dep.module("panto")); exe_mod.addImport("toml", toml_dep.module("toml")); 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, })); exe_mod.addImport("embedded_agent", b.createModule(.{ .root_source_file = agent_embed_path, })); exe_mod.addImport("embedded_panto_so", b.createModule(.{ .root_source_file = panto_so_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")); addMd4c(exe_mod, md4c_dep); // 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); const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); if (b.args) |args| { run_cmd.addArgs(args); } const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); // CLI unit tests const test_mod = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, .link_libc = true, }); test_mod.addImport("panto", panto_lib_dep.module("panto")); test_mod.addImport("toml", toml_dep.module("toml")); 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, })); test_mod.addImport("embedded_agent", b.createModule(.{ .root_source_file = agent_embed_path, })); test_mod.addImport("embedded_panto_so", b.createModule(.{ .root_source_file = panto_so_embed_path, })); addLuaSources(test_mod, lua_src, lua_anchor_path); test_mod.addIncludePath(lua_src.path("src")); addMd4c(test_mod, md4c_dep); test_mod.linkLibrary(lua_repl); const unit_tests = b.addTest(.{ .name = "panto-tests", .root_module = test_mod, }); const run_unit_tests = b.addRunArtifact(unit_tests); const test_step = b.step("test", "Run unit tests"); test_step.dependOn(&run_unit_tests.step); // Also run libpanto's own tests as part of `zig build test` const lib_test_step = panto_lib_dep.builder.top_level_steps.get("test").?; test_step.dependOn(&lib_test_step.step); } fn addMd4c(mod: *std.Build.Module, md4c_dep: *std.Build.Dependency) void { const cflags = [_][]const u8{ "-std=c99", "-Wall", "-Wextra", "-Wno-unused-parameter", }; mod.addCSourceFile(.{ .file = md4c_dep.path("src/md4c.c"), .flags = &cflags, }); mod.addIncludePath(md4c_dep.path("src")); } /// 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 mod = b.createModule(.{ .target = target, .optimize = optimize, .link_libc = true, }); const cflags = [_][]const u8{ "-std=gnu99", "-Wall", "-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", }; 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 => mod.addCMacro("LUA_USE_MACOSX", ""), .linux => mod.addCMacro("LUA_USE_LINUX", ""), .freebsd, .netbsd, .openbsd => mod.addCMacro("LUA_USE_POSIX", ""), else => {}, } } /// 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: walk the in-repo `agent/` tree and emit a Zig module /// embedding every file. Bootstrap stages the result under /// `/agent/` on first run, where the runtime's extension /// loader finds it as the "base" layer (below user and project). /// /// Unlike `generateLuarocksEmbed`, the agent tree lives in-repo and /// its contents change as we add/edit tools. `addDirectoryArg` on a /// local source dir does NOT hash directory contents into the cache /// key (only the path string), so editing a file under `agent/` /// wouldn't invalidate this step's cache. To make cache invalidation /// correct, we enumerate the tree at *build-script execution time* /// and call `addFileInput` for every file we find. Each file then /// participates in the cache key. /// /// `addCopyDirectory` (below) still does the actual staging of the /// tree alongside the generated zig file so `@embedFile("agent_src/...")` /// references resolve. fn generateAgentEmbed(b: *std.Build) std.Build.LazyPath { const tool = b.addExecutable(.{ .name = "gen-agent-embed", .root_module = b.createModule(.{ .root_source_file = b.path("build/gen_agent_embed.zig"), .target = b.graph.host, .optimize = .Debug, }), }); const run = b.addRunArtifact(tool); // Marker subpath: each emitted `@embedFile` reference is // `agent_src/`, and we stage the tree under that name. run.addArg("agent_src"); run.addDirectoryArg(b.path("agent")); const gen_file = run.addOutputFileArg("embedded_agent.zig"); // Enumerate every file under `agent/` and register it as an // explicit cache input. This is what makes `zig build` notice // edits to e.g. `agent/tools/read.lua` without a manual // `rm -rf .zig-cache`. addAgentTreeFileInputs(b, run, "agent") catch |err| { std.debug.panic("failed to enumerate agent/ tree: {t}", .{err}); }; const wf = b.addWriteFiles(); const out_zig = wf.addCopyFile(gen_file, "embedded_agent.zig"); _ = wf.addCopyDirectory(b.path("agent"), "agent_src", .{}); return out_zig; } /// Recursively walk `subpath` under the build root and call /// `addFileInput` on every regular file. Used to give the agent-embed /// codegen accurate cache invalidation. Dotfiles (any path segment /// starting with `.`) are skipped to match the codegen's filter. fn addAgentTreeFileInputs( b: *std.Build, run: *std.Build.Step.Run, subpath: []const u8, ) !void { const io = b.graph.io; var dir = b.build_root.handle.openDir(io, subpath, .{ .iterate = true }) catch |err| switch (err) { error.FileNotFound => return, else => return err, }; defer dir.close(io); var walker = try dir.walk(b.allocator); defer walker.deinit(); while (try walker.next(io)) |entry| { if (entry.kind != .file) continue; if (entry.basename.len == 0 or entry.basename[0] == '.') continue; if (std.mem.indexOf(u8, entry.path, "/.") != null) continue; const rel = b.pathJoin(&.{ subpath, entry.path }); run.addFileInput(b.path(rel)); } } /// 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 `/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; } /// Codegen step: embed the compiled `libpanto-lua` `panto.so` into a Zig /// module the bootstrap stages onto the embedded VM's `package.cpath`. /// /// We address the artifact via `dep.artifact("panto")` (the `addLibrary` /// Compile step named `panto` in `libpanto-lua/build.zig`) and embed its /// emitted binary by bytes — name-agnostic, so the upstream `libpanto.so`/ /// `libpanto.dylib` filename is irrelevant; we always stage it as /// `panto.so`. The `.so` is copied next to the generated module so /// `@embedFile` can resolve it. fn generatePantoSoEmbed( b: *std.Build, panto_lua_dep: *std.Build.Dependency, ) std.Build.LazyPath { const so_path = panto_lua_dep.artifact("panto").getEmittedBin(); const tool = b.addExecutable(.{ .name = "gen-panto-so-embed", .root_module = b.createModule(.{ .root_source_file = b.path("build/gen_panto_so_embed.zig"), .target = b.graph.host, .optimize = .Debug, }), }); const run = b.addRunArtifact(tool); // The generated module `@embedFile`s exactly this name. run.addArg("panto.so"); const gen_file = run.addOutputFileArg("embedded_panto_so.zig"); const wf = b.addWriteFiles(); const out_zig = wf.addCopyFile(gen_file, "embedded_panto_so.zig"); _ = wf.addCopyFile(so_path, "panto.so"); return out_zig; } // 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 "lapi.c", "lcode.c", "lctype.c", "ldebug.c", "ldo.c", "ldump.c", "lfunc.c", "lgc.c", "llex.c", "lmem.c", "lobject.c", "lopcodes.c", "lparser.c", "lstate.c", "lstring.c", "ltable.c", "ltm.c", "lundump.c", "lvm.c", "lzio.c", // Auxiliary + standard library "lauxlib.c", "lbaselib.c", "lcorolib.c", "ldblib.c", "liolib.c", "lmathlib.c", "loadlib.c", "loslib.c", "lstrlib.c", "ltablib.c", "lutf8lib.c", "linit.c", };