feat: --follow for deps and a custom example
--follow now follows the deptree of the definition --follow is incompatible with --all build_system = "custom" example with build.sh
This commit is contained in:
@@ -75,6 +75,8 @@ Flags:
|
||||
-w, --workers <N> Number of parallel package builds for --all (default: 1)
|
||||
--no-index Don't update packages/index.lua after building
|
||||
--keep-work Don't remove the build work directory
|
||||
--follow Build all dependencies first (bottom-up), then the package.
|
||||
Incompatible with --all.
|
||||
--force Overwrite existing package tarball
|
||||
--help Show this help
|
||||
|
||||
@@ -101,6 +103,7 @@ local function parse_cli(args)
|
||||
force = false,
|
||||
index = false,
|
||||
all = nil,
|
||||
follow = false,
|
||||
help = false,
|
||||
recipe = nil,
|
||||
jobs = nil, -- nil = auto-detect (nproc)
|
||||
@@ -142,6 +145,8 @@ local function parse_cli(args)
|
||||
opts.keep_work = true
|
||||
elseif a == "--force" then
|
||||
opts.force = true
|
||||
elseif a == "--follow" then
|
||||
opts.follow = true
|
||||
elseif a == "--jobs" then
|
||||
i = i + 1
|
||||
if i > #args then
|
||||
@@ -217,6 +222,11 @@ if opts.help then
|
||||
os.exit(0)
|
||||
end
|
||||
|
||||
if opts.follow and opts.all then
|
||||
io.stderr:write("error: --follow and --all are incompatible\n")
|
||||
os.exit(1)
|
||||
end
|
||||
|
||||
if opts.index then
|
||||
indexer.generate(path.join(opts.output, "packages"))
|
||||
elseif opts.all then
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/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/mytool-.../stage)
|
||||
#
|
||||
# This script must compile/assemble the software and install every output
|
||||
# file under $DESTDIR with standard FHS paths (usr/bin/, usr/lib/, etc.).
|
||||
#
|
||||
# For a real project you would replace the example below with:
|
||||
# - Go: go build -o "$DESTDIR/usr/bin/mytool" .
|
||||
# - Rust: cargo build --release && install -D target/release/mytool "$DESTDIR/usr/bin/mytool"
|
||||
# - Zig: zig build -Doptimize=ReleaseSafe --prefix /usr -Ddestdir="$DESTDIR"
|
||||
# - Shell: install -D mytool.sh "$DESTDIR/usr/bin/mytool"
|
||||
# - Python: install -D mytool.py "$DESTDIR/usr/bin/mytool"
|
||||
# - Anything you can script.
|
||||
|
||||
set -eu
|
||||
|
||||
echo "==> Building mytool (custom)"
|
||||
|
||||
# Standalone Go binary: init module, build, install
|
||||
go mod init mytool
|
||||
go build -o mytool .
|
||||
|
||||
# Install into the staging tree
|
||||
install -Dm755 mytool "$DESTDIR/usr/bin/mytool"
|
||||
|
||||
echo "==> Install complete"
|
||||
@@ -0,0 +1,23 @@
|
||||
-- custom.recipe — demonstrates build_system = "custom" with a build.sh script.
|
||||
--
|
||||
-- The custom build system runs `build_script` from inside the extracted
|
||||
-- source directory. The script receives $DESTDIR pointing at the staging
|
||||
-- tree where installed files must be placed.
|
||||
--
|
||||
-- To try this:
|
||||
-- 1. Unpack the source tarball: tar -xzf mytool-1.0.tar.gz
|
||||
-- 2. Run the script manually: cd mytool-1.0 && DESTDIR=/tmp/stage sh build.sh
|
||||
-- 3. Inspect the staged output: find /tmp/stage
|
||||
-- 4. Build with makepkg: zeta-makepkg custom.recipe
|
||||
|
||||
return {
|
||||
name = "mytool",
|
||||
version = "1.0",
|
||||
summary = "Example custom-build package (static Go binary)",
|
||||
url = "mytool-1.0.tar.gz",
|
||||
sha256 = nil,
|
||||
deps = {},
|
||||
build_system = "custom",
|
||||
build_script = "build.sh",
|
||||
test = "test -x ${DESTDIR}/usr/bin/mytool",
|
||||
}
|
||||
+77
-1
@@ -12,11 +12,86 @@ local builder = require("builder")
|
||||
local packager = require("packager")
|
||||
local indexer = require("indexer")
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Dependency resolution (--follow)
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
-- Given a recipe directory and a dep spec (e.g. "ncurses" or "libfoo>=2.0"),
|
||||
-- return the path to <dep>.recipe if it exists, or nil.
|
||||
local function find_dep_recipe(recipe_dir, dep_spec)
|
||||
local name = dep_spec:match("^([%w%._+%-]+)")
|
||||
if not name then return nil end
|
||||
local p = path.join(recipe_dir, name .. ".recipe")
|
||||
if path.exists(p) then return p end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Recursively collect all dependency recipe paths, bottom-up (deps first).
|
||||
-- Returns an ordered list (no duplicates) and an in-progress set for cycle
|
||||
-- detection. Recipes for deps that don't have a .recipe file are silently
|
||||
-- skipped (assumed already installed).
|
||||
local function collect_deps(recipe_path, seen, order, in_progress)
|
||||
if seen[recipe_path] then return end
|
||||
if in_progress[recipe_path] then
|
||||
error(("dependency cycle involving %s"):format(path.basename(recipe_path)), 0)
|
||||
end
|
||||
|
||||
in_progress[recipe_path] = true
|
||||
|
||||
local r, err = recipe.load(recipe_path)
|
||||
if not r then
|
||||
in_progress[recipe_path] = nil
|
||||
log.warn(("skipping %s: %s"):format(path.basename(recipe_path), tostring(err)))
|
||||
return
|
||||
end
|
||||
|
||||
local dir = path.dirname(recipe_path)
|
||||
for _, dep in ipairs(r.deps) do
|
||||
local dep_recipe = find_dep_recipe(dir, dep)
|
||||
if dep_recipe then
|
||||
collect_deps(dep_recipe, seen, order, in_progress)
|
||||
end
|
||||
end
|
||||
|
||||
in_progress[recipe_path] = nil
|
||||
seen[recipe_path] = true
|
||||
order[#order + 1] = recipe_path
|
||||
end
|
||||
|
||||
-- Return an ordered list of recipe paths to build: all dependencies
|
||||
-- (bottom-up), then the target recipe itself.
|
||||
local function resolve_deps(recipe_path)
|
||||
local seen = {}
|
||||
local order = {}
|
||||
local in_progress = {}
|
||||
collect_deps(recipe_path, seen, order, in_progress)
|
||||
return order
|
||||
end
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Single recipe pipeline
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
function build.single(recipe_path, opts)
|
||||
-- If --follow, build dependencies first (bottom-up order, already sorted).
|
||||
if opts.follow then
|
||||
log.step("resolving dependencies")
|
||||
local ordered = resolve_deps(recipe_path)
|
||||
local dep_count = #ordered - 1 -- last is the target itself
|
||||
if dep_count > 0 then
|
||||
log.info(("found %d dependenc%s"):format(dep_count, dep_count == 1 and "y" or "ies"))
|
||||
for i = 1, dep_count do
|
||||
io.write("\n" .. string.rep("─", 36) .. "\n")
|
||||
log.step(("building dependency %s (%d/%d)"):format(
|
||||
path.basename(ordered[i]), i, dep_count))
|
||||
build.single(ordered[i], opts)
|
||||
end
|
||||
io.write("\n" .. string.rep("─", 36) .. "\n")
|
||||
else
|
||||
log.info("no unresolved dependencies")
|
||||
end
|
||||
end
|
||||
|
||||
local r, err = recipe.load(recipe_path)
|
||||
if not r then error(("recipe error: %s"):format(tostring(err)), 0) end
|
||||
|
||||
@@ -37,7 +112,8 @@ function build.single(recipe_path, opts)
|
||||
log.info(("work directory: %s"):format(work_dir))
|
||||
|
||||
local cache_dir = path.join(os.getenv("HOME") or "/tmp", ".cache", "zeta-makepkg", "sources")
|
||||
local cache_file = builder.fetch_source(r, cache_dir)
|
||||
local recipe_dir = path.dirname(recipe_path)
|
||||
local cache_file = builder.fetch_source(r, cache_dir, recipe_dir)
|
||||
local source_dir = builder.extract_source(r, cache_file, work_dir)
|
||||
|
||||
builder.run_build(r, source_dir, stage_dir, { jobs = opts.jobs })
|
||||
|
||||
@@ -104,7 +104,7 @@ end
|
||||
|
||||
-- Fetch source into the cache directory. Returns the cached file path,
|
||||
-- or nil if the URL is a git repository (signals "use git clone").
|
||||
function builder.fetch_source(recipe, cache_dir)
|
||||
function builder.fetch_source(recipe, cache_dir, local_dir)
|
||||
local url = recipe.url
|
||||
|
||||
-- Git repos are cloned, not cached as files.
|
||||
@@ -140,9 +140,18 @@ function builder.fetch_source(recipe, cache_dir)
|
||||
if not path.run("cp " .. path.quote(url) .. " " .. path.quote(cache_file)) then
|
||||
error(("failed to copy %s"):format(url), 0)
|
||||
end
|
||||
else
|
||||
-- Bare filename: resolve relative to the recipe's directory.
|
||||
if local_dir then
|
||||
local src = path.join(local_dir, url)
|
||||
log.step("copying " .. src)
|
||||
if not path.run("cp " .. path.quote(src) .. " " .. path.quote(cache_file)) then
|
||||
error(("failed to copy %s"):format(src), 0)
|
||||
end
|
||||
else
|
||||
error(("unrecognised url scheme: %s"):format(url), 0)
|
||||
end
|
||||
end
|
||||
else
|
||||
log.info("using cached source: " .. path.basename(cache_file))
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user