Project file: pkgforge.cfg
A package that is staged the same way on every build can describe that once in
pkgforge.cfg: where the build root and the file DB are, which directories the
package owns, the owner, group and modes its entries get, and the steps that
stage it. pkgforge stage then runs the steps, and the commands read the rest.
Where the file is found
pkgforge reads one file, chosen in this order:
--config FILE(-c FILE), given before the command:pkgforge --config build/pkgforge.cfg stage.- The
PKGFORGE_CONFIGenvironment variable. It must name an existing file (exit 2 otherwise); an empty value reads no file at all, not even apkgforge.cfgin the working directory. pkgforge.cfgin the working directory, when there is one.
With none of these, pkgforge runs on its flags and environment alone. The file describes one package, so there is no per-user or system file.
A setting that can come from several places takes the first of: the command-line
flag, the environment variable, the file, the default. buildroot, db,
db_format and own are the settings the file and the command line share;
own has no environment variable.
Format
The file is TOML. A first line #!yaml makes it YAML instead (this needs the
yaml extra, pip install 'pkgforge[yaml]'). In TOML the top-level keys come
before the first table. A relative buildroot, db (unless it is "-") and a
[[stage]] step's src are relative to the directory of the file, so
pkgforge --config /work/pkg/pkgforge.cfg stage behaves the same from any
directory. A dest, dir, file and symlink are paths in the package, below
the build root, and are not.
Any other key is an error, in every table. The file describes one package, so
!include and !load in a YAML file are refused (exit 2).
buildroot = "build/root"
db = "build/files.jsonl"
own = ["/opt/tool"]
[package]
name = "tool"
version = "1.0"
[[stage]]
src = "bin/tool"
dest = "/opt/tool/bin"
mode = "755"
Keys
Top level
| Key | Type | Default | Meaning |
|---|---|---|---|
buildroot |
text | . |
the build root; same as --buildroot |
db |
text | no file DB | the file DB; "-" means none. Same as --db |
db_format |
text | from the db suffix |
jsonl, yaml, sqlite or a registered backend. Same as --db-format |
own |
list of text | none | directory prefixes the package owns, see Owned directories |
[package]
The main (and source) package. Other values can name its keys once with
${package.KEY}.
| Key | Type | Default | Grammar |
|---|---|---|---|
name |
text | required | a package name: [a-z0-9][a-z0-9+.-]+ |
version |
text | none | [0-9][A-Za-z0-9.+~]* |
release |
text | none | [A-Za-z0-9.+~]+ |
arch |
text | none | all or any |
maintainer |
text | none | Name <address> |
summary, license, homepage |
text | none | one line, no control character |
description |
text | none | no control character but newline |
[package.rpm] holds requires, a list of text (one Requires: item each),
and [package.debian] holds depends, a list of text (one dependency each, no
,), section (lowercase, such as utils) and priority (required,
important, standard, optional or extra). [package.pacman] and
[package.apk] each hold depends, a list of one-word items (no whitespace)
that pkgforge package -f arch and -f apk write as one depend line each;
they are named for the package manager because arch is already a key of
[package]. A list item is not empty and has no control character.
[package]
name = "tool"
version = "1.0"
summary = "A tool"
[package.pacman]
depends = ["glibc", "bash>=5"]
[package.apk]
depends = ["musl", "bash"]
[[packages]]
Any number of subpackages built from the same source. [[packages]] needs
[package].
| Key | Type | Default | Meaning |
|---|---|---|---|
name |
text | required | the full package name, such as ${package.name}-doc; unique and not the [package] name |
summary, description |
text | none | as in [package] |
arch |
text | the main package's | all or any |
rpm, debian, pacman, apk |
table | empty | as in [package] |
The package of a rule or step, and -P, name [package] or one of these.
[defaults]
What an entry gets when neither a flag nor a rule sets it.
| Key | Type | Default | Meaning |
|---|---|---|---|
owner, group |
text | none | for every entry |
file_mode |
text | none | a regular file |
exec_mode |
text | none | a regular file with an execute bit on the staged file |
dir_mode |
text | none | a directory |
[[rules]]
Any number of rules. Each sets mode, owner, group, kind, caps and package
for the entries whose path matches.
| Key | Type | Default | Meaning |
|---|---|---|---|
match |
list of text | required, not empty | -X statements, see the exclude grammar. A rule fits an entry when its statements match the entry's path in the file DB, which is always /-rooted |
mode |
text | none | |
owner, group |
text | none | |
kind |
text | none | file, config, config-noreplace, doc, license or ghost; all but file and ghost fit a regular file only, so such a rule should match (?type:file) |
caps |
text | none | one setcap clause such as cap_net_bind_service=ep; a regular file that is not a ghost |
package |
text | the [package] name |
the package the entry ships in: the [package] name or a [[packages]] name |
/etc/hello/** matches what is below /etc/hello, not the directory itself.
[[stage]]
Each element is one step. A step names what it stages with exactly one of
src, dir, file and symlink; see Steps.
| Key | Type | Default | Applies to | Meaning |
|---|---|---|---|---|
src |
text | src |
the source to install, relative to the file | |
dest |
text | required with src |
src |
where to install it: a directory, or the final path with into = false |
dir |
text | dir |
a directory to create and record | |
file |
text | file |
an empty file to create and record | |
symlink |
text | symlink |
the link to create and record | |
target |
text | required with symlink |
symlink |
what the link points to, stored as written |
mode |
text | none | all but symlink |
a link has no mode |
owner, group |
text | none | all | |
kind |
text | none | all | as install -k; a dir fits file or ghost, a symlink too |
caps |
text | none | src, file |
as install --caps |
package |
text | a matching rule's, else the [package] name |
all | as install -P |
meta |
table of text | none | all | free-form metadata, as install -O |
type |
text | from the source | src |
file, directory or symlink |
into |
boolean | true |
src |
false treats dest as the final path, as install -T |
decompress |
text | none | src |
auto (from the source suffix), gz, xz, bz2, zst or lzma |
method |
text | copy |
src |
copy, link or move |
exclude |
list of text | none | src |
-X statements; an absolute pattern is relative to the source |
record_tree |
boolean | false |
src |
also record everything below dest, as install --record-tree |
A key that does not apply to a step's kind is an error.
[[users]] and [[groups]]
Accounts the package's files belong to. An owner or group that is not in Debian's
base-passwd set must be declared here, or recording it is an error (exit 2), a name
read from disk with -- included.
| Key | Type | Default | Meaning |
|---|---|---|---|
name |
text | required | the account name. A created name matches [a-z_][a-z0-9_-]{0,30} and is not a base-passwd name |
description |
text | none | [[users]] only: no control character, " or \ |
home, shell |
text | none | [[users]] only: absolute, with no whitespace or \ |
groups |
list of text | none | [[users]] only: supplementary groups, base-passwd or declared |
create |
boolean | true |
false when the account already exists on the target |
A created account needs [package] name. A created user's group of the same name is
implied, so a [[groups]] element may not repeat it.
Accounts
Debian's base-passwd accounts (pkgforge.entry.BASE_USERS and BASE_GROUPS:
root, daemon, www-data, nobody, the groups adm, tty, disk, staff,
and so on) exist on every target and need no declaration. Any other owner or group must be
declared, or recording it is an error (exit 2):
pkgforge: error: install: owner 'alice' is neither a base-passwd account nor declared in [[users]]
The check runs when the file is loaded ([defaults], [[rules]], [[stage]],
[[users]].groups), before install and scan stage anything, and again for every
record written, so an owner read from disk with -- is checked too. With no
project file only base names pass.
create = true (the default) makes the package create the account at install
time; create = false names one a dependency creates, which writes no line but
still owns files at install time, and takes no description, home, shell or
groups (the account is not changed). A created name must match
[a-z_][a-z0-9_-]{0,30}; a base-passwd name is refused as a declaration.
[package]
name = "tool"
[[users]]
name = "tool"
description = "tool daemon"
home = "/var/lib/tool"
groups = ["adm"]
[[groups]]
name = "web"
create = false
pkgforge sysfiles records /usr/lib/sysusers.d/NAME.conf for the created
accounts and /usr/lib/tmpfiles.d/NAME.conf for the ghost directories, and
pkgforge stage runs it after the steps, so a package needs no other step for
them. How each format uses the accounts is in
Build time vs install time ownership.
Values
- A mode is text of one to four octal digits:
mode = "0644", never0o644or644. The sentinels-,--andautoare refused, since a file states values and never asks for one to be read from disk. - An owner or group is an account name (
[A-Za-z_][A-Za-z0-9_.-]*). - Text stays text. A number or a boolean where text is expected is refused
instead of being turned into some other text (
expected a quoted string, not int); a TOML integer such as0o644would otherwise become the text420. Onlyintoandrecord_treeare booleans. ${package.name}is replaced by that key of the file, anywhere in a text value: any${table.key}works, and${list.0}an element of a list.$${is a literal${. A reference to something the file does not define is an error. The environment is never read.
[package]
name = "hello"
[defaults]
owner = "root"
group = "${package.name}"
[[users]]
name = "${package.name}"
How an entry gets its mode, owner and group
For each of the three, in this order, the first that has a value wins:
- The value given for the entry: a flag (
-m,-o,-g) or the step's ownmode,ownerandgroup.--/autoreads the value from the staged file. - The first
[[rules]]element whosematchfits the entry's path, if it sets that field. Only the first fitting rule counts. [defaults]:ownerandgroup; for the mode,dir_modefor a directory, elseexec_modefor a file with an execute bit, elsefile_mode.- Nothing: the field stays
-, which dump formats leave to the OS. A symlink never gets a mode.
kind and caps follow the same order without step 3: a flag or the step's own
value, then the first fitting rule, then file and no capabilities.
An installed source, and what a dir or file step creates, is staged with the
mode that was recorded. The entries below a record_tree = true step are chmod'ed to the
mode they resolved to (a rule or [defaults]; with neither, they keep the mode
they have on disk). The owned directories and the entries scan records are
recorded only: they keep the mode they have on disk.
[defaults]
owner = "root"
group = "root"
file_mode = "644"
exec_mode = "755"
[[rules]]
match = ["/etc/hello/**"]
mode = "640"
group = "adm"
[[stage]]
src = "src/hello"
dest = "/usr/bin"
[[stage]]
src = "src/hello.conf"
dest = "/etc/hello"
/usr/bin/hello fits no rule; it is executable, so it is recorded
755 root:root from [defaults]. /etc/hello/hello.conf fits the rule, which
sets the mode and the group, and [defaults] supplies the owner: 640 root:adm.
Giving the second step mode = "600" would record 600 root:adm.
How an entry gets its package
The package an entry ships in is the first of: the flag (-P, or the step's own
package), then the first [[rules]] element whose match fits and sets
package, then the [package] name. [defaults] has no package. With no
[package] the entry is recorded as -, the one package there is. A name the
file does not declare is an error (exit 2). How the generators use the packages
is in From pkgforge.cfg to packages.
Owned directories
own lists the directories the package owns. When an entry at or below an owned
prefix is recorded, each directory above it that is at or below an owned prefix
(the prefix included) is recorded first, top-down, unless the file DB holds it
already. Nothing is created or changed on disk for them. Each is resolved like any
other entry, so a rule and [defaults] give it its mode, owner and group.
own = ["/opt/tool", "/etc/tool"]
[defaults]
dir_mode = "755"
[[stage]]
src = "bin/tool"
dest = "/opt/tool/bin"
Staging /opt/tool/bin/tool records /opt/tool and /opt/tool/bin as directories
too. A prefix is a /-rooted path with no .., and not / itself.
--own PREFIX (repeatable) is the same setting on any command and replaces the
file's list. pkgforge dbdump --dirs owned keeps a directory entry only at or
below an owned prefix and leaves every other entry alone, so a manifest does not
claim /usr or /usr/bin; it is an error with no own.
Steps
pkgforge stage runs the steps in order. Missing parent directories are created.
Every src is checked before the first step runs, so a missing source stages
nothing.
srcwithdest: install the source into the directorydest, aspkgforge install. A directory or archive records one entry unlessrecord_tree = true.dir: create the directory and record it, aspkgforge mkdir.file: create an empty file and record it. It is how a file that only the installed system creates, such as a log, gets an entry (withkind = "ghost").symlinkwithtarget: create the link and record it, aspkgforge symlink.
pkgforge stage --init resets the file DB first, as initdb does; with no file
DB it logs a warning and goes on. It is the way to make a build start clean.
Each step logs one line on pkgforge.stage, and a last line reports
staged N step(s), M record(s). With a file DB the records are in it. With none,
they are printed to standard output as JSON Lines. A file with no [[stage]]
steps is an error for stage.
Errors
A problem with the file is exit 2 and one line naming the file and, where known, the line or the key:
pkgforge: error: b.cfg:3:9: Expected newline or end of document after a statement
pkgforge: error: j.cfg: expected a quoted string, not int [at rules[0].mode (ProjectConfig)]
pkgforge: error: a.cfg: ProjectConfig has no field(s) 'dbb'
pkgforge: error: d.cfg: dest: required with src [at stage[0] (ProjectConfig)]
pkgforge: error: PKGFORGE_CONFIG names no file: nope.cfg
pkgforge: error: a.cfg: defaults: owner 'alice' is neither a base-passwd account nor declared in [[users]] [(ProjectConfig)]
pkgforge: error: b.cfg: [[users]] and [[groups]] that create an account need [package] name [(ProjectConfig)]
pkgforge: error: c.cfg: kind: config needs a regular file, not a directory [at stage[0] (ProjectConfig)]
pkgforge: error: d.cfg: caps: invalid caps 'cap_net_raw=ep cap_net_admin=p': expected one lowercase setcap clause without spaces, such as cap_net_bind_service=ep [at rules[0] (ProjectConfig)]
pkgforge: error: e.cfg: name: 'daemon' is a base-passwd account: it needs no entry [at users[0] (ProjectConfig)]
pkgforge: error: g.cfg: groups[0]: 'svc' is the group of the created user of the same name: drop the [[groups]] element [(ProjectConfig)]
pkgforge: error: h.cfg: refusing !include: the policy does not allow includes [in h.cfg; included from h.cfg, line 1]
The whole file is checked before any command runs, whichever command it is. A file that cannot be read is exit 1.
The example package
examples/pkgforge.cfg describes the hello and hello-doc packages. examples/hello.spec
and examples/debian/ are generated from it
(see From pkgforge.cfg to packages):
# The "hello" example package, described once for pkgforge.
# `pkgforge --config pkgforge.cfg stage --init` stages every [[stage]] step below
# into the build root and records the metadata the RPM and Debian manifests use;
# `pkgforge dbdump -f rpmspec` and `pkgforge dbdump -f debian --skeleton` write the
# spec and the debian/ files for the two packages declared here.
# Directories the package owns. Recording a file below one also records the
# directory itself (and any owned directory above it), so no step spells them out.
own = [
"/usr/libexec/${package.name}",
"/etc/${package.name}",
"/usr/share/licenses/${package.name}",
"/usr/share/doc/${package.name}-doc",
"/var/lib/${package.name}",
]
# Written once and reused as ${package.KEY} anywhere in this file.
[package]
name = "hello"
version = "1.0"
release = "1"
arch = "all"
summary = "pkgforge example package"
description = """
A sample package whose file list, modes and owners come from a pkgforge file
DB. The hello user comes from the sysusers.d file in the package."""
license = "MIT"
maintainer = "Example Maintainer <maintainer@example.org>"
[package.debian]
section = "misc"
# A second package built from the same source: what a rule or a step assigns to
# it with `package` ships there.
[[packages]]
name = "${package.name}-doc"
summary = "pkgforge example package documentation"
description = "The README of the pkgforge example package."
# The account the package creates, which also makes the group of the same name.
# Any owner or group outside Debian's base-passwd set must be declared like this.
[[users]]
name = "${package.name}"
description = "hello example daemon"
home = "/var/lib/${package.name}"
# What an entry gets when neither a step nor a rule sets it. A file takes its
# mode from its step, so nothing depends on the permission bits of a checkout.
[defaults]
owner = "root"
group = "root"
dir_mode = "755"
# The first rule whose pattern fits a path decides: everything below
# /etc/hello is private to the adm group.
[[rules]]
match = ["/etc/${package.name}/**"]
mode = "640"
group = "adm"
# Everything under the documentation directory belongs to the second package.
[[rules]]
match = ["/usr/share/doc/${package.name}-doc", "/usr/share/doc/${package.name}-doc/**"]
package = "${package.name}-doc"
[[stage]]
dir = "/var/lib/hello"
owner = "${package.name}"
group = "${package.name}"
mode = "750"
# Created at install time by the generated tmpfiles.d file, so it is not shipped.
[[stage]]
dir = "/run/hello"
kind = "ghost"
owner = "${package.name}"
group = "${package.name}"
mode = "755"
[[stage]]
src = "src/hello"
dest = "/usr/bin"
mode = "755"
[[stage]]
symlink = "/usr/bin/hi"
target = "hello"
[[stage]]
src = "src/helper"
dest = "/usr/libexec/hello"
mode = "755"
caps = "cap_net_bind_service=ep"
[[stage]]
src = "src/hello.conf"
dest = "/etc/hello"
kind = "config-noreplace"
[[stage]]
src = "src/hello.service"
dest = "/usr/lib/systemd/system"
mode = "644"
[[stage]]
src = "src/LICENSE"
dest = "/usr/share/licenses/hello"
mode = "644"
kind = "license"
[[stage]]
src = "src/README"
dest = "/usr/share/doc/hello-doc"
mode = "644"
kind = "doc"
[[stage]]
file = "/var/log/hello.log"
mode = "640"
group = "adm"
kind = "ghost"