//! 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, }); } }