#!/usr/local/bin/luajit package.path = "./?.lua;/usr/share/luajit-2.1/?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/share/lua/5.1/?.lua;/usr/share/lua/5.1/?/init.lua" local function read_file(path) local f, err = io.open(path, "r") if not f then return nil, err end local content = f:read("*a") f:close() return content end local function write_file(path, content) local f, err = io.open(path, "w") if not f then return nil, err end f:write(content) f:close() return true end local function file_exists(path) local f = io.open(path, "r") if f then f:close() return true end return false end local function run(cmd, msg) print("==========================================") print(msg) print("==========================================") local ok, _, status = os.execute(cmd) if not ok then print("FAILED with status: " .. tostring(status)) os.exit(-1) end end local function mkdir(path) os.execute("mkdir -p " .. path) end local function find_lua_files(dir) local results = {} local p = io.popen('find "' .. dir .. '" -name "*.lua" -type f') for line in p:lines() do results[line:sub(#dir + 2)] = true end p:close() return results end local function lua_file_to_c_const(name, filepath) local content = read_file(filepath) if not content then print("Can't read Lua entrypoint file: " .. filepath) os.exit(-1) end content = content:gsub("\\", "\\\\") content = content:gsub('"', '\\"') content = content:gsub("\n", "\\n") return "static const char " .. name .. '[] = "' .. content .. '";\n' end -- Map a source-relative path to a dotted module name. MUST stay identical to -- the LILPACK packer's derivation (src/shell/shell/builtins/lilpack.lua: -- path_to_module_name) so a pack built into a binary and the same pack packed -- into a .lilpack expose the same module names. local function path_to_module_name(rel_path) return rel_path:gsub("%.lua$", ""):gsub("/", "."):gsub("%.init$", "") end local function get_root() local p = io.popen("pwd") local cwd = p:read("*l") p:close() return cwd:gsub("/buildgen$", "") end local root = get_root() local entrypoints_dir = root .. "/buildgen/entrypoints" local app_config_file = arg[1] or "" if not file_exists(app_config_file) then print("Can't read app config file") os.exit(-1) end -- Optional external-pack root. When set (arg[2]), the pack's own Lua modules -- are scanned from this dir and embedded with names derived from the file -- tree -- no modinfo entry required. The descriptor (arg[1]) and its -- start_code/custom_main entrypoints live under /build. local pack_dir = arg[2] local entry_base = pack_dir and (pack_dir .. "/build") or entrypoints_dir local app_config = dofile(app_config_file) local modinfo = dofile("modinfo.lua") local tmpl = read_file("c_tmpl") local ver = read_file("apps/version") or "unknown" ver = ver:gsub("\n", "") -- The `-v` string differs for pack binaries: they report their OWN version -- (from the pack's lilpack.json) plus the lilush toolchain they were built -- against, e.g. "recall 0.3.1 (lilush 0.8.4)". In-tree builds keep the plain -- "version ". The build env (Alpine + bare LuaJIT) has no JSON parser, so -- the pack version is pulled from lilpack.json with a pattern match. local read_pack_version = function(dir) local content = read_file(dir .. "/lilpack.json") if not content then return nil end return content:match('"version"%s*:%s*"([^"]*)"') end local version_string if pack_dir then local pack_version = read_pack_version(pack_dir) or "0.0.0" version_string = app_config.binary .. " " .. pack_version .. " (lilush " .. ver .. ")" else version_string = "version " .. ver end -- Build start_code C constants from Lua files local start_code_c = "" for const_name, lua_path in pairs(app_config.start_code) do start_code_c = start_code_c .. lua_file_to_c_const(const_name, entry_base .. "/" .. lua_path) end local tmpl_vars = { VERSION_STRING = version_string, LILUSH_VERSION = ver, APP_NAME = app_config.binary, START_CODE = start_code_c, LUAMODS = "", CLIBS = "", } local ar_args = "" for _, lib in ipairs(app_config.c_libs) do local lib_dir = root .. "/src/" .. lib run("make -C " .. lib_dir, "Building module: " .. lib .. "...") run( "sh -c 'strip --strip-debug --strip-unneeded " .. lib_dir .. "/*.o'", "Stripping object files: " .. lib .. "..." ) local p = io.popen('find "' .. lib_dir .. '" -maxdepth 1 -name "*.o" -type f') for line in p:lines() do ar_args = ar_args .. " " .. line end p:close() end mkdir(root .. "/build") local mod_to_header = function(filepath, rel_path, out_dir) local mod_base = path_to_module_name(rel_path) local mod_name = "mod_lua_" .. mod_base .. ".h" local c_ident = mod_base:gsub("%.", "_") local out_path = out_dir .. "/" .. mod_name os.execute("luajit -b -n " .. c_ident .. " " .. filepath .. " " .. out_path) local content = read_file(out_path) content = content:gsub("#define luaJIT_BC_" .. c_ident, "const size_t mod_lua_" .. c_ident) content = content:gsub("_SIZE (%d+)", "_SIZE=%1;") content = content:gsub("static const unsigned char luaJIT_BC_" .. c_ident, "static const char mod_lua_" .. c_ident) write_file(out_path, content) end -- Collect every embedded Lua module as a record so the #include list and the -- lua_preload[] table are driven from one source of truth (the C symbol is -- always header:gsub("%.", "_")). Shared groups come from modinfo; an optional -- external pack contributes modules derived straight from its file tree. local mod_records = {} local seen_mod = {} local add_mod = function(mod_name, header_base, build_dir) if seen_mod[mod_name] then print("FATAL: duplicate Lua module name '" .. mod_name .. "'") os.exit(-1) end seen_mod[mod_name] = true mod_records[#mod_records + 1] = { name = mod_name, header = header_base, dir = build_dir } end -- Shared luamod groups (src/ + modinfo registry), as before. for _, luamod in ipairs(app_config.luamods) do local build_dir = root .. "/build/" .. luamod mkdir(build_dir) local lua_files = find_lua_files(root .. "/src/" .. luamod) for lua_file_rel_path, _ in pairs(lua_files) do mod_to_header(root .. "/src/" .. luamod .. "/" .. lua_file_rel_path, lua_file_rel_path, build_dir) end for _, entry in ipairs(modinfo.luamods[luamod]) do if type(entry) == "table" then add_mod(entry[1], entry[2], build_dir) else add_mod(entry, "mod_lua_" .. entry, build_dir) end end end -- Optional external pack: scan its module tree and derive names from the file -- paths exactly like the packer, skipping the top-level cli/, build/ and tests/ dirs. -- Sorted for deterministic, reproducible C output. if pack_dir then local build_dir = root .. "/build/" .. app_config.binary mkdir(build_dir) local pack_mods = {} for rel_path, _ in pairs(find_lua_files(pack_dir)) do if not (rel_path:match("^cli/") or rel_path:match("^build/") or rel_path:match("^tests/")) then pack_mods[#pack_mods + 1] = rel_path end end table.sort(pack_mods) for _, rel_path in ipairs(pack_mods) do mod_to_header(pack_dir .. "/" .. rel_path, rel_path, build_dir) local mod_name = path_to_module_name(rel_path) add_mod(mod_name, "mod_lua_" .. mod_name, build_dir) end end -- Emit #includes, then the preload table, from the single record list. for _, rec in ipairs(mod_records) do tmpl_vars.LUAMODS = tmpl_vars.LUAMODS .. '#include "' .. rec.dir .. "/" .. rec.header .. '.h"\n' end tmpl_vars.LUAMODS = tmpl_vars.LUAMODS .. "\n\nconst mod_lua__t lua_preload[] = {\n" for _, rec in ipairs(mod_records) do local sym = rec.header:gsub("%.", "_") tmpl_vars.LUAMODS = tmpl_vars.LUAMODS .. '{"' .. rec.name .. '", ' .. sym .. ", &" .. sym .. "_SIZE },\n" end tmpl_vars.LUAMODS = tmpl_vars.LUAMODS .. "{NULL, NULL, NULL } };\n" for _, lib in ipairs(app_config.c_libs) do for _, entry in ipairs(modinfo.c_libs[lib]) do tmpl_vars.CLIBS = tmpl_vars.CLIBS .. "extern int " .. entry[2] .. "(lua_State *L);\n" end end tmpl_vars.CLIBS = tmpl_vars.CLIBS .. "const luaL_Reg c_preload[] = {\n" for _, lib in ipairs(app_config.c_libs) do for _, entry in ipairs(modinfo.c_libs[lib]) do tmpl_vars.CLIBS = tmpl_vars.CLIBS .. '{"' .. entry[1] .. '", ' .. entry[2] .. "},\n" end end tmpl_vars.CLIBS = tmpl_vars.CLIBS .. "{NULL, NULL }\n};\n" -- Append custom_main or default_main if app_config.custom_main then tmpl = tmpl .. read_file(entry_base .. "/" .. app_config.custom_main) else tmpl = tmpl .. read_file(root .. "/buildgen/default_main.c") end tmpl = tmpl:gsub("{{([%w_]+)}}", tmpl_vars) write_file(root .. "/src/" .. app_config.binary .. ".c", tmpl) local ar_cmd = "ar rcs " .. root .. "/src/liblilush.a" .. ar_args run(ar_cmd, "Building static lib...") mkdir("/build") local clang_cmd = "clang -Os -s -O3 -Wall -Wl,-E -o " .. "/build/" .. app_config.binary .. " " .. root .. "/src/" .. app_config.binary .. ".c -I/usr/local/include/luajit-2.1 -L/usr/local/lib -lluajit-5.1 -Wl,--whole-archive " .. root .. "/src/liblilush.a -Wl,--no-whole-archive -static" run(clang_cmd, "Building the app...")