Files
zeta-toolchain/toolchain/builder/zeta-makepkg
T
2026-08-06 12:54:48 -04:00

274 lines
7.8 KiB
Plaintext
Executable File

#!/usr/bin/env lua
-- zeta-makepkg — build Zeta binary packages from declarative .recipe files.
--
-- Usage:
-- zeta-makepkg <recipe> [flags] Build a package from a recipe
-- zeta-makepkg --index [flags] Regenerate packages/index.lua
--
-- Flags:
-- --output <dir> 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 <recipe> [flags]
zeta-makepkg --index [flags]
Commands:
<recipe> Build a package from a .recipe file
--index Regenerate packages/index.lua from existing package.lua files
Flags:
--output <dir> Output repository root (default: .)
Creates packages/<name>/ with tarball + package.lua
-j, --jobs <N> 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/<name>-<timestamp>/
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)
}
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 == "--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)
-- 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