Dump formats (dbdump)
The packaging-manifest format registry and its built-in formats. A
third-party format subclasses PerEntryFormat or MultiArtifactFormat with
its own NAME and advertises it in the pkgforge.dump_formats entry-point
group; see Adding a format. A format
that writes files its user edits sets DumpFormat.SKELETON (written with
dump(..., skeleton=True)) or DumpFormat.KEEP_EXISTING; such a file is kept,
with a warning, unless force=True. The checks every format must pass are in
Contract suite.
pkgforge.exceptions.DumpError
Bases: PkgForgeError, ValueError
A DB entry cannot be rendered in the chosen format.
Raised for a path the target format's own tooling cannot represent -- a
control character, or (rpm only) a %. Caught by
:func:pkgforge.cli.main's error boundary like any
:class:~pkgforge.exceptions.PkgForgeError: one stderr line, exit 1. Nothing
is written to OUTPUT first: every entry is rendered before OUTPUT is
opened, so this leaves no partial file.
pkgforge.exceptions.InvalidEntryError
Bases: UsageError, DumpError
A DB entry is malformed for every format: a key that is not an absolute
normalized path, an unknown type, a mode/owner/group outside
the recorded grammar, an unknown kind or one that does not fit the
type, or caps that are malformed or on anything but a regular
file that is not a ghost.
Raised by :func:_validate_entries before any format renders, so it
leaves no output behind. A :class:~pkgforge.exceptions.UsageError (exit 2):
the DB, not the format, holds the bad value.
pkgforge.exceptions.UnsupportedOutputError
Bases: UsageError, DumpError, NotImplementedError
OUTPUT is the wrong shape for the chosen format: a file for a multi-artifact format, or an existing directory for a per-entry one.
Subclasses :class:~pkgforge.exceptions.UsageError (exit 2, same as this
replaces two separate raises with) and :class:DumpError (the format
itself is refusing this OUTPUT, not a bad argument in general) and, on
top of both, :class:NotImplementedError (this format cannot write
this output shape) -- a valid MRO (measured, py3.14).
pkgforge.dbdump.dump(db, fmt, output=None, *, exclude=(), own=None, config=None, config_path=None, skeleton=False, force=False)
Render the live entries of db with the format fmt.
fmt is a registered format name or alias. Removed entries (None)
and entries matched by an exclude statement are dropped. With own
(/-rooted prefixes), so is every directory entry that is neither a
prefix nor below one; entries of other types are untouched. The rest are
sorted by path and validated. config names the packages the entries
ship in (default: an empty configuration, one unnamed package) and
config_path is the file it was read from, for a format that points at
it. With output the result is also written whole: a file for a
per-entry format (or a directory holding one list per package), a
directory (created if missing) for a multi-artifact one.
With skeleton a format that has one (debian) also writes the files
its user edits, into a directory output. A skeleton file, or for
rpmspec the spec, that already exists is kept with a warning unless
force.
Returns the rendered bytes (a bytes value, or {filename: bytes}).
Raises :class:~pkgforge.exceptions.UsageError for an unknown format or
exclude statement, an entry of a package config does not declare, a
format that needs [package] name or other keys, skeleton with a
format that has none or without a directory output, or force where
nothing is a skeleton;
:class:~pkgforge.exceptions.UnsupportedOutputError
for an output of the wrong shape (checked before anything is
rendered), :class:~pkgforge.exceptions.InvalidEntryError for an entry no
format can render and :class:~pkgforge.exceptions.DumpError for one this
format cannot.
Source code in src/pkgforge/dbdump/__init__.py
526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 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 575 576 577 578 579 580 | |
pkgforge.dbdump.DumpContext(config=ProjectConfig(), config_path=None, output=None)
dataclass
What a format renders against besides the entries.
config is the project configuration (its [package] and
[[packages]] name the packages), config_path the file it was read
from when known, and output the path being written (None for
stdout or when nothing is written).
config = dataclasses.field(default_factory=ProjectConfig)
class-attribute
instance-attribute
config_path = None
class-attribute
instance-attribute
output = None
class-attribute
instance-attribute
pkgforge.dbdump.DumpFormat(*, skeleton=False)
Bases: Registered, ABC
A packaging-manifest format, selected by dbdump -f NAME.
A subclass registers by declaring its own NAME (plus, optionally,
ALIASES); see :class:pkgforge._registry.Registered. Subclass
:class:PerEntryFormat or :class:MultiArtifactFormat -- they supply
:meth:check_output and :meth:write for their respective output shape --
or this class, supplying both, for a format with another shape.
skeleton (dbdump --skeleton) asks a format with :attr:SKELETON to
write the files its user edits as well.
Source code in src/pkgforge/dbdump/__init__.py
354 355 356 357 | |
ALIASES = ()
class-attribute
KEEP_EXISTING = False
class-attribute
NAME = ''
class-attribute
SKELETON = False
class-attribute
skeleton = skeleton
instance-attribute
check_output(output)
abstractmethod
Raise :class:UnsupportedOutputError if output is the wrong
shape for this format.
Source code in src/pkgforge/dbdump/__init__.py
363 364 365 366 | |
render(entries, context)
abstractmethod
Render the (already validated) entries to bytes, writing nothing.
Source code in src/pkgforge/dbdump/__init__.py
359 360 361 | |
write(rendered, output, *, force=False)
abstractmethod
Write rendered to output, whole or not at all; return the
files written. A skeleton file that exists is kept (and not returned)
unless force.
Source code in src/pkgforge/dbdump/__init__.py
368 369 370 371 372 373 374 | |
pkgforge.dbdump.PerEntryFormat(*, skeleton=False)
Bases: DumpFormat
A format that renders one line per DB entry.
Given a file (or stdout) it renders the entries of the one package that
has any; more than one package is a
:class:~pkgforge.exceptions.UsageError. Given an existing directory it
renders :attr:PACKAGE_FILE for every declared package, so a package with
no entry still gets its (possibly empty) list.
Source code in src/pkgforge/dbdump/__init__.py
354 355 356 357 | |
EMPTY = b''
class-attribute
PACKAGE_FILE = 'files-{package}.txt'
class-attribute
check_output(output)
Any path fits: a file, or a directory for one list per package.
Source code in src/pkgforge/dbdump/__init__.py
425 426 | |
render(entries, context)
Source code in src/pkgforge/dbdump/__init__.py
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 | |
render_entry(path, entry)
abstractmethod
Render one DB entry to its line of bytes.
Source code in src/pkgforge/dbdump/__init__.py
392 393 394 | |
write(rendered, output, *, force=False)
Source code in src/pkgforge/dbdump/__init__.py
428 429 430 431 432 433 434 435 436 437 | |
pkgforge.dbdump.MultiArtifactFormat(*, skeleton=False)
Bases: DumpFormat
A format that renders several named artifacts into a directory.
Source code in src/pkgforge/dbdump/__init__.py
354 355 356 357 | |
check_output(output)
Source code in src/pkgforge/dbdump/__init__.py
449 450 451 452 453 454 | |
render(entries, context)
abstractmethod
Render every artifact this format produces: {filename: bytes}.
Source code in src/pkgforge/dbdump/__init__.py
443 444 445 446 447 | |
write(rendered, output, *, force=False)
Source code in src/pkgforge/dbdump/__init__.py
456 457 458 459 460 461 462 463 464 | |
Built-in formats
pkgforge.dbdump.RPMSpecFiles(*, skeleton=False)
Bases: PerEntryFormat
RPM %files lines: %attr(mode,owner,group) "path", a %dir
prefix for directories, %caps(CAPS) for an entry with file
capabilities, and the entry kind's directive first (%config,
%config(noreplace), %doc, %license or %ghost; none for
file).
The path is quoted for rpm's %files -f parser (targets rpm 4.19+):
a backslash and a double quote are escaped, and the whole line is
written as UTF-8 with surrogateescape (a non-UTF-8 name round-trips
its original bytes). Glob characters are never escaped -- rpm's own
quoted-string globbing already matches only the literal name. A %
anywhere in the path, or a control character (including a tab), raises
:class:~pkgforge.exceptions.DumpError. Below rpm 4.19, use
:class:RPMSpecFilesPre419 instead -- this quoting is not safe there.
Into a directory it writes files-PACKAGE.txt for every declared
package (%files -n PACKAGE -f .../files-PACKAGE.txt); a package with
no entry gets one empty line, which rpm builds as an empty package.
Source code in src/pkgforge/dbdump/__init__.py
354 355 356 357 | |
ALIASES = ('rpm',)
class-attribute
instance-attribute
EMPTY = b'\n'
class-attribute
instance-attribute
NAME = 'rpmspecfiles'
class-attribute
instance-attribute
render_entry(path, entry)
Source code in src/pkgforge/dbdump/_rpm.py
176 177 | |
pkgforge.dbdump.RPMSpecFilesPre419(*, skeleton=False)
Bases: RPMSpecFiles
RPM %files lines for rpm older than 4.19 (measured against real
rpmbuild runs on 4.14.3, 4.16.1 and 4.18.2).
Inherits :class:RPMSpecFiles's %attr/%dir/kind/caps
rendering and batch-then-per-entry validation, overriding only the
escaping/rejection hooks: the path is written completely unquoted, and a
path containing a space, a glob character (* ? [ ] { }), % (in
any form), or a byte that is not valid UTF-8 raises
:class:~pkgforge.exceptions.DumpError instead of risking a silently wrong
or overmatched package. Use :class:RPMSpecFiles instead when the rpm
that builds the package is 4.19 or newer.
Source code in src/pkgforge/dbdump/__init__.py
354 355 356 357 | |
ALIASES = ('rpm-pre419',)
class-attribute
instance-attribute
NAME = 'rpmspecfiles-pre419'
class-attribute
instance-attribute
pkgforge.dbdump.RPMSpec(*, skeleton=False)
Bases: DumpFormat
The RPM spec for the packages pkgforge.cfg declares: one Name
preamble ([package]), a %package -n block per [[packages]]
element, a %install that runs pkgforge stage --init and writes one
%files list per package, and one %files -f per package. A
%post creates the tmpfiles.d paths of the package that ships
/usr/lib/tmpfiles.d/NAME.conf.
A package that records a systemd unit directly in
/usr/lib/systemd/system gets %systemd_post, %systemd_preun and
%systemd_postun_with_restart for its units. They need
systemd-rpm-macros (or EL8's systemd) when the package is built, and
expand to nothing without them.
Needs [package] name, version, summary and license, and a
summary for each [[packages]] element; an any subpackage of an
all main package is refused. Every configuration text has its %
doubled, so no macro is read from the file, and a %description line
whose first non-blank character is % or # is refused, since rpm
reads it as a section, a conditional or a comment. The spec holds no absolute
path: build it with rpmbuild -bb --define "pkgforge_config PATH"
NAME.spec, PATH being pkgforge.cfg, and pkgforge on PATH.
The package is built from the recorded files unchanged (no brp scripts, no
debuginfo). Writes a file or -; a spec that exists is kept with a
warning unless force, since it is meant to be edited. rpm 4.19+
creates the accounts from the shipped sysusers.d file; use
:class:RPMSpecPre419 for older rpm.
Source code in src/pkgforge/dbdump/__init__.py
354 355 356 357 | |
KEEP_EXISTING = True
class-attribute
instance-attribute
NAME = 'rpmspec'
class-attribute
instance-attribute
check_output(output)
Source code in src/pkgforge/dbdump/_rpmspec.py
229 230 231 232 233 234 | |
render(entries, context)
Source code in src/pkgforge/dbdump/_rpmspec.py
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | |
write(rendered, output, *, force=False)
Source code in src/pkgforge/dbdump/_rpmspec.py
236 237 238 239 240 241 | |
pkgforge.dbdump.RPMSpecPre419(*, skeleton=False)
Bases: RPMSpec
:class:RPMSpec for rpm older than 4.19: the %files lists are
rpmspecfiles-pre419's, and a %pre of the package that ships the
sysusers.d file creates the accounts itself.
Source code in src/pkgforge/dbdump/__init__.py
354 355 356 357 | |
NAME = 'rpmspec-pre419'
class-attribute
instance-attribute
pkgforge.dbdump.DebianFiles(*, skeleton=False)
Bases: MultiArtifactFormat
Debian packaging artifacts built from surviving DB entries, named for
the packages pkgforge.cfg declares ([package] and
[[packages]]; [package] name is required). For each package P:
P.install--dh_install-style lines<src> <dest-dir>(the source is the build-root-relative path, debhelper-escaped; the destination is the entry's parent directory, escaped for$/space only), one per non-directory entry;P.dirs--dh_installdirs-style lines (dest-escaped, one per directory entry, in every case -- an already-populated directory is a harmless duplicatemkdir), so a directory recorded empty (install -d) still reaches the package; never routed throughinstall, which would re-copy any children--excludedropped;P.conffiles-- theconfigandconfig-noreplaceentries outside/etc/(debhelper adds those under/etc/itself), one absolute path per line; whitespace in such a path raises :class:~pkgforge.exceptions.DumpError;P.postinst-- a maintainer script with#DEBHELPER#; onconfigureit runs, for each entry owned by an account the package creates (an owner or group outside the base-passwd set), a guardeddpkg-statoverride --update --addand then, for each entry withcaps, asetcapline;P.postrm-- the same header; onpurgeit removes those overrides;P.substvars--pkgforge:Depends=libcap2-binwhenPhas an entry withcaps, else an empty value, forDepends: ${pkgforge:Depends}.dh_prepdeletes this file, so it only takes effect when the dump runs afterdh_prep(override_dh_auto_install).
And once for the source:
not-installed-- the ghost entries of every package that are not directories, sorted, ininstall's source escaping, sodh_missing --fail-missingaccepts a ghost file left in the staging directory (debhelper reads only the source-wide file);fixperms-- a POSIXshscript, always written, that applies every pinned mode/owner/group directly to the package directories: run it fromoverride_dh_fixperms(afterdh_fixperms, which would otherwise strip the very bits this pins) assh debian/fixperms debian. Per pinned entry: onechown owner:group(orchown/chgrpwhen only one is pinned), thenchmod(chown before chmod, since chown clears a regular file's setuid/setgid bit even to the same owner); a symlink getschown -h/chgrp -hand never achmod(POSIXchmodhas no-hand would follow the link to a target outside the package tree). A path that dh_compress renamed to<path>.gzis found there; one that is gone (dh_usrlocal) is reported on stderr and skipped. With no pinned entry, the script is just its own header.
With skeleton (dbdump --skeleton) it also writes the files you edit:
control (a stanza per package, Depends listing
${pkgforge:Depends}), changelog (dated SOURCE_DATE_EPOCH, else
the current time), rules (the debian/tmp recipe, naming the project
file, mode 755) and source/format. They need a directory OUTPUT, the
project file's path and [package] version, summary and
maintainer; one that exists is kept with a warning unless force.
A ${NAME} in the summary, description, homepage or
maintainer of any package is :class:~pkgforge.exceptions.UsageError:
dpkg-gencontrol replaces it and nothing escapes it.
Every artifact is written for every declared package, so no stale one
survives. Writing refuses, before it writes anything: an existing
install, dirs, not-installed, postinst, postrm or
fixperms without the marker line this format puts first (a hand-written
file); a nameless install, dirs, conffiles, postinst or
postrm in the directory, which debhelper ignores once the
package-named file exists; and an existing P.conffiles that lists a
path the new one lacks.
The build needs dh $@ --with installsysusers (debhelper 13.10+) or
Build-Depends: dh-sequence-installsysusers so the accounts exist
before postinst applies the overrides.
Ghost entries are left out of install, dirs and fixperms, and
so are entries owned by an account the package creates: dpkg unpacks them
before postinst can create the account, so fixperms leaves them
root-owned and the override applies them. Such a symlink, or one with no
recorded mode, raises :class:~pkgforge.exceptions.DumpError.
A path with a control character raises
:class:~pkgforge.exceptions.DumpError while the artifacts are built in
memory; they are written only after :meth:render returns, so nothing
partial is left behind.
Source code in src/pkgforge/dbdump/__init__.py
354 355 356 357 | |
ALIASES = ('deb',)
class-attribute
instance-attribute
NAME = 'debian'
class-attribute
instance-attribute
SKELETON = True
class-attribute
instance-attribute
render(entries, context)
Source code in src/pkgforge/dbdump/_debian.py
571 572 573 574 575 576 577 578 579 580 581 582 | |
write(rendered, output, *, force=False)
As for any multi-artifact format, after refusing a directory whose
existing files this dump would replace or shadow (see the class). A
skeleton file that exists is kept unless force; rules is made
executable.
Source code in src/pkgforge/dbdump/_debian.py
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 | |