#!/usr/bin/env lua -- zeta-makepkg — build Zeta binary packages from declarative .recipe files. -- -- Usage: -- zeta-makepkg [flags] Build a package from a recipe -- zeta-makepkg --index [flags] Regenerate packages/index.lua -- -- Flags: -- --output Output repository root (default: .) -- --no-index Skip index.lua update -- --keep-work Preserve build work directory -- --force Overwrite existing package -- --help Show this help -- -- Pipeline: -- LOAD recipe → FETCH source → EXTRACT → BUILD → TEST → PACKAGE → INDEX -- Lua 5.1 compat: arg[0] is the script path, not the interpreter. local here if arg and arg[0] and arg[0]:match("[/\\]") then here = arg[0]:match("^(.*)[/\\][^/\\]*$") elseif arg and arg[-1] then -- Lua 5.1 sends script path as arg[-1] when using env. here = arg[-1]:match("^(.*)[/\\][^/\\]*$") else here = "." end -- Resolve the real directory in case we were invoked via a symlink. if here then local f = io.popen("cd " .. here:gsub("'", "'\\''") .. " 2>/dev/null && pwd") if f then local real = f:read("*l") f:close() if real and real ~= "" then here = real end end else here = "." end -- Library path: works both from the repo tree (../lib/) and when installed -- via 'make install' (/usr/local/lib/zeta-toolchain/). package.path = here .. "/../lib/?.lua;" .. here .. "/../lib/zeta-toolchain/?.lua;" .. package.path local recipe = require("recipe") local builder = require("builder") local packager = require("packager") local indexer = require("indexer") local path = require("path") local log = require("log") --------------------------------------------------------------------------- -- Help --------------------------------------------------------------------------- local HELP = [[ zeta-makepkg — build Zeta binary packages from declarative .recipe files. Usage: zeta-makepkg [flags] zeta-makepkg --index [flags] Commands: Build a package from a .recipe file --index Regenerate packages/index.lua from existing package.lua files Flags: --output Output repository root (default: .) Creates packages// with tarball + package.lua --repo Base URL for generated package.lua manifests (default: https://raw.githubusercontent.com/gretagen/...) -j, --jobs Number of parallel build jobs (default: nproc) --no-index Don't update packages/index.lua after building --keep-work Don't remove the build work directory --force Overwrite existing package tarball --help Show this help Cache: Downloaded sources are cached in ~/.cache/zeta-makepkg/sources/ Build work is done in /tmp/zeta-makepkg/-/ Examples: zeta-makepkg mypkg.recipe zeta-makepkg mypkg.recipe --output ~/zeta-packages --force zeta-makepkg --index --output ~/zeta-packages ]] --------------------------------------------------------------------------- -- CLI parsing --------------------------------------------------------------------------- local function parse_cli(args) local opts = { output = ".", no_index = false, keep_work = false, force = false, index = false, help = false, recipe = nil, jobs = nil, -- nil = auto-detect (nproc) repo = "https://raw.githubusercontent.com/gretagen/zeta-packages/refs/heads/main", } local i = 1 while i <= #args do local a = args[i] if a == "--help" or a == "-h" then opts.help = true elseif a == "--index" then opts.index = true elseif a == "--output" then i = i + 1 if i > #args then io.stderr:write("error: --output requires a directory argument\n") return nil end opts.output = args[i] elseif a == "--repo" then i = i + 1 if i > #args then io.stderr:write("error: --repo requires a URL argument\n") return nil end opts.repo = args[i] elseif a == "--no-index" then opts.no_index = true elseif a == "--keep-work" then opts.keep_work = true elseif a == "--force" then opts.force = true elseif a == "--jobs" then i = i + 1 if i > #args then io.stderr:write("error: --jobs requires a number argument\n") return nil end local n = tonumber(args[i]) if not n or n < 1 then io.stderr:write(("error: invalid jobs value %q\n"):format(args[i])) return nil end opts.jobs = n elseif a:match("^%-j%d+$") then local n = tonumber(a:match("^%-j(%d+)$")) opts.jobs = n elseif a == "-j" then i = i + 1 if i > #args then io.stderr:write("error: -j requires a number argument\n") return nil end local n = tonumber(args[i]) if not n or n < 1 then io.stderr:write(("error: invalid jobs value %q\n"):format(args[i])) return nil end opts.jobs = n elseif a:match("^%-") then io.stderr:write(("error: unknown flag %s\n"):format(a)) return nil elseif not opts.recipe and not opts.index then opts.recipe = a else io.stderr:write(("error: unexpected argument %s\n"):format(a)) return nil end i = i + 1 end return opts end --------------------------------------------------------------------------- -- Build pipeline --------------------------------------------------------------------------- local function run_build(recipe_path, opts) -- 1. Load recipe local r, err = recipe.load(recipe_path) if not r then log.fatal(("recipe error: %s"):format(tostring(err))) end -- 2. Set up output directory local packages_dir = path.join(opts.output, "packages") local pkg_dir = path.join(packages_dir, r.name) if path.exists(pkg_dir) and not opts.force then log.fatal(("package %s already exists at %s (use --force to overwrite)"):format( r.name, pkg_dir)) end path.mkdir_p(pkg_dir) -- 3. Work directories local stamp = os.date("%Y%m%d-%H%M%S") local work_dir = path.join("/tmp/zeta-makepkg", r.name .. "-" .. stamp) local stage_dir = path.join(work_dir, "stage") path.mkdir_p(stage_dir) log.info(("work directory: %s"):format(work_dir)) -- 4. Cache directory local cache_dir = path.join(os.getenv("HOME") or "/tmp", ".cache", "zeta-makepkg", "sources") -- 5. Fetch source local cache_file = builder.fetch_source(r, cache_dir) -- 6. Extract source local source_dir = builder.extract_source(r, cache_file, work_dir) -- 7. Build builder.run_build(r, source_dir, stage_dir, { jobs = opts.jobs }) -- 8. Test (optional) if r.test then builder.run_test(r.test, stage_dir) else log.info("no test command — skipping verification") end -- 9. Package local tarball_path = path.join(pkg_dir, r.name .. "-" .. r.version .. ".tar.gz") local tarball_sha256 = packager.create_tarball(stage_dir, tarball_path) -- 10. Write package.lua manifest packager.write_manifest(r, tarball_sha256, pkg_dir, opts.repo) -- 11. Update index if not opts.no_index then indexer.generate(packages_dir) else log.info("--no-index: skipping index update") end -- 12. Cleanup if not opts.keep_work then path.run("rm -rf " .. path.quote(work_dir)) else log.info(("--keep-work: work directory preserved at %s"):format(work_dir)) end log.ok(("%s-%s built successfully"):format(r.name, r.version)) log.info(("output: %s"):format(pkg_dir)) end local function run_index(opts) local packages_dir = path.join(opts.output, "packages") indexer.generate(packages_dir) end --------------------------------------------------------------------------- -- Main --------------------------------------------------------------------------- local args = {} for i = 1, #arg do args[i] = arg[i] end local opts = parse_cli(args) if not opts then io.stderr:write("Run 'zeta-makepkg --help' for usage.\n") os.exit(1) end if opts.help then io.write(HELP) os.exit(0) end if opts.index then run_index(opts) elseif opts.recipe then local ok, err = pcall(run_build, opts.recipe, opts) if not ok then log.fatal(tostring(err)) end else io.stderr:write("error: no recipe specified\n") io.stderr:write("Run 'zeta-makepkg --help' for usage.\n") os.exit(1) end