1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
|
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 <data home>/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 <data home>/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
/// `<data home>/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/<rel>`, 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 `<data 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/<header>`, 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",
};
|