Skip to content

v0.3.0

Latest

Choose a tag to compare

@github-actions github-actions released this 22 Oct 01:17

Upgrading from 0.2.0

Every entry under Removed is also a break; its replacement is named there.

  • Install PyYAML for a yaml DB: pip install 'pkgforge[yaml]'.
  • Re-create every 0.2.0 file DB: a record lacks kind, caps and package, so
    loading it fails. Run pkgforge initdb and stage again.
  • Call pkgforge.cli.main(argv) where you called pkgforge.main(argv), or use
    pkgforge.Project from a build script; import the entry functions from
    pkgforge.entry and the exceptions from pkgforge.exceptions.
  • Rename JsonlDb, YamlDb, SqliteDb, RpmSpecFiles, RpmSpecFilesPre419,
    Debian and pkgforge.dbdump.Entries to JSONLinesDb, YAMLDb,
    SQLiteDb, RPMSpecFiles, RPMSpecFilesPre419, DebianFiles and
    pkgforge.FileEntries, and lookup(name) to get(name).
  • Port a dump format of your own from dump(entries, output, logger) to
    render(entries, context), check_output(output) and write(rendered, output, *, force=False).
  • Rename a third-party backend or format whose NAME, alias or suffix clashes
    with another class's: the second one raises ValueError.
  • Rewrite an absolute install/scan -X pattern that names the install path
    (-X /opt/app/build/tmp) relative to the source (-X /tmp), and
    PathMatch(stmts, installroot=...) as PathMatch(stmts, root=...).
  • Replace dbdump --stdin with dbdump --db -, DUHO_TRACEBACK with
    PKGFORGE_TRACEBACK, and -f rpmspec (the %files lines) with -f rpmspecfiles or -f rpm: rpmspec is the spec format.
  • Pass --db-format for a file DB whose suffix does not name its format: its
    content is not sniffed.
  • Run pkgforge initdb before dbdump or compact on a DB that does not exist
    yet: both exit 2 for it.
  • Record owners and groups by account name, not number, and declare every
    account outside Debian's base-passwd set in [[users]] or [[groups]] of a
    pkgforge.cfg (create = false for one that exists on the target).
  • Replace -O rpmprefix=... with -k/--kind and --caps.
  • For dbdump -f debian, add a pkgforge.cfg with [package] name, delete
    the nameless install, dirs, permissions and fixperms files the dump
    wrote into debian/, and run sh debian/fixperms debian (the debian
    directory, not the package's) from override_dh_fixperms.
  • Remove any tail after -- from a command line (pkgforge initdb -- junk):
    it is now a usage error, exit 2, where it was ignored.

Added

  • pkgforge.cfg, a TOML project file read from --config/-c FILE (before the
    subcommand), else PKGFORGE_CONFIG (an empty value means no file), else
    ./pkgforge.cfg. db, db_format and buildroot in it are defaults below the
    command line and the environment, and relative paths are relative to the
    file. ${package.name} and any other key of the file can be reused in its
    text values ($${ is a literal ${). A YAML file's !include and !load are
    refused. The whole file is validated before any command runs: a problem is one
    pkgforge: error: line, status 2. pkgforge.config holds the schema
    (ProjectConfig and one dataclass per table) and load(path); Project(..., config=, config_path=), Project.config, Project.config_path and
    Project.from_config(path) use it.

  • [package] (name, version, release, arch, summary, license,
    homepage, maintainer, description, each checked when the file is read;
    name is a lowercase package name [a-z0-9][a-z0-9+.-]+) with
    [package.rpm] requires, [package.debian] depends, section and
    priority, [package.pacman] depends and [package.apk] depends, and
    [[packages]], the subpackages built from the same source.

  • [defaults] and [[rules]] fill the mode, owner, group, kind, caps
    and package an entry leaves at - when it is recorded: the first rule whose
    match (-X statements) fits the DB path, then [defaults] (dir_mode,
    exec_mode or file_mode by type and execute bit, owner, group). A flag
    wins. install without -m stages the file with the resolved mode, and
    install --record-tree (and record_tree = true in a [[stage]] step)
    chmods each child to its resolved mode, or records the child's mode from disk
    when no rule or default gives one.

  • own in pkgforge.cfg and --own PREFIX (repeatable) name the directories a
    package owns: recording an entry at or below one also records each missing
    ancestor directory down from the prefix. dbdump --dirs owned
    (Project.dump(dirs=), own= of pkgforge.dbdump.dump) keeps only the
    directories at or below an owned prefix, so a manifest claims no directory the
    distribution owns.

  • Entry kinds and file capabilities: an entry records a kind (file,
    config, config-noreplace, doc, license or ghost; unset is file)
    and caps (one setcap clause such as cap_net_bind_service=ep), set with
    install -k/--kind and --caps, mkdir -k, symlink -k, [[rules]] and
    [[stage]]. pkgforge.entry gains FileKind, normalize_kind and
    normalize_caps. rpmspecfiles starts a line with %config,
    %config(noreplace), %doc, %license or %ghost and writes %caps(CAPS);
    dbdump -f debian writes P.conffiles (config files outside /etc),
    not-installed (ghost files, so dh_missing --fail-missing accepts them), a
    setcap line per capability in P.postinst, and ${pkgforge:Depends} in
    P.substvars naming libcap2-bin. Ghost entries are left out of install,
    dirs and fixperms.

  • An entry records the package it ships in: -P/--package NAME on install,
    mkdir and symlink, package in [[rules]] and [[stage]], else the
    [package] name. A name the configuration does not declare exits 2 before
    anything is staged; pkgforge.entry.normalize_package checks one.

  • [[users]] and [[groups]] declare the accounts the package's files belong
    to (UserAccount, GroupAccount): name, and for a user description,
    home, shell and supplementary groups; create = false marks an account
    that exists on the target. pkgforge.entry.BASE_USERS and BASE_GROUPS are
    Debian's base-passwd names, which need no declaration.

  • pkgforge sysfiles (Project.sysfiles) writes and records
    /usr/lib/sysusers.d/NAME.conf for the accounts the package creates and
    /usr/lib/tmpfiles.d/NAME.conf for its ghost directories; stage runs it.
    rpm 4.19+ and debhelper act on the files when the package is installed, so a
    file owned by a service user is staged without that user existing at build
    time. dbdump -f debian leaves an entry owned by a created account out of
    fixperms: P.postinst applies it with a guarded dpkg-statoverride --update --add (an administrator's override is kept) and P.postrm removes
    it on purge. The Debian recipes need dh $@ --with installsysusers
    (debhelper 13.10 or later).

  • pkgforge stage [--init] (Project.stage) runs the [[stage]] steps in
    order: install a source at a destination, or record a directory, an empty file
    or a symlink. Every source is checked before the first step runs.

  • pkgforge mkdir [-p] DEST... and pkgforge symlink TARGET LINK
    (Project.mkdir, Project.symlink) record a directory or a symbolic link
    that has no source.

  • pkgforge verify [-X STMT]... [-f text|json] (Project.verify, returning
    VerifyIssue tuples) checks the build root against the file DB before
    rpmbuild or dh_missing runs: a file or symlink no entry records
    (unrecorded), an entry whose path is gone (missing, never for a ghost),
    a changed type (type) and a retargeted symlink (target). It exits 1 when
    it finds any, and 2 with no file DB or no build root.

  • dbdump -f rpmspec (RPMSpec) and rpmspec-pre419 (RPMSpecPre419, for rpm
    older than 4.19) write the whole spec from pkgforge.cfg: the preamble, a
    %package -n block per [[packages]] element, a %install that runs
    pkgforge stage --init and writes one %files list per package, the %post
    that creates the tmpfiles.d paths (and, below rpm 4.19, the %pre that
    creates the accounts), %systemd_post, %systemd_preun and
    %systemd_postun_with_restart for the systemd units a package records
    (*.service, *.socket, *.timer and *.path directly in
    /usr/lib/systemd/system; they expand to nothing without systemd-rpm-macros,
    EL8: systemd), and one %files -f per package. Build it with rpmbuild -bb --define "pkgforge_config PATH" NAME.spec. It needs [package] name,
    version, summary and license and a summary per subpackage, and exits 2
    naming what is missing; a description line whose first non-blank character is
    % or # exits 2, since rpm would read it as a section, a conditional or a
    comment.

  • dbdump -f debian --skeleton (skeleton=True of Project.dump and
    pkgforge.dbdump.dump) also writes control, changelog, rules (a
    debian/tmp recipe that runs pkgforge stage and the dump) and
    source/format, with Build-Depends: debhelper-compat (= 13), dh-sequence-installsysusers and a stanza per package. It needs a directory
    OUTPUT, the path of pkgforge.cfg and [package] version, summary and
    maintainer; a ${NAME} in the summary, description, homepage or
    maintainer of a package exits 2, since dpkg-gencontrol would replace it and
    nothing escapes it.

  • A spec or skeleton file that exists is kept, with a WARNING, so edits survive
    regenerating; --force (force=True) replaces it. --force with a format
    that writes no such file, and --skeleton with a format that has no skeleton
    or without a directory OUTPUT, exit 2. DumpFormat.SKELETON and
    DumpFormat.KEEP_EXISTING mark such a format.

  • pkgforge package -f FORMAT [-o DIR] [--arch NAME] [--compression KIND]
    (Project.package, pkgforge.package.build) builds one package per declared
    package straight from the file DB and the build root, with the DB's modes and
    owner names, and prints each path:

    • deb: NAME_VERSION[-RELEASE]_ARCH.deb, without dpkg-deb, debhelper or
      fakeroot, with the control file, conffiles, md5sums and the postinst,
      prerm and postrm scripts debhelper would build from the same
      pkgforge.cfg (sysusers, tmpfiles, the systemd units, dpkg-statoverride
      for an account created at install time, setcap); --compression xz
      (default), gz, zst or none. A compressed member over 9999999999 bytes,
      more than an ar header holds, exits 1.
    • rpm: NAME-VERSION-RELEASE.ARCH.rpm through rpmbuild, from the spec
      dbdump -f rpmspec writes (rpmspec-pre419 below rpm 4.19); --arch is
      passed as --target, and without the systemd rpm macros it logs a warning
      and leaves the unit scriptlets out.
    • arch: NAME-VERSION-PKGREL-ARCH.pkg.tar.zst (or .xz, .gz), without
      makepkg or fakeroot. pacman ignores the owner names of a tar, so every
      header is root:root and .INSTALL creates the accounts and applies each
      other owner, group, setuid mode and capability.
    • apk: NAME-VERSION-rREL.apk (apk v2, unsigned: apk add --allow-untrusted), without abuild; its scripts create the accounts and
      ghost directories and set capabilities.
    • tar: NAME-VERSION[-RELEASE].tar[.gz|.xz|.zst], --compression gz,
      xz, zst or none.

    A recorded path that is missing or of another type, or whose parent is a
    symlink leading outside the build root, exits 2 before anything is written.
    The files of a hardlinked set ship as one file and hardlinks to it. Payloads
    are streamed, so memory does not grow with them (about 100 MiB of compressor
    state for xz or zstd). A build is all or nothing: files are written beside
    their final names and renamed into place once every package is built, and a
    failure (Ctrl-C included) removes what the build created. With
    SOURCE_DATE_EPOCH set, two deb, arch, apk or tar builds of one build
    root are byte-identical. A format is a PackageFormat subclass, selected by
    NAME or advertised in the pkgforge.package_formats entry-point group, that
    writes through BuildContext.writer, a PackageWriter.

  • A zstd extra (pip install 'pkgforge[zstd]') provides zstd compression
    below Python 3.14, which has compression.zstd.

  • pkgforge.Project, the Python API for build scripts: Project(buildroot, db=None, *, db_format=None, config=None, config_path=None) with init,
    load, entries, compact, install, scan, mkdir, symlink, stage,
    sysfiles, dump, verify and package. It does what the commands do,
    takes and returns plain Python values, prints nothing and returns the records
    it wrote (with no file DB, those are the only result). install(type=) takes
    a FileType and exclude= statement text. pkgforge.FileRecords and
    pkgforge.StrPath are its types.

  • pkgforge.dbdump.dump(db, fmt, output=None, *, exclude=(), own=None, config=None, config_path=None, skeleton=False, force=False) renders an
    already-loaded DB; pkgforge.dbdump.Rendered is the result type and
    pkgforge.dbdump.DumpContext the context a format renders with.

  • pkgforge.db.jsonl_line(path, entry) and pkgforge.db.parse_jsonl(text, source="text") write and read the JSON Lines records the jsonl backend
    stores.

  • dbdump --db - reads the file DB as JSON Lines from standard input, so
    pkgforge install --db - ... | pkgforge dbdump --db - -f rpm works. Stdin
    must be a pipe, a socket or a regular file: a terminal, a closed stdin or a
    device such as /dev/null exits 2 instead of dumping an empty manifest.
    compact and scan --drop-stale refuse --db - (exit 2).

  • Third-party DB backends, dump formats and package formats are found through
    the entry-point groups pkgforge.dbs, pkgforge.dump_formats and
    pkgforge.package_formats (name = module:Class): installing the package is
    enough for --db-format NAME, a --db suffix, dbdump -f NAME and package -f NAME to select it. names() includes advertised names without loading
    them.

  • pkgforge.testing: check_db_provider(cls, directory) and
    check_dump_format(cls, directory) run the checks every DB backend and dump
    format must pass, so a third-party class is tested against the same contract
    as the built-in ones. DB_PROVIDER_CHECKS and DUMP_FORMAT_CHECKS hold the
    individual checks.

  • --debug prints the traceback of an error before its one-line
    pkgforge: error: message, like PKGFORGE_TRACEBACK.

  • The example examples/pkgforge.cfg declares two packages, hello and
    hello-doc; examples/hello.spec and examples/debian/ are generated from
    it. A guide page, "From pkgforge.cfg to packages", builds them with
    rpmbuild, dpkg-buildpackage and pkgforge package, and tabulates what
    each format needs, writes and runs at install time.

  • --mode, --owner, --group, --type and --dir-mode take the next word
    as their value even when it is --, so -o -- t /l reads the owner from
    disk like --owner=--.

  • The agent help (AGENT_HELP=1 pkgforge --help) lists the exit codes 0, 1, 2
    and 130, and a failed MCP tool call reports its code, so verify with
    differences ends as exit code: 1.

Changed

  • Requires duho>=0.7.8,<0.8 (was >=0.6.0,<0.7) and anycfg>=0.15.1,<0.16,
    which reads pkgforge.cfg and is imported only when a file is read. PyYAML is
    the optional yaml extra instead of a core dependency: pip install 'pkgforge[yaml]' to keep a yaml DB, which otherwise exits 1 with a message
    naming the extra. The command line keeps its flags, its --help text and its
    0, 1 and 2 exit statuses.
  • Ctrl-C ends the command with status 130 and no output, or raises
    KeyboardInterrupt under PKGFORGE_TRACEBACK.
  • PKGFORGE_TRACEBACK replaces DUHO_TRACEBACK as the switch that prints the
    traceback before the pkgforge: error: line, and DUHO_TRACEBACK has no
    effect on pkgforge: set PKGFORGE_TRACEBACK=1 where you set
    DUHO_TRACEBACK=1. A closed output pipe is silent with status 1 either way.
  • A bad -O value reports duho's usage message, which still reads expected KEY=VALUE, got 'novalue'.
  • The console script is pkgforge.cli:main (was pkgforge:main); python -m pkgforge runs the same. pkgforge.cli is the command line, not library API:
    from Python, use pkgforge.Project.
  • The package root exports the library names only: Project, VerifyIssue,
    FileEntry, FileType, FileEntries, FileRecords, StrPath,
    DbProvider, open_db, DumpFormat, PackageFormat, the exceptions and
    __version__. Import apply_entry, entry_from_path, normalize_mode and
    resolve_entry from pkgforge.entry. import pkgforge loads neither duho
    nor the command line.
  • Every exception class is in pkgforge.exceptions (and pkgforge), not in
    pkgforge.db, pkgforge.dbdump or pkgforge.exclude.
    ExcludeSyntaxError is a UsageError only (it was also an
    argparse.ArgumentTypeError).
  • Classes with an acronym in the name spell it in capitals: JsonlDb is
    JSONLinesDb, YamlDb YAMLDb, SqliteDb SQLiteDb, RpmSpecFiles
    RPMSpecFiles, RpmSpecFilesPre419 RPMSpecFilesPre419, and the debian
    format class Debian is DebianFiles. Format and DB names, aliases and
    suffixes are the same. pkgforge.dbdump.Entries is pkgforge.FileEntries.
  • PathMatchStmt subclasses argparse.Namespace (was duho.NS), and
    PathMatch takes PathMatch(stmts, root=None): the installroot argument
    is gone, and a path outside root raises ValueError.
  • On install and scan, an absolute -X pattern is relative to the input,
    like tar --exclude, where 0.2.0 matched it against the install path:
    install -X /tmp ./build /opt/app excludes ./build/tmp, install -X '/docs/**' app-1.0.tgz /opt excludes the archive's docs/ members, and
    scan -X /cache /usr/share/app excludes /usr/share/app/cache. A pattern
    that carries the destination (-X /opt/app/build/tmp) matches nothing: drop
    that prefix. A pattern without a leading / matches at any depth, and
    dbdump matches the DB paths, as in 0.2.0.
  • -X rejects a path in microseconds for glob patterns that took seconds (eight
    ** segments, or many * in one segment), does not warn about &&, ~~ or
    || inside a [...] class, and matches faster over a large DB in dbdump.
  • A dump format implements render(entries, context), check_output(output)
    and write(rendered, output, *, force=False), where 0.2.0 had
    DumpFormat.dump(entries, output, logger) and render(entries). context is
    a pkgforge.dbdump.DumpContext holding the configuration, its path and the
    OUTPUT, and - (standard output) is handled by the dbdump command. A
    format of your own moves its dump code to write, takes the context
    argument, accepts force and drops its - case; PerEntryFormat groups the
    entries per package and its check_output accepts a directory.
  • A DB backend or dump format whose NAME, alias or suffix another class holds
    raises ValueError when the class is created and is not registered, where it
    replaced the earlier class (for a suffix, the newer class won): rename the
    new one. DbProvider.compact() drops removed paths, and the contract suite
    fails a third-party compact that keeps them.
  • A file DB record has three more fields, kind, caps and package, and the
    sqlite table their columns. A 0.2.0 DB fails to load with missing field kind; this DB was written by an older pkgforge: run initdb and record again:
    run pkgforge initdb and stage again.
  • Every DB loader (jsonl, yaml, sqlite) enforces one strict record
    schema and fails with one pkgforge: error: <file>: <path>: ... line, status
    1, where 0.2.0 coerced or defaulted: a number for a mode, owner or group, a
    missing field, a null type and an unknown field are refused. Re-record the
    DB.
  • The file DB format is --db-format, else the --db suffix, else jsonl;
    0.2.0 also read an unrecognized text file as YAML. A file whose content is
    not in that format fails to load with an error that names the format and
    suggests --db-format: pass --db-format for a file with another suffix.
  • dbdump and compact exit 2 when no DB is configured or --db names a file
    that does not exist (no file DB at PATH; run pkgforge initdb), where they
    logged a WARNING and exited 0. An existing empty DB exits 0 with a WARNING.
    Run pkgforge initdb first, or pass the right path.
  • dbdump refuses a DB entry that is not well formed, with one pkgforge: error: line naming the entry, status 2, and writes nothing: a key that is
    not an absolute path with no ./../empty segment, a type other than
    file, directory or symlink, a mode that is not 1-4 octal digits, - or
    --, an owner or group that is not an account name, - or --, and a
    kind, caps or package value that is not valid. Fix or re-record the
    entry.
  • install and scan refuse an --owner/--group that is not an account
    name ([A-Za-z_][A-Za-z0-9_.-]*, - or --), with status 2 before anything
    is staged or recorded: a numeric owner such as 1000 is refused, so use the
    account name.
  • An owner or group outside Debian's base-passwd set must be declared in
    [[users]] or [[groups]] of pkgforge.cfg, or recording it is exit 2;
    0.2.0 recorded any name. The check covers -o/-g, [defaults],
    [[rules]], [[stage]] and the names that -- reads from disk, so scan --owner=-- over files owned by an ordinary account needs that account
    declared (create = false).
  • install and scan with no --db (or --db -) print their JSON Lines
    records once the operation has succeeded, instead of one by one as they are
    written.
  • rpmspec is the name of the spec format, where 0.2.0 took it as an alias of
    rpmspecfiles: use -f rpmspecfiles (or -f rpm) for the %files lines.
    A per-entry format (rpmspecfiles, rpmspecfiles-pre419) writes the entries
    of one package into a file or -, and refuses a DB holding entries of two
    packages (exit 2); given an existing directory it writes files-PACKAGE.txt
    for every package of pkgforge.cfg.
  • dbdump -f debian writes one set of files per package, named for it, where
    0.2.0 wrote a nameless install, dirs, permissions and fixperms:
    PKG.install, PKG.dirs, PKG.conffiles, PKG.postinst, PKG.postrm and
    PKG.substvars for [package] and each [[packages]] name, and once
    not-installed and fixperms. It needs [package] name in pkgforge.cfg
    (exit 2 without it). Delete the nameless files the dump wrote into
    debian/: debhelper reads debian/PKG.NAME first and ignores the nameless
    file beside it, and compat 15 refuses it for a source with several binary
    packages. fixperms takes the debian directory, not the package's: run sh debian/fixperms debian.
  • dbdump -f debian refuses, before it writes anything (exit 2), to replace a
    hand-written debhelper file: an existing install, dirs, not-installed,
    postinst, postrm or fixperms without the line # Generated by pkgforge dbdump -f debian, a nameless install, dirs, conffiles, postinst or
    postrm that debhelper would ignore, and a PKG.conffiles listing a path the
    dump would drop. Move such lines into pkgforge.cfg or the package-named file
    and delete the stray one.

Removed

  • pkgforge.main, pkgforge.command and the pkgforge.install,
    pkgforge.scan, pkgforge.initdb and pkgforge.compact modules, with
    PkgForgeCmd, PkgForge, FileEntryArgs, FileEntryBaseArgs,
    entry_from_args, parsepath, ExcludeArgs and pkgforge.dbdump.DbDump.
    The commands are in pkgforge.cli (not library API) and the operations are
    Project methods: replace pkgforge.main(argv) with
    pkgforge.cli.main(argv), or call Project from a build script.
  • pkgforge.errors: import the exceptions from pkgforge.exceptions or
    pkgforge.
  • The pkgforge.install.archive, decompress, staging, transfer, record
    and _tool modules and the Install staging methods (_resolve, _stage
    and the like); staging is Project.install.
  • The backend submodules pkgforge.db.jsonl, pkgforge.db.yaml,
    pkgforge.db.sqlite, pkgforge.dbdump.rpm and pkgforge.dbdump.debian:
    import the classes from pkgforge.db and pkgforge.dbdump.
  • DbProvider.lookup(name) and DumpFormat.lookup(name): call get(name).
  • DumpFormat.dump(entries, output, logger): a format implements render,
    check_output and write (see Changed).
  • The FileEntry.from_args, from_path, resolve_for and apply class
    aliases: call entry_from_path, resolve_entry and apply_entry.
  • The List[str] form of normalize_mode and normalize_name, and an int
    mode in normalize_mode: both take a str, and an int raises UsageError.
  • FileType._AUTO: use the AUTO string. list(FileType) is file,
    directory, symlink.
  • PathTest.GENERATORS, PathTest.factory, FilterTestRe, filetypetest and
    metatest in pkgforge.exclude, and NAME_PATTERN, MODE_PATTERN,
    CONTROL_PATTERN and RPM_EXPANSION_PATTERN in pkgforge.entry.
  • PathMatch.unreachable(), log_unreachable() and the warning that an
    absolute -X pattern could match no install path.
  • The RPMSpecFiles._quote and RPMSpecFilesPre419._quote class hooks, which
    render never called.
  • The sniff_format function, the DbProvider.sniff hook and the for_read
    argument of open_db: open_db(path, fmt=None) resolves by format name or
    suffix only (see Changed).
  • dbdump --stdin: use --db -.
  • scan --type/-t, which was hidden and ignored: scan exits 2 for it.
  • The --decompress tool-name aliases gzip, gunzip, xz, unxz,
    bzip2, bunzip2, zstd, unzstd, lzma and unlzma: a kind is exactly
    gz, xz, bz2, zst or lzma, in any case.
  • The archive destination-name fallback that cut a name at its last tar or
    iso segment; .tar.zst, .tzst, .tar.lz4, .tar.lzma, .tlz,
    .tar.lz and .cpio are listed suffixes.
  • The permissions artifact of dbdump -f debian (and its section in
    dbdump -f debian -): nothing consumed it, and fixperms applies the same
    pins.
  • meta.rpmprefix: a -O rpmprefix=... or a meta argument holding the key
    is refused (exit 2). Use -k/--kind and --caps, or kind and caps in
    pkgforge.cfg.
  • The import-time binding of PKGFORGE_DB, PKGFORGE_DB_FORMAT,
    PKGFORGE_ROOT, PKGFORGE_INSTALL_METHOD and PKGFORGE_INSTALL_RECORD_TREE
    as class defaults: pkgforge.cli.main() reads them when it parses, and a
    command class built directly in Python ignores the environment.

Fixed

  • On Windows, a drive-qualified path (mkdir D:/x) raised ValueError: Paths don't have the same drive. It is a usage error, exit 2, saying the path
    resolves outside --buildroot.
  • install --method move of a directory onto another filesystem failed with
    Invalid cross-device link (and a rollback could fail the same way). It
    moves the tree file by file, keeping modes, mtimes and symlinks, and removes
    the source.
  • install --remove-source --method link kept a file source and removed a
    directory source. Both are removed; the staged hardlinks keep the data. The
    source is kept only when it is the destination itself.
  • A failed install -p left the parent directories it had created. It removes
    them before exiting with status 1; directories that already existed are never
    touched.
  • An Install(source=["/x"], destination="/usr/bin", ...)() command built from
    Python with str paths raised AttributeError. str and path-like values
    are accepted, and anything else raises UsageError.
  • scan --drop-stale into a sqlite DB committed once per removed row; it
    holds one connection for all its removals.
  • A %(command) or a newline in a recorded owner, group or mode reached
    rpmbuild's %files list, where it ran a command or injected a line; dbdump
    refuses the entry (exit 2).
  • A DB key such as /../../x, or a relative one, reached the debian
    fixperms script, where it chmod-ed or chown-ed a path outside the package
    directory; dbdump refuses it (exit 2).
  • An entry with no type (or -, or an unknown value) rendered as a plain
    %files path, which made rpm ship the whole subtree; dbdump refuses it.
  • A corrupt or truncated .tar, .tar.gz, .tar.xz or .tar.bz2 archive
    ended in a traceback. It is one pkgforge: error: cannot extract ... line,
    status 1.
  • A failing bsdtar or decompressor (install -x) ended in a
    CalledProcessError traceback. It is one pkgforge: error: <tool> failed (exit N): <its last message> line, status 1, and the tool reads standard
    input only when the source is -.
  • A SQLite file DB that cannot be opened, written or read (an unwritable
    directory, a locked or non-SQLite file, a schema mismatch, a corrupt
    meta_json) ended in a traceback. It is one pkgforge: error: <path>: ...
    line, status 1.
  • A JSON Lines or YAML file DB nested too deeply to parse ended in a
    RecursionError traceback. It is a one-line error naming the file.
  • Under MCP, dbdump --stdin overwrote an existing OUTPUT with an empty
    manifest and install - failed with an AttributeError, both reading the
    protocol channel. dbdump --db - and install - return an error result
    there.
  • A text DB was split on every Unicode line break, so a record holding U+2028,
    U+2029 or U+0085 failed to load, and a .jsonl starting with a byte-order mark
    was appended to as YAML. Records split on \n only and a leading BOM is
    ignored.
  • rpmspecfiles wrote a symlink's recorded mode in %attr, and rpmbuild warned
    Explicit %attr() mode not applicable to symlink; the mode is -.
  • A failed dbdump could leave a truncated or mixed OUTPUT. Each file is
    written to a sibling .<name>.pkgforge-tmp and renamed into place once every
    write has succeeded.

Full Changelog: v0.2.0...v0.3.0