Skip to content

Unattended builds

pkgforge is built to run non-interactively inside a build pipeline. The environment variables are the primary configuration mechanism — set them once and every command picks them up. A typical staging sequence in a build script:

work="$(mktemp -d)"
export PKGFORGE_ROOT="$work/stage" PKGFORGE_DB="$work/files.jsonl"
mkdir -p "$PKGFORGE_ROOT"
printf '[package]\nname = "tool"\n' > "$work/pkgforge.cfg"
export PKGFORGE_CONFIG="$work/pkgforge.cfg"

pkgforge initdb

# stage binaries, config, and a whole tree; --record-tree also records the
# tree's own contents -- for a tree staged by something else, use
# "scan --missing DEST" instead, but never scan a directory the distro owns
# (e.g. /usr or /etc): every directory scan records becomes an RPM %dir
# claim, and a shared directory's mode/owner/group would conflict with the
# one the distro's own package ships
pkgforge install -p -m 755 -o root -g root ./build/tool /usr/bin
pkgforge install -p -m 640 -o root -g adm  ./config     /etc/tool
pkgforge install -D -d -m 755 -o root -g root --record-tree ./share /usr/share/tool

# emit packaging manifests
pkgforge dbdump -f rpmspecfiles rpm-files.txt
pkgforge dbdump -f debian debian/

The project file

PKGFORGE_CONFIG names the project file to read, and --config FILE (given before the command) beats it. It must name an existing file; an empty value reads no file at all, not even pkgforge.cfg in the working directory, so a pipeline sets PKGFORGE_CONFIG="" to ignore a stray file. A setting the file holds takes the first of: the command-line flag, the environment variable, the file, the default.

A pipeline whose package has a project file stages it in one command:

pkgforge --config pkgforge.cfg stage --init

Logging

Every command logs under its own logger, pkgforge.<command> (pkgforge.install, pkgforge.scan, pkgforge.dbdump, pkgforge.initdb, pkgforge.compact, pkgforge.stage, pkgforge.mkdir, pkgforge.symlink, pkgforge.verify, pkgforge.package), so logging.getLogger("pkgforge") controls all of them from Python.

  • -v, --verbose (repeatable) raises the running command's own log level; -q, --quiet (repeatable) lowers it. They offset each other in one combined count.
  • --loglevel LEVEL sets the running command's level directly; --loglevel pkgforge.<command>:LEVEL[,...] targets one or more loggers by name (e.g. --loglevel pkgforge.scan:WARNING). A bare --loglevel pkgforge:LEVEL does not change a command's output: the CLI always sets the dispatched command's own logger level explicitly, and that explicit level wins over anything inherited from the pkgforge parent logger.
  • A malformed --loglevel value (an unknown level name, or the wrong NAME:LEVEL separator) exits 2 with a usage error, before the command runs.
  • Log color follows the standard NO_COLOR/FORCE_COLOR convention and whether stderr is a terminal, so output redirected to a file or a CI log is always plain text.

Exit codes and completion

pkgforge's exit codes: 0 success; 2 a usage mistake (a bad or missing argument value, e.g. a missing source or an unknown --chown owner); 1 any other failure, including a closed output pipe (e.g. pkgforge dbdump ... | head); 130 Ctrl-C. On a 1 or 2, one pkgforge: error: ... line goes to stderr, no traceback. Set PKGFORGE_TRACEBACK=1 (1/true/yes/on; 0/false/no/off/n/f are off) to also print the traceback before that line. A closed pipe and Ctrl-C print nothing (Ctrl-C raises KeyboardInterrupt under PKGFORGE_TRACEBACK). pkgforge verify also exits 1 when it finds a difference, with the differences on stdout and no error line. A tail after -- is a usage mistake, not ignored.

The table is also in the agent help (AGENT_HELP=1 pkgforge --help), and an MCP tool call that fails reports exit code: N with its meaning.

Install shell completion by writing pkgforge --print-completion <shell> where the shell looks for it:

# bash: user-local (or /etc/bash_completion.d/pkgforge for a system install)
pkgforge --print-completion bash > ~/.local/share/bash-completion/completions/pkgforge
# fish
pkgforge --print-completion fish > ~/.config/fish/completions/pkgforge.fish
# zsh: anywhere on $fpath, named _pkgforge
pkgforge --print-completion zsh > "${fpath[1]}/_pkgforge"

Design notes for unattended use

  • No prompts. Commands never wait for input unless explicitly told to. A - source reads stdin to EOF (an empty stdin, e.g. /dev/null or an empty pipe, stages an empty file); a terminal or closed stdin is a usage error, not a wait, and a - source needs -T/-D (an explicit destination file name). dbdump --db - is the same shape: it reads the file DB as JSON Lines from stdin, and only ever reads to EOF (a terminal, a closed stdin or a device such as /dev/null exits 2 instead of waiting or dumping an empty manifest).
  • Private build root. The build root is a private directory the pipeline creates (mktemp -d), never a fixed path in a shared, world-writable directory such as /tmp — a predictable, pre-existing path lets another local user plant a symlink inside it before the run.
  • Resilient defaults. --buildroot defaults to the current directory and --db to PKGFORGE_DB; dbdump and compact exit 2 when no DB is configured or the named file does not exist (an existing empty DB is exit 0 with a WARNING), so a missing PKGFORGE_DB or a typo fails the build instead of packaging an empty manifest; install and scan create the DB. The one exception: if that default resolves to / (e.g. a container's default WORKDIR, with PKGFORGE_ROOT unset or lost), install/scan exit 2 instead of treating the live filesystem as the build root -- pass --buildroot / (or PKGFORGE_ROOT=/) to do that on purpose.
  • Ownership is opt-in. install records owner/group but only applies them with --chown, so an unprivileged build doesn't fail trying to chown.
  • Sources are never modified, by default. install's default --method copy copies a file source's content, permission bits and modification time, so -m/--chown never touch the source, and the build root may sit on any filesystem (including a tmpfs /tmp). A pipeline staging a build's own output -- already private, and never needed again afterwards -- can opt into --method link (hardlink; -m/--chown then affect the source too) or --method move (consumes the source, rolled back on a staging failure) via PKGFORGE_INSTALL_METHOD; see Commands.
  • No external archiver required for a tar-family source given as a real path — stdlib tarfile handles it; bsdtar is needed for a - (stdin) source (any format, including a tar stream) and for other formats (e.g. .iso). Neither path restores an archive's ownership or special bits as root, and a device node, FIFO or socket in the archive is refused.