patch: hooks from zeta
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
#!/bin/sh
|
||||
# build.sh — example custom build script for zeta-makepkg.
|
||||
#
|
||||
# Invoked from the extracted source directory. The tool sets:
|
||||
# $DESTDIR → staging tree root (e.g. /tmp/zeta-makepkg/hooks-demo-.../stage)
|
||||
#
|
||||
# This script installs a small command under usr/bin/ and a post-transaction
|
||||
# hook under usr/share/zeta/hooks/. A hook is a pure-data Lua file:
|
||||
#
|
||||
# return {
|
||||
# order = 50,
|
||||
# trigger = { op = "install", type = "package", target = { "hooks-demo" } },
|
||||
# action = { when = "post", exec = "true", description = "example hook" },
|
||||
# }
|
||||
#
|
||||
# zeta-makepkg scans the stage for usr/share/zeta/hooks/*.hook, validates the
|
||||
# format, and merges the paths into the generated `files` whitelist.
|
||||
|
||||
set -eu
|
||||
|
||||
echo "==> Building hooks-demo (custom)"
|
||||
|
||||
# Install a tiny command
|
||||
mkdir -p "$DESTDIR/usr/bin"
|
||||
cat > "$DESTDIR/usr/bin/hooks-demo" <<'EOF'
|
||||
#!/bin/sh
|
||||
echo "hooks-demo: post-transaction hooks are working"
|
||||
EOF
|
||||
chmod 755 "$DESTDIR/usr/bin/hooks-demo"
|
||||
|
||||
# Install a post-transaction hook (pure data, no side effects)
|
||||
mkdir -p "$DESTDIR/usr/share/zeta/hooks"
|
||||
cat > "$DESTDIR/usr/share/zeta/hooks/hooks-demo.hook" <<'EOF'
|
||||
return {
|
||||
order = 50,
|
||||
trigger = { op = "install", type = "package", target = { "hooks-demo" } },
|
||||
action = { when = "post", exec = "true", description = "example hook" },
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "==> Install complete"
|
||||
@@ -0,0 +1,26 @@
|
||||
-- hooks-demo.recipe — demonstrates shipping a post-transaction hook.
|
||||
--
|
||||
-- The custom build script installs a small command and a hook file under
|
||||
-- usr/share/zeta/hooks/. zeta-makepkg scans the stage for *.hook files,
|
||||
-- validates each one (a hook is a pure-data Lua table carrying `trigger`
|
||||
-- and `action`), and merges the found paths into the generated `files`
|
||||
-- whitelist so the hook is committed alongside the declared files.
|
||||
--
|
||||
-- To try this:
|
||||
-- 1. Unpack the source tarball: tar -xzf hooks-demo-1.0.tar.gz
|
||||
-- 2. Run the script manually: cd hooks-demo-1.0 && DESTDIR=/tmp/stage sh build.sh
|
||||
-- 3. Inspect the staged output: find /tmp/stage
|
||||
-- 4. Build with makepkg: zeta-makepkg hooks-demo.recipe
|
||||
|
||||
return {
|
||||
name = "hooks-demo",
|
||||
version = "1.0",
|
||||
summary = "Example package shipping a post-transaction hook",
|
||||
url = "hooks-demo-1.0.tar.gz",
|
||||
sha256 = nil,
|
||||
deps = {},
|
||||
build_system = "custom",
|
||||
build_script = "build.sh",
|
||||
files = { "usr/bin/hooks-demo", "usr/share/zeta/hooks/hooks-demo.hook" },
|
||||
test = "test -x ${DESTDIR}/usr/bin/hooks-demo",
|
||||
}
|
||||
@@ -11,6 +11,7 @@ local recipe = require("recipe")
|
||||
local builder = require("builder")
|
||||
local packager = require("packager")
|
||||
local indexer = require("indexer")
|
||||
local hooks = require("hooks")
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Dependency resolution (--follow)
|
||||
@@ -118,6 +119,8 @@ function build.single(recipe_path, opts)
|
||||
|
||||
builder.run_build(r, source_dir, stage_dir, { jobs = opts.jobs })
|
||||
|
||||
local hook_paths = hooks.scan_hooks(stage_dir)
|
||||
|
||||
if r.test then
|
||||
builder.run_test(r.test, stage_dir)
|
||||
else
|
||||
@@ -126,7 +129,7 @@ function build.single(recipe_path, opts)
|
||||
|
||||
local tarball_path = path.join(pkg_dir, r.name .. "-" .. r.version .. ".tar.gz")
|
||||
local sha256 = packager.create_tarball(stage_dir, tarball_path)
|
||||
packager.write_manifest(r, sha256, pkg_dir, opts.repo)
|
||||
packager.write_manifest(r, sha256, pkg_dir, opts.repo, hook_paths)
|
||||
|
||||
if not opts.no_index then
|
||||
indexer.generate(packages_dir)
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
-- hooks.lua -- scan and validate post-transactional hook files in the stage.
|
||||
--
|
||||
-- Zeta packages may ship hooks under usr/share/zeta/hooks/*.hook. A hook is
|
||||
-- a Lua file returning a table of the form:
|
||||
--
|
||||
-- return {
|
||||
-- order = 100,
|
||||
-- trigger = { ... },
|
||||
-- action = { ... },
|
||||
-- }
|
||||
--
|
||||
-- scan_hooks(stage_dir) finds every *.hook file under the stage's
|
||||
-- usr/share/zeta/hooks/ directory, loads each one in an empty environment
|
||||
-- (the same lockdown as recipe.load), verifies it returns a table carrying
|
||||
-- `trigger` and `action` tables, and returns the stage-relative paths
|
||||
-- (e.g. "usr/share/zeta/hooks/demo.hook"). A malformed hook — bad syntax,
|
||||
-- runtime error, non-table result, or missing trigger/action — raises.
|
||||
|
||||
local hooks = {}
|
||||
|
||||
local path = require("path")
|
||||
local log = require("log")
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
-- Private helpers
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
-- Load a single hook file under an empty environment and perform minimal
|
||||
-- validation. Returns the hook table or raises on any failure.
|
||||
local function load_hook(filepath)
|
||||
local f, err = io.open(filepath, "rb")
|
||||
if not f then
|
||||
error(("hook %q: cannot open: %s"):format(filepath, tostring(err)), 0)
|
||||
end
|
||||
local src = f:read("*a")
|
||||
f:close()
|
||||
|
||||
-- Hooks run in a minimal environment — they're pure data, but we still
|
||||
-- lock down globals to prevent accidental side effects (same as recipes).
|
||||
local env = {}
|
||||
local chunk
|
||||
if setfenv then
|
||||
chunk, err = loadstring(src, "@" .. filepath)
|
||||
if chunk then setfenv(chunk, env) end
|
||||
else
|
||||
chunk, err = load(src, "@" .. filepath, "t", env)
|
||||
end
|
||||
if not chunk then
|
||||
error(("hook %q: syntax error: %s"):format(filepath, tostring(err)), 0)
|
||||
end
|
||||
|
||||
local ok, raw = pcall(chunk)
|
||||
if not ok then
|
||||
error(("hook %q: runtime error: %s"):format(filepath, tostring(raw)), 0)
|
||||
end
|
||||
|
||||
if type(raw) ~= "table" then
|
||||
error(("hook %q: must return a table"):format(filepath), 0)
|
||||
end
|
||||
if type(raw.trigger) ~= "table" then
|
||||
error(("hook %q: missing or invalid `trigger` table"):format(filepath), 0)
|
||||
end
|
||||
if type(raw.action) ~= "table" then
|
||||
error(("hook %q: missing or invalid `action` table"):format(filepath), 0)
|
||||
end
|
||||
|
||||
return raw
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
-- Public API
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
-- Scan the stage for usr/share/zeta/hooks/*.hook, validate each one, and
|
||||
-- return their stage-relative paths (sorted for deterministic manifests).
|
||||
-- Returns an empty list when no hooks directory exists. Raises on the first
|
||||
-- malformed hook.
|
||||
function hooks.scan_hooks(stage_dir)
|
||||
local hooks_dir = path.join(stage_dir, "usr/share/zeta/hooks")
|
||||
if not path.exists(hooks_dir) then
|
||||
return {}
|
||||
end
|
||||
|
||||
log.step("scanning hooks in " .. hooks_dir)
|
||||
|
||||
local found = {}
|
||||
local f = io.popen("find " .. path.quote(hooks_dir)
|
||||
.. " -type f -name '*.hook' 2>/dev/null | sort")
|
||||
if f then
|
||||
for line in f:lines() do
|
||||
if line ~= "" then found[#found + 1] = line end
|
||||
end
|
||||
f:close()
|
||||
end
|
||||
|
||||
local base = stage_dir:gsub("/+$", "")
|
||||
local rels = {}
|
||||
for _, abs in ipairs(found) do
|
||||
load_hook(abs) -- raises on malformed hooks
|
||||
local rel = abs:sub(#base + 2) -- strip "<stage_dir>/" prefix
|
||||
rels[#rels + 1] = rel
|
||||
log.ok(("hook %s"):format(rel))
|
||||
end
|
||||
|
||||
return rels
|
||||
end
|
||||
|
||||
return hooks
|
||||
@@ -97,7 +97,13 @@ end
|
||||
-- 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, repo)
|
||||
--
|
||||
-- `hooks` (optional) is a list of stage-relative paths of packaged hook
|
||||
-- files (usr/share/zeta/hooks/*.hook). When `recipe.files` is set, the
|
||||
-- hook paths are merged into the emitted files whitelist so the hooks are
|
||||
-- committed alongside the whitelisted files. When `recipe.files` is nil,
|
||||
-- hooks are ignored (all staged files commit anyway).
|
||||
function packager.write_manifest(recipe, tarball_sha256, pkg_dir, repo, hooks)
|
||||
local repo = repo or "https://raw.githubusercontent.com/gretagen/zeta-packages/refs/heads/main"
|
||||
local lines = {}
|
||||
local function emit(fmt, ...)
|
||||
@@ -129,6 +135,9 @@ function packager.write_manifest(recipe, tarball_sha256, pkg_dir, repo)
|
||||
for _, f in ipairs(recipe.files) do
|
||||
file_strs[#file_strs + 1] = string.format('"%s"', escape(f))
|
||||
end
|
||||
for _, h in ipairs(hooks or {}) do
|
||||
file_strs[#file_strs + 1] = string.format('"%s"', escape(h))
|
||||
end
|
||||
emit(' files = { %s },', table.concat(file_strs, ", "))
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user