Skip to content

Project (project)

The Python API for build scripts: stage files into a build root, record their install metadata in a file DB and dump it into packaging manifests. The pkgforge command line is a wrapper over this class.

from pkgforge import Project

project = Project("build/root", "build/files.jsonl")
project.init()
project.install(["dist/app"], "/usr/bin", mode="755", parents=True)
project.scan("/etc")
project.dump("rpm", "build/files.txt")

pkgforge.project.Project(buildroot, db=None, *, db_format=None, config=None, config_path=None)

A build root plus an optional file DB.

buildroot is the local directory that maps to / in the DB. db is the file DB: a path (its format is db_format, else inferred from the suffix, else JSON Lines), a ready :class:~pkgforge.db.DbProvider, or None for no file DB, where operations write nothing and return the records they would have written.

Constructing a :class:Project validates its arguments and opens nothing. It holds no open resource, so it is not a context manager, and it is not thread-safe.

DB paths (dest, a scan path, a DB key) are /-rooted POSIX text; local paths are :data:~pkgforge.entry.StrPath.

config is the parsed pkgforge.cfg (:class:~pkgforge.config.ProjectConfig) and config_path the file it was read from, when known (a generated debian/rules points at it); :meth:from_config builds a project from a file.

Raises:

Type Description
TypeError

buildroot or db is not a path (or provider), or config is not a :class:~pkgforge.config.ProjectConfig.

ValueError

db is "-", or a provider is given with db_format.

UsageError

db_format names no registered format.

Source code in src/pkgforge/project.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
def __init__(
    self,
    buildroot: StrPath,
    db: typing.Optional[typing.Union[StrPath, DbProvider]] = None,
    *,
    db_format: typing.Optional[str] = None,
    config: typing.Optional[ProjectConfig] = None,
    config_path: typing.Optional[StrPath] = None,
) -> None:
    if config is None:
        config = ProjectConfig()
    elif not isinstance(config, ProjectConfig):
        raise TypeError(
            f"config must be a ProjectConfig, not {config.__class__.__name__}"
        )
    self.config = config
    self.config_path = (
        None if config_path is None else _path(config_path, "config_path").resolve()
    )
    self._begin_operation()
    self.buildroot = _path(buildroot, "buildroot")
    fmt = _check_db_format(db_format)
    if isinstance(db, DbProvider):
        if db_format:
            raise ValueError("db_format cannot be given with a DbProvider")
        self.db = db
    elif db is None:
        self.db = None
    elif isinstance(db, str) and db == "-":
        raise ValueError(
            "db '-' means standard input/output on the command line; "
            "pass db=None for no file DB"
        )
    else:
        self.db = open_db(_path(db, "db"), fmt)

buildroot = _path(buildroot, 'buildroot') instance-attribute

config = config instance-attribute

config_path = None if config_path is None else _path(config_path, 'config_path').resolve() instance-attribute

db instance-attribute

compact()

Collapse the DB to one record per live path, dropping superseded and removed ones.

Raises:

Type Description
UsageError

there is no file DB, or its file does not exist.

Source code in src/pkgforge/project.py
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
def compact(self) -> None:
    """Collapse the DB to one record per live path, dropping superseded and
    removed ones.

    Raises:
        UsageError: there is no file DB, or its file does not exist.
    """
    db = self._existing_db()
    before = self.load()
    live = sum(1 for entry in before.values() if entry is not None)
    db.compact()
    _COMPACT_LOGGER.info(
        "Compacted %s: %d live path(s), %d removal(s) dropped",
        db.path,
        live,
        len(before) - live,
    )

dbpath(localpath)

The /-rooted DB path for localpath, which must lie under the build root (ValueError otherwise).

Source code in src/pkgforge/project.py
237
238
239
240
def dbpath(self, localpath: StrPath) -> str:
    """The ``/``-rooted DB path for ``localpath``, which must lie under
    the build root (``ValueError`` otherwise)."""
    return Path("/", Path(localpath).relative_to(self.buildroot)).as_posix()

dump(fmt, output=None, *, exclude=(), dirs='all', skeleton=False, force=False)

Render the file DB with the format fmt (see :func:pkgforge.dbdump.dump).

With output, also write it whole: a file for a per-entry format (or an existing directory, which gets one list per package), a directory (created if missing) for a multi-artifact one. The packages are those of config ([package], [[packages]]). dirs is "all" or "owned": the latter keeps a directory entry only at or below a prefix of config.own.

With skeleton, debian also writes control, changelog, rules and source/format into a directory output; they and an rpmspec file are kept when they exist unless force. debian's rules points at config_path, which a skeleton therefore needs.

Returns:

Type Description
Rendered

The rendered bytes (bytes, or {filename: bytes}).

Raises:

Type Description
UsageError

fmt is unknown, dirs is neither "all" nor "owned" or is "owned" with no own prefixes, the file DB is missing, an entry ships in a package the configuration does not declare, a file output would hold the entries of two packages, the format needs keys the configuration lacks ([package] name; for a spec or a skeleton also version, summary and more), skeleton names a format with none, has no directory output or has no config_path to point at, or force fits neither.

UnsupportedOutputError

output has the wrong shape for fmt.

InvalidEntryError

an entry no format can render.

DumpError

an entry fmt cannot represent.

Source code in src/pkgforge/project.py
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
def dump(
    self,
    fmt: str,
    output: typing.Optional[StrPath] = None,
    *,
    exclude: typing.Sequence[str] = (),
    dirs: str = "all",
    skeleton: bool = False,
    force: bool = False,
) -> dbdump.Rendered:
    """Render the file DB with the format ``fmt`` (see
    :func:`pkgforge.dbdump.dump`).

    With ``output``, also write it whole: a file for a per-entry format
    (or an existing directory, which gets one list per package), a
    directory (created if missing) for a multi-artifact one. The packages
    are those of ``config`` (``[package]``, ``[[packages]]``). ``dirs`` is
    ``"all"`` or ``"owned"``: the latter keeps a directory entry only at or
    below a prefix of ``config.own``.

    With ``skeleton``, ``debian`` also writes ``control``, ``changelog``,
    ``rules`` and ``source/format`` into a directory ``output``; they and an
    ``rpmspec`` file are kept when they exist unless ``force``. ``debian``'s
    ``rules`` points at ``config_path``, which a skeleton therefore needs.

    Returns:
        The rendered bytes (``bytes``, or ``{filename: bytes}``).

    Raises:
        UsageError: ``fmt`` is unknown, ``dirs`` is neither ``"all"`` nor
            ``"owned"`` or is ``"owned"`` with no ``own`` prefixes, the
            file DB is missing, an entry ships in a package the
            configuration does not declare, a file ``output`` would hold
            the entries of two packages, the format needs keys the
            configuration lacks (``[package] name``; for a spec or a
            skeleton also ``version``, ``summary`` and more), ``skeleton``
            names a format with none, has no directory ``output`` or has no
            ``config_path`` to point at, or ``force`` fits neither.
        UnsupportedOutputError: ``output`` has the wrong shape for ``fmt``.
        InvalidEntryError: an entry no format can render.
        DumpError: an entry ``fmt`` cannot represent.
    """
    own = self._dump_own(dirs)
    instance, target = dbdump._prepare(fmt, output, skeleton=skeleton, force=force)
    db = self._existing_db()
    loaded = self.load()
    if not loaded:
        _DUMP_LOGGER.warning(
            "DB %s holds no entries; dumping an empty manifest", db.path
        )
    return dbdump.dump(
        loaded,
        instance.NAME,
        target,
        exclude=exclude,
        own=own,
        config=self.config,
        config_path=self.config_path,
        skeleton=skeleton,
        force=force,
    )

entries()

The live entries as (dbpath, entry) pairs sorted by path.

Source code in src/pkgforge/project.py
588
589
590
591
592
593
594
def entries(self) -> FileEntries:
    """The live entries as ``(dbpath, entry)`` pairs sorted by path."""
    live = [
        (path, entry) for path, entry in self.load().items() if entry is not None
    ]
    live.sort(key=lambda pathentry: pathentry[0])
    return live

from_config(path, *, buildroot=None, db=None, db_format=None) classmethod

A project described by the configuration file at path.

An argument beats the file, and the file beats the default (build root ".", no file DB). A db of "-" in the file means no file DB. The environment is not read.

Raises:

Type Description
UsageError

the file is invalid (see :func:pkgforge.config.load).

FileNotFoundError

there is no such file.

Source code in src/pkgforge/project.py
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
@classmethod
def from_config(
    cls,
    path: StrPath,
    *,
    buildroot: typing.Optional[StrPath] = None,
    db: typing.Optional[typing.Union[StrPath, DbProvider]] = None,
    db_format: typing.Optional[str] = None,
) -> "Project":
    """A project described by the configuration file at ``path``.

    An argument beats the file, and the file beats the default (build root
    ``"."``, no file DB). A ``db`` of ``"-"`` in the file means no file DB.
    The environment is not read.

    Raises:
        UsageError: the file is invalid (see :func:`pkgforge.config.load`).
        FileNotFoundError: there is no such file.
    """
    config = _load_config(path)
    if buildroot is None:
        buildroot = config.buildroot or "."
    if db is None and config.db not in (None, "-"):
        db = config.db
    if db_format is None and not isinstance(db, DbProvider):
        db_format = config.db_format
    return cls(buildroot, db, db_format=db_format, config=config, config_path=path)

init()

Create or reset (truncate) an empty file DB, creating its parent directory.

Raises:

Type Description
UsageError

there is no file DB.

Source code in src/pkgforge/project.py
564
565
566
567
568
569
570
571
572
573
574
575
def init(self) -> None:
    """Create or reset (truncate) an empty file DB, creating its parent
    directory.

    Raises:
        UsageError: there is no file DB.
    """
    if self.db is None:
        raise UsageError("no file DB configured; pass --db or set PKGFORGE_DB")
    Path(self.db.path).parent.mkdir(parents=True, exist_ok=True)
    self.db.init()  # provider-specific empty DB (truncate file / reset table)
    _INIT_LOGGER.info("Initialized empty DB at %s", self.db.path)

install(sources, dest, *, mode=DEFAULT, owner=DEFAULT, group=DEFAULT, type=None, kind=DEFAULT, caps=DEFAULT, package=DEFAULT, meta=None, into=True, parents=False, decompress=None, method='copy', exclude=(), record=True, record_tree=False, chown=False, remove_source=False)

Stage each source under the build root at dest and record it.

sources are files, directories or archives (a sequence, never one bare path). "-" reads standard input (only one source may), and "" means no source: an empty file, or with type directory an empty directory. dest is the /-rooted DB path: the directory to install into, or with into=False the final path (one source only).

Parameters:

Name Type Description Default
mode str

1-4 octal digits, "-" to leave the default, or "--" to read it from the staged file.

DEFAULT
owner str

an account name, "-" or "--" as for mode.

DEFAULT
group str

a group name, "-" or "--" as for mode.

DEFAULT
type Optional[FileType]

the entry's type; detected from each source when None.

None
kind str

how packaging treats the entry: "file", "config", "config-noreplace", "doc", "license" or "ghost"; "-" takes a matching rule's kind, else "file". All but file and ghost need a regular file.

DEFAULT
caps str

file capabilities the package sets on install, one setcap clause such as "cap_net_bind_service=ep"; "-" takes a matching rule's, else none. Regular files that are not ghosts only.

DEFAULT
package str

the package the entry ships in: the [package] name or a [[packages]] name; "-" takes a matching rule's package, else the [package] name ("-" with none).

DEFAULT
meta Optional[Mapping[str, str]]

extra {key: value} metadata recorded with the entry.

None
into bool

install into the directory dest (False: dest is the final path).

True
parents bool

create the missing parent directories of the destination.

False
decompress Optional[str]

a compression kind (gz, xz, bz2, zst or lzma) to decompress the source with, or "auto" to infer it from the source's suffix; None for no decompression.

None
method str

"copy", "link" (hardlink) or "move".

'copy'
exclude Sequence[str]

statements ([!][(?[!]test:arg)...]GLOB) for paths of a directory or archive source to skip; absolute globs are relative to the source.

()
record bool

write the entry to the file DB.

True
record_tree bool

also record every path below an installed directory that the DB does not hold yet.

False
chown bool

apply the recorded owner and group with chown.

False
remove_source bool

delete each source after it is staged.

False

Every source is validated and resolved before any is staged, so a bad argument or a destination collision leaves nothing on disk.

Returns:

Type Description
FileRecords

What was written, in write order (also with no file DB, when

FileRecords

nothing is written).

Raises:

Type Description
TypeError

sources is not a sequence of paths, dest is not text, type is not a :class:~pkgforge.entry.FileType, or an exclude statement is not text.

UsageError

an argument or a source is unusable, or dest leaves the build root.

PkgForgeError

staging failed (a missing decompressor or bsdtar, an unsafe archive member).

OSError

the file system refused.

Source code in src/pkgforge/project.py
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
def install(
    self,
    sources: typing.Sequence[StrPath],
    dest: str,
    *,
    mode: str = DEFAULT,
    owner: str = DEFAULT,
    group: str = DEFAULT,
    type: typing.Optional[FileType] = None,
    kind: str = DEFAULT,
    caps: str = DEFAULT,
    package: str = DEFAULT,
    meta: typing.Optional[typing.Mapping[str, str]] = None,
    into: bool = True,
    parents: bool = False,
    decompress: typing.Optional[str] = None,
    method: str = "copy",
    exclude: typing.Sequence[str] = (),
    record: bool = True,
    record_tree: bool = False,
    chown: bool = False,
    remove_source: bool = False,
) -> FileRecords:
    """Stage each source under the build root at ``dest`` and record it.

    ``sources`` are files, directories or archives (a sequence, never one
    bare path). ``"-"`` reads standard input (only one source may), and
    ``""`` means no source: an empty file, or with ``type`` directory an
    empty directory. ``dest`` is the ``/``-rooted DB path: the directory
    to install into, or with ``into=False`` the final path (one source
    only).

    Args:
        mode: 1-4 octal digits, ``"-"`` to leave the default, or ``"--"``
            to read it from the staged file.
        owner: an account name, ``"-"`` or ``"--"`` as for ``mode``.
        group: a group name, ``"-"`` or ``"--"`` as for ``mode``.
        type: the entry's type; detected from each source when ``None``.
        kind: how packaging treats the entry: ``"file"``, ``"config"``,
            ``"config-noreplace"``, ``"doc"``, ``"license"`` or
            ``"ghost"``; ``"-"`` takes a matching rule's kind, else
            ``"file"``. All but ``file`` and ``ghost`` need a regular file.
        caps: file capabilities the package sets on install, one
            ``setcap`` clause such as ``"cap_net_bind_service=ep"``;
            ``"-"`` takes a matching rule's, else none. Regular files
            that are not ghosts only.
        package: the package the entry ships in: the ``[package]`` name or
            a ``[[packages]]`` name; ``"-"`` takes a matching rule's
            package, else the ``[package]`` name (``"-"`` with none).
        meta: extra ``{key: value}`` metadata recorded with the entry.
        into: install into the directory ``dest`` (False: ``dest`` is the
            final path).
        parents: create the missing parent directories of the destination.
        decompress: a compression kind (gz, xz, bz2, zst or lzma) to
            decompress the source with, or ``"auto"`` to infer it from the
            source's suffix; ``None`` for no decompression.
        method: ``"copy"``, ``"link"`` (hardlink) or ``"move"``.
        exclude: statements (``[!][(?[!]test:arg)...]GLOB``) for paths of a
            directory or archive source to skip; absolute globs are
            relative to the source.
        record: write the entry to the file DB.
        record_tree: also record every path below an installed directory
            that the DB does not hold yet.
        chown: apply the recorded owner and group with ``chown``.
        remove_source: delete each source after it is staged.

    Every source is validated and resolved before any is staged, so a bad
    argument or a destination collision leaves nothing on disk.

    Returns:
        What was written, in write order (also with no file DB, when
        nothing is written).

    Raises:
        TypeError: ``sources`` is not a sequence of paths, ``dest`` is
            not text, ``type`` is not a :class:`~pkgforge.entry.FileType`,
            or an ``exclude`` statement is not text.
        UsageError: an argument or a source is unusable, or ``dest``
            leaves the build root.
        PkgForgeError: staging failed (a missing decompressor or ``bsdtar``,
            an unsafe archive member).
        OSError: the file system refused.
    """
    return self._run_install(
        sources,
        dest,
        mode=mode,
        owner=owner,
        group=group,
        type=type,
        kind=kind,
        caps=caps,
        package=package,
        meta=meta,
        into=into,
        parents=parents,
        decompress=decompress,
        method=method,
        exclude=exclude,
        record=record,
        record_tree=record_tree,
        chown=chown,
        remove_source=remove_source,
    )

load()

The whole DB as {dbpath: entry}; a removed path maps to None. {} when there is no file DB or its file does not exist yet.

Raises:

Type Description
DbError

the file is not valid in its format.

Source code in src/pkgforge/project.py
577
578
579
580
581
582
583
584
585
586
def load(self) -> Db:
    """The whole DB as ``{dbpath: entry}``; a removed path maps to ``None``.
    ``{}`` when there is no file DB or its file does not exist yet.

    Raises:
        DbError: the file is not valid in its format.
    """
    if self.db is None:
        return {}
    return self.db.load()

localpath(dbpath)

The local path under the build root for the DB path dbpath.

Source code in src/pkgforge/project.py
231
232
233
234
235
def localpath(self, dbpath: str) -> Path:
    """The local path under the build root for the DB path ``dbpath``."""
    p = PurePosixPath(os.fspath(dbpath))
    rel = p.relative_to("/") if p.is_absolute() else p
    return Path(self.buildroot, rel)

mkdir(paths, *, mode=DEFAULT, owner=DEFAULT, group=DEFAULT, kind=DEFAULT, package=DEFAULT, meta=None, parents=False)

Record each of paths (/-rooted DB paths) as a directory, creating it under the build root.

Nothing is read from a source, so this is how a directory with no files of its own is packaged. Running it again changes nothing. mode, owner, group, kind (file or ghost fit a directory), package and meta are as for :meth:install; parents creates the missing parent directories.

Returns:

Type Description
FileRecords

What was written, in write order.

Raises:

Type Description
TypeError

paths is not a sequence, or a path is not text.

UsageError

an argument is unusable, a path leaves the build root, or a parent is missing and parents is false.

OSError

the file system refused.

Source code in src/pkgforge/project.py
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
def mkdir(
    self,
    paths: typing.Sequence[str],
    *,
    mode: str = DEFAULT,
    owner: str = DEFAULT,
    group: str = DEFAULT,
    kind: str = DEFAULT,
    package: str = DEFAULT,
    meta: typing.Optional[typing.Mapping[str, str]] = None,
    parents: bool = False,
) -> FileRecords:
    """Record each of ``paths`` (``/``-rooted DB paths) as a directory,
    creating it under the build root.

    Nothing is read from a source, so this is how a directory with no
    files of its own is packaged. Running it again changes nothing.
    ``mode``, ``owner``, ``group``, ``kind`` (``file`` or ``ghost`` fit a
    directory), ``package`` and ``meta`` are as for :meth:`install`; ``parents``
    creates the missing parent directories.

    Returns:
        What was written, in write order.

    Raises:
        TypeError: ``paths`` is not a sequence, or a path is not text.
        UsageError: an argument is unusable, a path leaves the build root,
            or a parent is missing and ``parents`` is false.
        OSError: the file system refused.
    """
    return self._mkdir(
        paths,
        mode=mode,
        owner=owner,
        group=group,
        kind=kind,
        package=package,
        meta=meta,
        parents=parents,
        logger=_MKDIR_LOGGER,
        fresh=True,
    )

package(fmt, output='.', *, arch=None, compression=None)

Build one package per declared package, in the format fmt, from the file DB and the build root (see :func:pkgforge.package.build).

Nothing is staged: every live entry that is not a ghost must already be in the build root with its recorded type, and files the DB does not record are not shipped. Owners and modes come from the DB, so a package built from a build root owned by anyone holds the same bytes. The packages go into the directory output (created with the first package, and not left behind by a failed build); arch names the architecture of an any package in the format's own spelling (default: this machine's) and compression is one the format offers (default: its first).

Returns:

Type Description
List[Path]

The paths written, the main package first, then each

List[Path]

[[packages]] element in order.

Raises:

Type Description
UsageError

there is no file DB or its file is missing, the build root does not exist, fmt is unknown, arch or compression does not fit the format, the format needs a [package] key the configuration lacks, SOURCE_DATE_EPOCH is not a number, an entry ships in a package the configuration does not declare, a recorded path is missing from the build root, has another type there or lies below a symlink that leads outside it, or zstd is asked for with neither Python 3.14 nor the zstd extra.

InvalidEntryError

an entry no format can represent.

DumpError

an entry fmt cannot represent (a user or group name longer than a tar header holds).

PkgForgeError

a tool the format runs failed.

OSError

a file could not be read or written.

Source code in src/pkgforge/project.py
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
def package(
    self,
    fmt: str,
    output: StrPath = ".",
    *,
    arch: typing.Optional[str] = None,
    compression: typing.Optional[str] = None,
) -> typing.List[Path]:
    """Build one package per declared package, in the format ``fmt``, from
    the file DB and the build root (see :func:`pkgforge.package.build`).

    Nothing is staged: every live entry that is not a ghost must already be
    in the build root with its recorded type, and files the DB does not
    record are not shipped. Owners and modes come from the DB, so a package
    built from a build root owned by anyone holds the same bytes. The
    packages go into the directory ``output`` (created with the first
    package, and not left behind by a failed build); ``arch`` names the
    architecture of an ``any`` package in the format's own spelling
    (default: this machine's) and ``compression`` is one the format offers
    (default: its first).

    Returns:
        The paths written, the main package first, then each
        ``[[packages]]`` element in order.

    Raises:
        UsageError: there is no file DB or its file is missing, the build
            root does not exist, ``fmt`` is unknown, ``arch`` or
            ``compression`` does not fit the format, the format needs a
            ``[package]`` key the configuration lacks, ``SOURCE_DATE_EPOCH``
            is not a number, an entry ships in a package the configuration
            does not declare, a recorded path is missing from the build root,
            has another type there or lies below a symlink that leads
            outside it, or ``zstd`` is asked for with neither Python 3.14
            nor the ``zstd`` extra.
        InvalidEntryError: an entry no format can represent.
        DumpError: an entry ``fmt`` cannot represent (a user or group name
            longer than a tar header holds).
        PkgForgeError: a tool the format runs failed.
        OSError: a file could not be read or written.
    """
    self._existing_db()
    root = self._rootpath("/")
    if not root.exists():
        raise UsageError(f"build root {root} does not exist")
    return _build_packages(
        self.load(),
        fmt,
        output,
        buildroot=root,
        config=self.config,
        config_path=self.config_path,
        arch=arch,
        compression=compression,
    )

scan(path, *, mode=DEFAULT, owner=DEFAULT, group=DEFAULT, meta=None, dir_mode=None, missing=False, drop_stale=False, exclude=())

Walk path (a /-rooted DB path, below the build root) and record every path below it with its on-disk type; path itself is not recorded.

Parameters:

Name Type Description Default
mode str

the mode of regular files: 1-4 octal digits, "-" or "--" (read it from disk). Never applies to a directory or a symlink.

DEFAULT
owner str

an account name, "-" or "--" (read it from disk).

DEFAULT
group str

a group name, "-" or "--" (read it from disk).

DEFAULT
meta Optional[Mapping[str, str]]

extra {key: value} metadata for every entry.

None
dir_mode Optional[str]

the mode of directories; by default "--" when mode is "--", else "-".

None
missing bool

only record paths the DB does not hold yet.

False
drop_stale bool

afterwards remove each DB entry below path whose file is gone from the build root, unless exclude keeps it (needs a file DB).

False
exclude Sequence[str]

statements for paths to skip, as for :meth:install; absolute globs are relative to path.

()

Returns:

Type Description
FileRecords

What was written, in write order.

Raises:

Type Description
UsageError

path is missing or leaves the build root, an argument is invalid, or drop_stale has no file DB.

PkgForgeError

a path is neither a file, directory nor symlink and was not excluded.

Source code in src/pkgforge/project.py
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
def scan(
    self,
    path: str,
    *,
    mode: str = DEFAULT,
    owner: str = DEFAULT,
    group: str = DEFAULT,
    meta: typing.Optional[typing.Mapping[str, str]] = None,
    dir_mode: typing.Optional[str] = None,
    missing: bool = False,
    drop_stale: bool = False,
    exclude: typing.Sequence[str] = (),
) -> FileRecords:
    """Walk ``path`` (a ``/``-rooted DB path, below the build root) and
    record every path below it with its on-disk type; ``path`` itself is
    not recorded.

    Args:
        mode: the mode of regular files: 1-4 octal digits, ``"-"`` or
            ``"--"`` (read it from disk). Never applies to a directory or
            a symlink.
        owner: an account name, ``"-"`` or ``"--"`` (read it from disk).
        group: a group name, ``"-"`` or ``"--"`` (read it from disk).
        meta: extra ``{key: value}`` metadata for every entry.
        dir_mode: the mode of directories; by default ``"--"`` when
            ``mode`` is ``"--"``, else ``"-"``.
        missing: only record paths the DB does not hold yet.
        drop_stale: afterwards remove each DB entry below ``path`` whose
            file is gone from the build root, unless ``exclude`` keeps it
            (needs a file DB).
        exclude: statements for paths to skip, as for :meth:`install`;
            absolute globs are relative to ``path``.

    Returns:
        What was written, in write order.

    Raises:
        UsageError: ``path`` is missing or leaves the build root, an
            argument is invalid, or ``drop_stale`` has no file DB.
        PkgForgeError: a path is neither a file, directory nor symlink and
            was not excluded.
    """
    return _scan(
        self,
        path,
        mode=mode,
        owner=owner,
        group=group,
        meta=meta,
        dir_mode=dir_mode,
        missing=missing,
        drop_stale=drop_stale,
        exclude=exclude,
    )

stage(*, init=False)

Run the [[stage]] steps of the configuration, in order.

A step with src installs it at dest (as :meth:install, with the step's options); dir, file and symlink steps record a directory, an empty file or a link with no source. Missing parent directories are created. Every src is checked before the first step runs, so a missing source stages nothing.

Parameters:

Name Type Description Default
init bool

reset the file DB first, as :meth:init does; with no file DB, log a warning and go on.

False

Returns:

Type Description
FileRecords

What was written, in write order.

Raises:

Type Description
UsageError

the configuration has no [[stage]] steps, a source is missing (the text names the step), or a step is refused.

PkgForgeError

staging failed.

OSError

the file system refused.

Source code in src/pkgforge/project.py
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
def stage(self, *, init: bool = False) -> FileRecords:
    """Run the ``[[stage]]`` steps of the configuration, in order.

    A step with ``src`` installs it at ``dest`` (as :meth:`install`, with
    the step's options); ``dir``, ``file`` and ``symlink`` steps record a
    directory, an empty file or a link with no source. Missing parent
    directories are created. Every ``src`` is checked before the first
    step runs, so a missing source stages nothing.

    Args:
        init: reset the file DB first, as :meth:`init` does; with no file
            DB, log a warning and go on.

    Returns:
        What was written, in write order.

    Raises:
        UsageError: the configuration has no ``[[stage]]`` steps, a source
            is missing (the text names the step), or a step is refused.
        PkgForgeError: staging failed.
        OSError: the file system refused.
    """
    steps = self.config.stage
    if not steps:
        raise UsageError("no [[stage]] steps in the configuration")
    for number, step in enumerate(steps, 1):
        if step.src is not None and not (
            os.path.exists(step.src) or os.path.islink(step.src)
        ):
            raise UsageError(
                f"[[stage]] #{number}: source {step.src} does not exist"
            )
    if init:
        if self.db is None:
            _STAGE_LOGGER.warning("no file DB: nothing to initialize")
        else:
            self.init()
    self._begin_operation()
    records: FileRecords = []
    for number, step in enumerate(steps, 1):
        what = step.src or next(
            kind for kind in ("dir", "file", "symlink") if getattr(step, kind)
        )
        _STAGE_LOGGER.info(
            "[[stage]] #%d: %s -> %s",
            number,
            what,
            step.dest or step.dir or step.file or step.symlink,
        )
        records.extend(self._stage_step(step))
    live = dict(self.entries()) if self.db is not None else {}
    live.update({path: entry for path, entry in records if entry is not None})
    generated = self._sysfiles(sorted(live.items()), _STAGE_LOGGER)
    records.extend(generated)
    _STAGE_LOGGER.info(
        "generated %d system file(s)",
        sum(1 for _path, entry in generated if entry is not None),
    )
    _STAGE_LOGGER.info("staged %d step(s), %d record(s)", len(steps), len(records))
    return records

Record link (a /-rooted DB path) as a symbolic link to target and create it under the build root.

target is stored as written, relative or absolute, and may name nothing. A symlink has no mode. owner, group, kind (file or ghost fit a link), package and meta are as for :meth:install; parents creates the missing parent directories.

Returns:

Type Description
FileRecords

What was written, in write order.

Raises:

Type Description
TypeError

target is not text.

UsageError

target is empty or meta already holds target, an argument is unusable, link leaves the build root, or a parent is missing and parents is false.

OSError

the file system refused.

Source code in src/pkgforge/project.py
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
def symlink(
    self,
    target: str,
    link: str,
    *,
    owner: str = DEFAULT,
    group: str = DEFAULT,
    kind: str = DEFAULT,
    package: str = DEFAULT,
    meta: typing.Optional[typing.Mapping[str, str]] = None,
    parents: bool = False,
) -> FileRecords:
    """Record ``link`` (a ``/``-rooted DB path) as a symbolic link to
    ``target`` and create it under the build root.

    ``target`` is stored as written, relative or absolute, and may name
    nothing. A symlink has no mode. ``owner``, ``group``, ``kind`` (``file``
    or ``ghost`` fit a link), ``package`` and ``meta`` are as for :meth:`install`;
    ``parents`` creates the missing parent directories.

    Returns:
        What was written, in write order.

    Raises:
        TypeError: ``target`` is not text.
        UsageError: ``target`` is empty or ``meta`` already holds
            ``target``, an argument is unusable, ``link`` leaves the build
            root, or a parent is missing and ``parents`` is false.
        OSError: the file system refused.
    """
    return self._symlink(
        target,
        link,
        owner=owner,
        group=group,
        kind=kind,
        package=package,
        meta=meta,
        parents=parents,
        logger=_SYMLINK_LOGGER,
        fresh=True,
    )

sysfiles()

Write the sysusers.d and tmpfiles.d files the package needs under the build root and record them.

/usr/lib/sysusers.d/NAME.conf holds the accounts [[users]] and [[groups]] create; /usr/lib/tmpfiles.d/NAME.conf creates each ghost directory in the file DB at install time. NAME is [package] name. A file with nothing to say is not written, and one recorded earlier is removed with its entry. :meth:stage runs this after its steps.

Returns:

Type Description
FileRecords

What was written: the entry of each file, or None for a file

FileRecords

removed.

Raises:

Type Description
UsageError

there is something to write and no [package] name, or a ghost directory's path cannot be spelled in tmpfiles.d.

DbError

the file DB cannot be read.

OSError

the file system refused.

Source code in src/pkgforge/project.py
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
def sysfiles(self) -> FileRecords:
    """Write the ``sysusers.d`` and ``tmpfiles.d`` files the package needs
    under the build root and record them.

    ``/usr/lib/sysusers.d/NAME.conf`` holds the accounts ``[[users]]`` and
    ``[[groups]]`` create; ``/usr/lib/tmpfiles.d/NAME.conf`` creates each
    ghost directory in the file DB at install time. NAME is ``[package]
    name``. A file with nothing to say is not written, and one recorded
    earlier is removed with its entry. :meth:`stage` runs this after its
    steps.

    Returns:
        What was written: the entry of each file, or ``None`` for a file
        removed.

    Raises:
        UsageError: there is something to write and no ``[package]
            name``, or a ghost directory's path cannot be spelled in
            ``tmpfiles.d``.
        DbError: the file DB cannot be read.
        OSError: the file system refused.
    """
    self._begin_operation()
    return self._sysfiles(self.entries(), _SYSFILES_LOGGER)

verify(*, exclude=())

Compare the build root with the file DB, the way a packaging tool will see them.

Reports a file or symlink on disk with no live entry (unrecorded), a live entry whose path does not exist (missing; a ghost is never expected on disk), an on-disk type that differs from the recorded one (type) and a symlink whose target differs from the recorded target (target). A directory with no entry is not reported: the package owns only the directories it records. Mode, owner and group are not compared, since the DB's values are what every dump format ships (owned parents and scanned entries keep their on-disk mode by design).

The file DB inside the build root is skipped. exclude holds --exclude statements matched against DB paths, as dump does; a matching path is dropped from both sides and a matching directory is not descended.

Returns:

Type Description
List[VerifyIssue]

The issues sorted by path, then problem; empty when the tree

List[VerifyIssue]

matches the DB.

Raises:

Type Description
UsageError

there is no file DB or its file is missing, or the build root does not exist.

ExcludeSyntaxError

an exclude statement is invalid.

Source code in src/pkgforge/project.py
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
def verify(self, *, exclude: typing.Sequence[str] = ()) -> typing.List[VerifyIssue]:
    """Compare the build root with the file DB, the way a packaging tool
    will see them.

    Reports a file or symlink on disk with no live entry (``unrecorded``),
    a live entry whose path does not exist (``missing``; a ``ghost`` is
    never expected on disk), an on-disk type that differs from the recorded
    one (``type``) and a symlink whose target differs from the recorded
    ``target`` (``target``). A directory with no entry is not reported: the
    package owns only the directories it records. Mode, owner and group are
    not compared, since the DB's values are what every dump format ships
    (owned parents and scanned entries keep their on-disk mode by design).

    The file DB inside the build root is skipped. ``exclude`` holds
    ``--exclude`` statements matched against DB paths, as ``dump`` does; a
    matching path is dropped from both sides and a matching directory is
    not descended.

    Returns:
        The issues sorted by path, then problem; empty when the tree
        matches the DB.

    Raises:
        UsageError: there is no file DB or its file is missing, or the
            build root does not exist.
        ExcludeSyntaxError: an ``exclude`` statement is invalid.
    """
    self._existing_db()
    stmts = _statements(exclude)
    root = self._rootpath("/")
    if not root.exists():
        raise UsageError(f"build root {root} does not exist")
    live = {path: entry for path, entry in self.entries()}
    issues = [
        VerifyIssue(path, problem, recorded, found)
        for problem, path, recorded, found in _verify(
            self, live, stmts, logger=_VERIFY_LOGGER
        )
    ]
    _VERIFY_LOGGER.info("verified %d entries: %d issue(s)", len(live), len(issues))
    return issues

pkgforge.project.VerifyIssue

Bases: NamedTuple

One difference :meth:Project.verify found between the build root and the file DB.

found instance-attribute

path instance-attribute

problem instance-attribute

recorded instance-attribute