Skip to content

Configuration (config)

The project file pkgforge.cfg, read into frozen dataclasses. load reads and validates a file; every problem is a UsageError that names the file and the key. Project.from_config builds a project from a file, and Project(..., config=) takes a ProjectConfig you built yourself.

from pkgforge import Project
from pkgforge.config import load

config = load("pkgforge.cfg")
project = Project.from_config("pkgforge.cfg", buildroot="build/root")

pkgforge.config.ProjectConfig(buildroot=None, db=None, db_format=None, own=(), package=None, packages=(), defaults=EntryDefaults(), rules=(), stage=(), users=(), groups=()) dataclass

The whole pkgforge.cfg document.

buildroot, db and db_format set the options of the same name (db = "-" means no file DB), own lists the directory prefixes the package owns, and the tables are [package], [defaults], [[packages]], [[rules]], [[stage]], [[users]] and [[groups]]. A key that is not listed here is an error. An owner or group named by [defaults], a rule or a step must be a base-passwd account (BASE_USERS, BASE_GROUPS) or declared in [[users]]/[[groups]]; a created account needs [package] name. Build one with :func:load or :meth:from_dict.

buildroot = None class-attribute instance-attribute

db = None class-attribute instance-attribute

db_format = None class-attribute instance-attribute

defaults = field(default_factory=EntryDefaults) class-attribute instance-attribute

groups = () class-attribute instance-attribute

own = () class-attribute instance-attribute

package = None class-attribute instance-attribute

packages = () class-attribute instance-attribute

rules = () class-attribute instance-attribute

stage = () class-attribute instance-attribute

users = () class-attribute instance-attribute

__post_init__()

Source code in src/pkgforge/config.py
725
726
727
728
def __post_init__(self) -> None:
    _set(self, "own", tuple(_own_prefix(prefix) for prefix in self.own))
    _check_account_names(self)
    _check_packages(self)

from_dict(data) classmethod

The configuration described by data, a mapping shaped like the file (${...} already replaced).

Raises:

Type Description
UsageError

a key is unknown, a value has the wrong type or is not allowed; the text ends with [at KEY (ProjectConfig)].

Source code in src/pkgforge/config.py
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
@classmethod
def from_dict(cls, data: typing.Mapping[str, typing.Any]) -> "ProjectConfig":
    """The configuration described by ``data``, a mapping shaped like the
    file (``${...}`` already replaced).

    Raises:
        UsageError: a key is unknown, a value has the wrong type or is not
            allowed; the text ends with ``[at KEY (ProjectConfig)]``.
    """
    import anycfg

    try:
        return anycfg.hydrate(cls, data, extra="forbid")
    except anycfg.ConfigError as exc:
        raise UsageError(str(exc)) from exc

pkgforge.config.PackageInfo(name, version=None, release=None, arch=None, summary=None, license=None, maintainer=None, description=None, homepage=None, rpm=RpmInfo(), debian=DebianInfo(), pacman=PacmanInfo(), apk=ApkInfo()) dataclass

The [package] table: the main (and source) package, whose facts other values can name once with ${package.KEY}. name is required and a package name ([a-z0-9][a-z0-9+.-]+); arch is all or any; maintainer reads Name <address>. rpm, debian, pacman and apk hold the facts only that format uses.

apk = field(default_factory=ApkInfo) class-attribute instance-attribute

arch = None class-attribute instance-attribute

debian = field(default_factory=DebianInfo) class-attribute instance-attribute

description = None class-attribute instance-attribute

homepage = None class-attribute instance-attribute

license = None class-attribute instance-attribute

maintainer = None class-attribute instance-attribute

name instance-attribute

pacman = field(default_factory=PacmanInfo) class-attribute instance-attribute

release = None class-attribute instance-attribute

rpm = field(default_factory=RpmInfo) class-attribute instance-attribute

summary = None class-attribute instance-attribute

version = None class-attribute instance-attribute

__post_init__()

Source code in src/pkgforge/config.py
306
307
308
309
310
311
312
313
314
def __post_init__(self) -> None:
    _package_name(self.name)
    _matches(self.version, "version", _VERSION, "a version ([0-9][A-Za-z0-9.+~]*)")
    _matches(self.release, "release", _RELEASE, "a release ([A-Za-z0-9.+~]+)")
    _arch(self.arch)
    _matches(self.maintainer, "maintainer", _MAINTAINER, "'Name <address>'")
    for name in ("summary", "license", "homepage"):
        _one_line(getattr(self, name), name)
    _description(self.description)

pkgforge.config.SubPackageInfo(name, summary=None, description=None, arch=None, rpm=RpmInfo(), debian=DebianInfo(), pacman=PacmanInfo(), apk=ApkInfo()) dataclass

One [[packages]] element: a subpackage built from the same source. name is its full package name (hello-doc); the other keys are as in :class:PackageInfo, and an unset arch is the main package's.

apk = field(default_factory=ApkInfo) class-attribute instance-attribute

arch = None class-attribute instance-attribute

debian = field(default_factory=DebianInfo) class-attribute instance-attribute

description = None class-attribute instance-attribute

name instance-attribute

pacman = field(default_factory=PacmanInfo) class-attribute instance-attribute

rpm = field(default_factory=RpmInfo) class-attribute instance-attribute

summary = None class-attribute instance-attribute

__post_init__()

Source code in src/pkgforge/config.py
332
333
334
335
336
def __post_init__(self) -> None:
    _package_name(self.name)
    _one_line(self.summary, "summary")
    _description(self.description)
    _arch(self.arch)

pkgforge.config.RpmInfo(requires=()) dataclass

The rpm table of a package: facts for the RPM format only. requires lists one Requires: item each.

requires = () class-attribute instance-attribute

__post_init__()

Source code in src/pkgforge/config.py
229
230
def __post_init__(self) -> None:
    _set(self, "requires", _items(self.requires, "requires"))

pkgforge.config.DebianInfo(depends=(), section=None, priority=None) dataclass

The debian table of a package: facts for the Debian format only. depends lists one dependency each (no ,); section and priority are control fields.

depends = () class-attribute instance-attribute

priority = None class-attribute instance-attribute

section = None class-attribute instance-attribute

__post_init__()

Source code in src/pkgforge/config.py
243
244
245
246
247
248
249
def __post_init__(self) -> None:
    _set(self, "depends", _items(self.depends, "depends", comma=False))
    _matches(self.section, "section", _SECTION, "a section name")
    if self.priority is not None and self.priority not in _PRIORITIES:
        raise UsageError(
            f"priority: {self.priority!r} is not one of {', '.join(_PRIORITIES)}"
        )

pkgforge.config.PacmanInfo(depends=()) dataclass

The pacman table of a package: facts for the Arch format only. depends lists one depend item each, a single word such as glibc or bash>=5.

depends = () class-attribute instance-attribute

__post_init__()

Source code in src/pkgforge/config.py
268
269
def __post_init__(self) -> None:
    _set(self, "depends", _one_word_items(self.depends, "depends"))

pkgforge.config.ApkInfo(depends=()) dataclass

The apk table of a package: facts for the Alpine format only. depends lists one depend item each, a single word such as musl or bash>=5.

depends = () class-attribute instance-attribute

__post_init__()

Source code in src/pkgforge/config.py
280
281
def __post_init__(self) -> None:
    _set(self, "depends", _one_word_items(self.depends, "depends"))

pkgforge.config.EntryDefaults(owner=None, group=None, file_mode=None, exec_mode=None, dir_mode=None) dataclass

The [defaults] table: the owner, group and modes recorded for an entry that no flag and no rule sets. file_mode applies to a regular file, exec_mode to one with an execute bit and dir_mode to a directory. Modes are 1-4 octal digits; owner and group are account names.

dir_mode = None class-attribute instance-attribute

exec_mode = None class-attribute instance-attribute

file_mode = None class-attribute instance-attribute

group = None class-attribute instance-attribute

owner = None class-attribute instance-attribute

__post_init__()

Source code in src/pkgforge/config.py
352
353
354
355
356
def __post_init__(self) -> None:
    _set(self, "owner", _name(self.owner, "owner"))
    _set(self, "group", _name(self.group, "group"))
    for name in ("file_mode", "exec_mode", "dir_mode"):
        _set(self, name, _mode(getattr(self, name), name))

pkgforge.config.EntryRule(match, mode=None, owner=None, group=None, kind=None, caps=None, package=None) dataclass

One [[rules]] element: mode, owner, group, kind, caps and package for the entries whose DB path matches a match statement (the -X grammar). match is not empty. A kind other than file or ghost fits regular files only, so such a rule's match should say (?type:file).

caps = None class-attribute instance-attribute

group = None class-attribute instance-attribute

kind = None class-attribute instance-attribute

match instance-attribute

mode = None class-attribute instance-attribute

owner = None class-attribute instance-attribute

package = None class-attribute instance-attribute

__post_init__()

Source code in src/pkgforge/config.py
375
376
377
378
379
380
381
382
383
384
def __post_init__(self) -> None:
    if not self.match:
        raise UsageError("match: needs at least one statement")
    _set(self, "match", _statements(self.match, "match"))
    _set(self, "mode", _mode(self.mode, "mode"))
    _set(self, "owner", _name(self.owner, "owner"))
    _set(self, "group", _name(self.group, "group"))
    _set(self, "kind", _kind(self.kind))
    _set(self, "caps", _caps(self.caps))
    _set(self, "package", _package(self.package))

pkgforge.config.StageStep(src=None, dest=None, dir=None, file=None, symlink=None, target=None, mode=None, owner=None, group=None, kind=None, caps=None, package=None, meta=dict(), type=None, into=True, decompress=None, method=None, exclude=(), record_tree=False) dataclass

One [[stage]] element: a step that stages one thing into the build root. Exactly one of src (install a source at dest), dir, file or symlink (with target) names it. kind and package apply to every step, caps to src and file steps; type, into, decompress, method, exclude and record_tree are the install options and apply to src only.

caps = None class-attribute instance-attribute

decompress = None class-attribute instance-attribute

dest = None class-attribute instance-attribute

dir = None class-attribute instance-attribute

exclude = () class-attribute instance-attribute

file = None class-attribute instance-attribute

group = None class-attribute instance-attribute

into = True class-attribute instance-attribute

kind = None class-attribute instance-attribute

meta = field(default_factory=dict) class-attribute instance-attribute

method = None class-attribute instance-attribute

mode = None class-attribute instance-attribute

owner = None class-attribute instance-attribute

package = None class-attribute instance-attribute

record_tree = False class-attribute instance-attribute

src = None class-attribute instance-attribute

target = None class-attribute instance-attribute

type = None class-attribute instance-attribute

__post_init__()

Source code in src/pkgforge/config.py
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
def __post_init__(self) -> None:
    # Imported here: project.py imports this module.
    from ._install.decompress import _resolve_kind
    from .project import _INSTALL_METHODS

    named = [
        n for n in ("src", "dir", "file", "symlink") if getattr(self, n) is not None
    ]
    if len(named) != 1:
        raise UsageError("step needs exactly one of src, dir, file, symlink")
    for name in (*named, "dest", "target"):
        if getattr(self, name) == "":
            raise UsageError(f"{name}: must not be empty")
    has_src = self.src is not None
    if has_src and self.dest is None:
        raise UsageError("dest: required with src")
    if not has_src and self.dest is not None:
        raise UsageError("dest: only valid with src")
    if self.symlink is not None and self.target is None:
        raise UsageError("target: required with symlink")
    if self.symlink is None and self.target is not None:
        raise UsageError("target: only valid with symlink")
    if self.symlink is not None and self.mode is not None:
        raise UsageError("mode: not valid with symlink")
    if self.symlink is not None and "target" in self.meta:
        raise UsageError("meta: must not hold 'target' with symlink")
    if self.caps is not None and self.src is None and self.file is None:
        raise UsageError("caps: only valid with src or file")
    if not has_src:
        for name, default in (
            ("type", None),
            ("into", True),
            ("decompress", None),
            ("method", None),
            ("exclude", ()),
            ("record_tree", False),
        ):
            if getattr(self, name) != default:
                raise UsageError(f"{name}: only valid with src")

    _set(self, "mode", _mode(self.mode, "mode"))
    _set(self, "owner", _name(self.owner, "owner"))
    _set(self, "group", _name(self.group, "group"))
    _set(self, "kind", _kind(self.kind))
    _set(self, "caps", _caps(self.caps))
    _set(self, "package", _package(self.package))
    no_source = "symlink" if self.symlink is not None else "directory"
    if (
        (self.symlink is not None or self.dir is not None)
        and self.kind is not None
        and no_source not in _KIND_TYPES[self.kind]
    ):
        raise UsageError(
            f"kind: {self.kind} needs a regular file, not a {no_source}"
        )
    check_meta(self.meta)
    if self.type is not None:
        try:
            kind = _filetype(self.type)
        except UsageError as exc:
            raise UsageError(f"type: {exc}") from None
        if not isinstance(kind, FileType):
            raise UsageError(f"type: invalid type {self.type!r}")
        _set(self, "type", kind.value)
    if self.decompress is not None and self.decompress != "auto":
        try:
            _set(self, "decompress", _resolve_kind(self.decompress))
        except UsageError as exc:
            raise UsageError(f"decompress: {exc}") from None
    if self.method is not None and self.method not in _INSTALL_METHODS:
        raise UsageError(
            f"method: unknown install method {self.method!r}; choose from "
            f"{', '.join(_INSTALL_METHODS)}"
        )
    _set(self, "exclude", _statements(self.exclude, "exclude"))

pkgforge.config.UserAccount(name, description=None, home=None, shell=None, groups=(), create=True) dataclass

One [[users]] element: an account that owns files of the package.

With create = true the package creates it at install time (a systemd-sysusers u line, which also makes the group of the same name) with description as its GECOS text, home and shell, and adds it to the supplementary groups. With create = false the account exists on the target already, made by a dependency: no line is written and only name may be set, but its files are still owned by it at install time. Either way the name may own recorded files.

create = True class-attribute instance-attribute

description = None class-attribute instance-attribute

groups = () class-attribute instance-attribute

home = None class-attribute instance-attribute

name instance-attribute

shell = None class-attribute instance-attribute

__post_init__()

Source code in src/pkgforge/config.py
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
def __post_init__(self) -> None:
    name = _declared_name(self.name, self.create, BASE_USERS)
    if self.create and name in BASE_GROUPS:
        raise UsageError(
            f"name: {name!r} is a base-passwd group, which its own group "
            "would clash with"
        )
    _set(self, "name", name)
    if not self.create:
        # No sysusers line is written for an existing account, so these
        # would change nothing.
        for field_name in ("description", "home", "shell", "groups"):
            if getattr(self, field_name):
                raise UsageError(
                    f"{field_name}: only valid with create = true: an "
                    "existing account is not changed"
                )
    if self.description is not None and re.search(
        r'["\\\x00-\x1f\x7f]', self.description
    ):
        # systemd-sysusers ends the quoted text at `"` and drops a backslash.
        raise UsageError(
            "description: must hold no control character, double quote or "
            "backslash"
        )
    _set(self, "home", _absolute_word(self.home, "home"))
    _set(self, "shell", _absolute_word(self.shell, "shell"))
    _set(self, "groups", tuple(_name(group, "groups") for group in self.groups))

pkgforge.config.GroupAccount(name, create=True) dataclass

One [[groups]] element: a group that owns files of the package.

With create = true the package creates it at install time (a systemd-sysusers g line); with create = false it exists on the target already, and its files are still owned by it at install time. The group of a created user needs no element: it is implied.

create = True class-attribute instance-attribute

name instance-attribute

__post_init__()

Source code in src/pkgforge/config.py
590
591
def __post_init__(self) -> None:
    _set(self, "name", _declared_name(self.name, self.create, BASE_GROUPS))

pkgforge.config.load(path)

Read and validate the configuration file at path.

Relative buildroot, db (unless "-") and [[stage]] src values are taken relative to the file's directory.

Raises:

Type Description
UsageError

the file is malformed, names an undefined ${...}, holds an unknown key or a value that is not allowed; the text starts with path.

FileNotFoundError

there is no such file.

OSError

the file cannot be read.

Source code in src/pkgforge/config.py
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
def load(path: StrPath) -> ProjectConfig:
    """Read and validate the configuration file at ``path``.

    Relative ``buildroot``, ``db`` (unless ``"-"``) and ``[[stage]]`` ``src``
    values are taken relative to the file's directory.

    Raises:
        UsageError: the file is malformed, names an undefined ``${...}``, holds
            an unknown key or a value that is not allowed; the text starts
            with ``path``.
        FileNotFoundError: there is no such file.
        OSError: the file cannot be read.
    """
    data = _read(path)
    try:
        return ProjectConfig.from_dict(data)
    except UsageError as exc:
        raise UsageError(f"{os.fspath(path)}: {exc}") from exc