//! Build-time codegen: walk the in-repo `agent/` tree and emit a Zig //! module that exposes every file as an `@embedFile` entry. //! //! Invoked from `build.zig`: //! //! gen-agent-embed //! //! `` is the repo's `agent/` directory. `` is //! the subpath the generated zig file's neighboring `addCopyDirectory` //! stages the tree under; every `@embedFile` reference uses this //! prefix so Zig can resolve it relative to the generated module's //! directory. //! //! The generated file looks like: //! //! pub const Entry = struct { //! /// Path relative to the agent tree root (e.g. "tools/read.lua"). //! path: []const u8, //! /// File contents. //! contents: []const u8, //! }; //! pub const files: []const Entry = &.{ //! .{ .path = "tools/read.lua", .contents = @embedFile("...") }, //! ... //! }; //! //! Bootstrap walks `files` and stages each entry at //! `$PANTO_HOME/agent/` using the same `writeIfDifferent` //! discipline as `stageLuaHeaders`. 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; var entries: std.array_list.Managed(Entry) = .init(arena); try collect(arena, io, src_dir_arg, embed_prefix, &entries); // Sort for deterministic output across builds. std.mem.sort(Entry, entries.items, {}, Entry.lessThan); 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_agent_embed.zig. \\ \\pub const Entry = struct { \\ /// Path relative to the agent tree root (e.g. "tools/read.lua"). \\ path: []const u8, \\ /// File contents (raw bytes). \\ contents: []const u8, \\}; \\ \\pub const files: []const Entry = &.{ \\ ); for (entries.items) |e| { try w.print( " .{{ .path = \"{s}\", .contents = @embedFile(\"{s}\") }},\n", .{ e.rel_path, e.embed_path }, ); } try w.writeAll("};\n"); try w.flush(); } const Entry = struct { /// Path inside the agent tree, forward-slash separated. rel_path: []const u8, /// Path the generated `@embedFile` references (under embed prefix). embed_path: []const u8, fn lessThan(_: void, a: Entry, b: Entry) bool { return std.mem.lessThan(u8, a.rel_path, b.rel_path); } }; /// Walk `` recursively, collecting every file. Dotfiles /// (entries whose basename starts with `.`) are skipped at every /// level so we don't accidentally embed editor backups, `.DS_Store`, /// or VCS metadata. fn collect( arena: std.mem.Allocator, io: std.Io, root_abs: []const u8, embed_prefix: []const u8, out: *std.array_list.Managed(Entry), ) !void { var dir = std.Io.Dir.cwd().openDir(io, root_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 (entry.basename.len == 0 or entry.basename[0] == '.') continue; // Skip anything along the path that looks like a dotfile dir. if (std.mem.indexOf(u8, entry.path, "/.") != null) continue; const rel_path = try arena.dupe(u8, entry.path); const embed_path = try std.fs.path.join(arena, &.{ embed_prefix, entry.path }); try out.append(.{ .rel_path = rel_path, .embed_path = embed_path, }); } }