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 LEVELsets 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:LEVELdoes 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 thepkgforgeparent logger.- A malformed
--loglevelvalue (an unknown level name, or the wrongNAME:LEVELseparator) exits 2 with a usage error, before the command runs. - Log color follows the standard
NO_COLOR/FORCE_COLORconvention 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/nullor 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/nullexits 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.
--buildrootdefaults to the current directory and--dbtoPKGFORGE_DB;dbdumpandcompactexit 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 missingPKGFORGE_DBor a typo fails the build instead of packaging an empty manifest;installandscancreate the DB. The one exception: if that default resolves to/(e.g. a container's defaultWORKDIR, withPKGFORGE_ROOTunset or lost),install/scanexit 2 instead of treating the live filesystem as the build root -- pass--buildroot /(orPKGFORGE_ROOT=/) to do that on purpose. - Ownership is opt-in.
installrecords owner/group but only applies them with--chown, so an unprivileged build doesn't fail trying tochown. - Sources are never modified, by default.
install's default--method copycopies a file source's content, permission bits and modification time, so-m/--chownnever 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/--chownthen affect the source too) or--method move(consumes the source, rolled back on a staging failure) viaPKGFORGE_INSTALL_METHOD; see Commands. - No external archiver required for a tar-family source given as a real
path — stdlib
tarfilehandles it;bsdtaris 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.