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/gen_luarocks_embed.zig | 157 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 build/gen_luarocks_embed.zig (limited to 'build/gen_luarocks_embed.zig') 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 +//! +//! `` is `luarocks_src/src` (contains `luarocks/`, `compat53/`, +//! and `bin/`). `` 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 `/` recursively, collecting every `.lua` file. The +/// `require` name is computed as the path relative to `` 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 (`/...`) + // 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, + }); + } +} -- cgit v1.3