First Commit

This commit is contained in:
2026-08-06 12:54:48 -04:00
commit dd01ccb258
15 changed files with 1780 additions and 0 deletions
+167
View File
@@ -0,0 +1,167 @@
-- packager.lua -- tarball creation, sha256 computation, and package.lua generation.
--
-- Converts a staged install tree (DESTDIR layout) into a Zeta-compatible
-- binary tarball with archive strip=1 format. Computes the sha256 checksum
-- of the output tarball and writes the package.lua manifest.
local packager = {}
local path = require("path")
local log = require("log")
---------------------------------------------------------------------------
-- Private helpers (must be defined before the public API that uses them)
---------------------------------------------------------------------------
local function escape(s)
return s:gsub('"', '\\"'):gsub("\n", "\\n")
end
local function escape_lua_string(s)
return s:gsub("\\", "\\\\"):gsub("'", "\\'")
end
local function human_size(filepath)
local f = io.open(filepath, "rb")
if not f then return "? B" end
local size = f:seek("end")
f:close()
if size >= 1024 * 1024 then
return string.format("%.1f MiB", size / (1024 * 1024))
elseif size >= 1024 then
return string.format("%.0f KiB", size / 1024)
end
return string.format("%d B", size)
end
local function sha256_file(filepath)
local f = io.popen("sha256sum " .. path.quote(filepath) .. " 2>/dev/null")
if f then
local line = f:read("*l")
f:close()
local hex = line and line:match("^(%x+)")
if hex then return hex end
end
f = io.popen("shasum -a 256 " .. path.quote(filepath) .. " 2>/dev/null")
if f then
local line = f:read("*l")
f:close()
local hex = line and line:match("^(%x+)")
if hex then return hex end
end
f = io.popen("openssl dgst -sha256 " .. path.quote(filepath) .. " 2>/dev/null")
if f then
local line = f:read("*l")
f:close()
local hex = line and line:match("=(%x+)")
if hex then return hex end
end
error("sha256: no supported tool found (install sha256sum, shasum, or openssl)", 0)
end
local function write_atomic(filepath, content)
local tmp = filepath .. ".tmp"
local f, err = io.open(tmp, "wb")
if not f then return nil, err end
f:write(content)
f:close()
return os.rename(tmp, filepath)
end
---------------------------------------------------------------------------
-- Public API
---------------------------------------------------------------------------
-- Tar up the staging directory, strip=1 format.
-- Files in the tarball are rooted at "./" so Zeta's strip=1 produces clean
-- relative paths (usr/bin/foo, not ./usr/bin/foo).
function packager.create_tarball(stage_dir, dest_path)
log.step("packaging " .. path.basename(dest_path))
path.mkdir_p(path.dirname(dest_path))
local cmd = "tar -czf " .. path.quote(dest_path)
.. " -C " .. path.quote(stage_dir) .. " ."
if not path.run(cmd) then
error(("failed to create tarball %s"):format(dest_path), 0)
end
local sha256 = sha256_file(dest_path)
log.detail(("sha256: %s"):format(sha256))
log.ok(("packaged %s (%s)"):format(path.basename(dest_path), human_size(dest_path)))
return sha256
end
-- Generate a Zeta-compatible package.lua manifest and write it to pkg_dir.
-- The generated manifest uses archive mode (strip=1) and includes the
-- computed sha256 of the binary tarball. Optional fields (deps, files,
-- test) are included when present in the recipe.
function packager.write_manifest(recipe, tarball_sha256, pkg_dir)
local lines = {}
local function emit(fmt, ...)
lines[#lines + 1] = string.format(fmt, ...)
end
emit("return {")
emit(' name = "%s",', recipe.name)
emit(' version = "%s",', recipe.version)
emit(' summary = "%s",', escape(recipe.summary))
emit(' url = "https://github.com/gretagen/zeta-packages/packages/%s/%s-%s.tar.gz",',
recipe.name, recipe.name, recipe.version)
emit(' sha256 = "%s",', tarball_sha256)
if #recipe.deps > 0 then
local dep_strs = {}
for _, d in ipairs(recipe.deps) do
dep_strs[#dep_strs + 1] = string.format('"%s"', escape(d))
end
emit(' deps = { %s },', table.concat(dep_strs, ", "))
else
emit(' deps = {},')
end
emit(' archive = { strip = 1 },')
if recipe.files then
local file_strs = {}
for _, f in ipairs(recipe.files) do
file_strs[#file_strs + 1] = string.format('"%s"', escape(f))
end
emit(' files = { %s },', table.concat(file_strs, ", "))
end
if recipe.test then
-- Generate a test function that substitutes ${DESTDIR} -> p.install_root.
-- Splits the test command around each ${DESTDIR} and uses Lua's %q format
-- so every string segment is properly quoted.
emit(" test = function(p)")
local parts = {}
local rem = recipe.test
while true do
local si, ei = rem:find("${DESTDIR}", 1, true)
if not si then
if rem ~= "" then parts[#parts + 1] = string.format("%q", rem) end
break
end
if si > 1 then
parts[#parts + 1] = string.format("%q", rem:sub(1, si - 1))
end
parts[#parts + 1] = "p.install_root"
rem = rem:sub(ei + 1)
end
emit(" p:run(%s)", table.concat(parts, " .. "))
emit(" end,")
end
emit("}")
local content = table.concat(lines, "\n") .. "\n"
local dest = path.join(pkg_dir, "package.lua")
write_atomic(dest, content)
log.ok("wrote package.lua")
return dest
end
return packager