Skip to content

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 29 Sep 10:27

Upgrading from 0.1.2

  • pkgforge.common and its dict-based registries are removed. Import
    PkgForgeError/UsageError from pkgforge.errors; FileType, FileEntry,
    FileEntryArgs, AUTO, DEFAULT, mode_to_octal, normalize_mode and the
    entry functions from pkgforge.entry; PkgForgeCmd, PkgForge, parsepath
    from pkgforge.command (or straight from pkgforge, which still exports
    every name in pkgforge.__all__). pkgforge.db.register_provider()/
    PROVIDERS/SUFFIX_FORMATS/_SNIFFERS and pkgforge.dbdump.PER_ENTRY_FORMATS/
    MULTI_ARTIFACT_FORMATS/dump_formats()/PerEntryDumper are gone too --
    subclass DbProvider/PerEntryFormat/MultiArtifactFormat with your own
    NAME instead, and look one up with DbProvider.lookup(name)/
    DumpFormat.lookup(name). pkgforge.dbdump.rpmspecfile() is gone -- call
    RpmSpecFiles().render_entry(path, entry).
  • pkgforge.db and pkgforge.dbdump are now packages (one module per
    backend/format), and so is pkgforge.install (TAR_SUFFIXES/
    ARCHIVE_SUFFIXES/BSDTAR_EXTRACT_FLAGS moved to pkgforge.install.archive);
    only a direct import pkgforge.db/.dbdump expecting a plain module needs
    updating. DbProvider.format is renamed to DbProvider.NAME.
  • -X/--exclude on every command now matches the same install path (the
    /-rooted path the entry has, or will have, in the DB) instead of a
    per-command root. Rewrite an absolute pattern written for the old anchor as
    an install path (install -X /tmp build /opt/app -> -X /opt/app/build/tmp)
    or make it relative; install/scan log a warning for a pattern that can no
    longer match. PathMatchStmt.rebased() is removed -- pass installroot= to
    PathMatch instead.
  • ** in --exclude now matches any number of directories on every Python
    version; some patterns now exclude more than before -- review any -X value
    containing **.
  • install copies file sources by default instead of hardlinking them, so
    -m/--chown no longer mutate the source and a build root on another
    filesystem no longer fails. For the old hardlink behavior, or to have
    install consume the source, pass --method link or --method move (env
    PKGFORGE_INSTALL_METHOD).
  • scan -m now applies to regular files only; directories take the new
    --dir-mode. Add --dir-mode if you relied on -m for directories.
  • Suffix stripping without -T is wider: .tgz, .tbz2, .tbz, .txz,
    .zip, .iso and upper-case suffixes now land at DESTINATION/app-1.0
    (previously only a literal .tar/.tar.gz/.tar.bz2/.tar.xz dot-segment
    was stripped). Update any script relying on the old paths.
  • Commands now log as pkgforge.<command> (was <command>) -- update any
    --loglevel selector, e.g. --loglevel pkgforge.scan:WARNING. Usage,
    errors, --version and shell completion now name the command pkgforge
    (was PkgForge) -- regenerate installed completions.
  • install, scan and dbdump now exit 2 (instead of a traceback, exit 1, or
    silently doing the wrong thing) for bad input: an out-of-buildroot
    DESTINATION/PATH, no --buildroot/PKGFORGE_ROOT at all (pass
    --buildroot / to do that on purpose), an unknown --db-format/-t/-m
    value, a malformed -X statement, or a bad dbdump -f/OUTPUT combination.
    Scripts that depended on exit 1 or a traceback for these should check for
    exit 2 instead.
  • dbdump -f debian output gains two new sections: dirs (dh_installdirs
    lines) and fixperms (a script to run from override_dh_fixperms, after
    dh_fixperms, applying every pinned owner/group/mode). Wire both into
    debian/rules if your packaging pipeline parses dbdump -f debian -
    directly.
  • Requires duho>=0.6.0,<0.7 (was >=0.5.0,<0.6) and pyyaml>=6.0,<7 (was
    unbounded); building from source now requires hatchling>=1.27; the docs
    extra is bounded to mkdocs 1.x/mkdocs-material 9.x/mkdocstrings below 2;
    twine and hatchling are no longer in the dev extra (install them
    directly if you used them from it).
  • DUHO_TRACEBACK now follows duho's own boolean tokens: n/f turn it off
    (they used to turn it on).

Added

  • dbdump -f rpmspecfiles-pre419 (alias rpm-pre419) writes %files lines
    for rpm older than 4.19 (measured on rpm 4.14, 4.16 and 4.18): the path is
    written completely unquoted, and a space, a glob character
    (* ? [ ] { }), % (any form), or a non-UTF-8 byte raises an error
    instead of packaging the wrong file. Choose it when the rpm that will
    build the package is older than 4.19; with rpm 4.19 or newer, use
    rpmspecfiles (unchanged) instead.
  • dbdump -f accepts rpm/rpmspec as aliases for rpmspecfiles, and
    deb as an alias for debian.
  • debian also writes fixperms, a POSIX sh script that applies every
    pinned owner, group and mode to a package directory (sh debian/fixperms debian/<pkg>, after dh_fixperms); dbdump -f debian - gains a
    # === fixperms === section. permissions is unchanged.
  • dbdump --stdin reads the file DB as JSON Lines from standard input
    instead of --db, e.g. install ... | pkgforge dbdump --stdin -f FORMAT
    to render a manifest without ever writing a DB file. No environment
    variable enables it, so an existing unattended script keeps its current
    behavior unchanged; --db - (or PKGFORGE_DB=-) is still write-only and
    reads as an empty DB. --stdin ignores --db/--db-format entirely. A
    terminal or already-closed stdin exits 2 immediately instead of waiting;
    otherwise it reads to EOF (blocking on a pipe that is never closed, same
    as any other stdin source). Only JSON Lines is accepted.
  • scan --drop-stale records a removal for each DB entry below PATH whose
    file is gone from the build root, so dbdump stops listing deleted
    files. Entries matching -X are kept. Needs a --db file.
  • install --record-tree (env PKGFORGE_INSTALL_RECORD_TREE) also records
    every path below a directory or archive DESTINATION that the DB does not
    already hold: owner/group/-O from the install, mode and type from disk
    (symlinks -), honouring -X. It replaces the scan --missing step after
    a tree install; without it an install still records only DESTINATION.
  • install gains --method {copy,link,move} (env PKGFORGE_INSTALL_METHOD),
    applying only to a real file/directory source (a - stdin, -x, archive or
    symlink source is unaffected). copy (the default) is unchanged. link
    hardlinks instead of copying -- fast and free of disk use, but the staged
    copy then shares the source's inode, so -m/-o/-g/--chown change the
    source too; a file/entry os.link can't span falls back to a copy
    automatically. move consumes the source (a rename, or a copy-then-delete
    across filesystems for a file; a whole-tree rename when the destination
    doesn't exist yet and there's no -X, otherwise file-by-file, leaving
    excluded files behind); a failure applying/recording the entry afterwards
    restores the source where that's still a single reversible unit.
    --remove-source is a no-op with move (the source is already gone).
  • The unattended-build guide shows how to install shell completion
    (--print-completion).
  • The command guide documents -O/--meta KEY=VALUE (rpmprefix=%config(noreplace),
    a symlink target) and scan's entry options.
  • PKGFORGE_MCP=stdio serves pkgforge's commands as MCP tools over stdio
    (from duho 0.6). Unset, nothing changes.
  • Python 3.14 classifier.
  • The header documents __version__.
  • PkgForgeError, UsageError (a ValueError). Errors now print one
    pkgforge: error: ... line and exit 2 for a usage mistake or 1 for a
    runtime failure, instead of a Python traceback (DUHO_TRACEBACK=1 adds the
    traceback back); a closed output pipe now exits 1 silently. An unknown
    --chown owner or group now raises UsageError instead of a raw
    KeyError.
  • PathMatch(stmts, root=None, installroot="/") and PathMatch.unreachable():
    the former's new installroot keyword names the install path a matched
    entry will have; the latter lists anchored statements that can never match
    under it.
  • pkgforge.db.DbProvider and pkgforge.dbdump.DumpFormat (plus its two
    subclasses PerEntryFormat and MultiArtifactFormat) are now the
    documented extension points: add a backend or a dump format by
    subclassing one of these with your own NAME (and, optionally,
    ALIASES/SUFFIXES/sniff) -- no registration call needed.
  • pkgforge.dbdump.UnsupportedOutputError: raised for OUTPUT of the wrong
    shape for the chosen dump format (a file for a multi-artifact format, or
    an existing directory for a per-entry one). Also a UsageError (exit 2,
    unchanged) and, per its name, a NotImplementedError.
  • pkgforge.dbdump.DumpError, raised for a DB entry a dump format's own
    tooling cannot represent.
  • pkgforge.DbError: a malformed jsonl/yaml file DB fails with one
    clear line naming the file (and, for jsonl, the line) instead of a
    traceback.
  • pkgforge.exclude.ExcludeSyntaxError, a UsageError and
    argparse.ArgumentTypeError, raised for a malformed --exclude statement.
  • entry_from_args, entry_from_path, resolve_entry, apply_entry, typed
    forms of the FileEntry helpers (entries are dicts; .resolve_for/.apply
    on an entry never worked).
  • normalize_mode(): normalizes a mode value (an int, an octal string, or
    the -/--/auto sentinels) to the octal permission string the file DB
    stores.
  • pkgforge.DbProvider.batch(): an optional context manager a backend can
    override to batch many writes; the default does nothing, so existing and
    third-party providers are unaffected. scan into a sqlite DB now uses
    it: one connection, committed every 1000 rows (and once more at the end),
    instead of connecting, creating the schema and committing once per file.

Changed

  • pkgforge.db and pkgforge.dbdump are now packages, one module per
    backend/format (db/{jsonl,yaml,sqlite}.py, dbdump/{rpm,debian}.py),
    instead of one flat module each. Import from pkgforge.db/pkgforge.dbdump
    as before; only a direct import pkgforge.db (or .dbdump) expecting a
    plain module, rather than a package, would need updating.
  • DbProvider.format is renamed to DbProvider.NAME.
  • pkgforge.install is a package: TAR_SUFFIXES, ARCHIVE_SUFFIXES and
    BSDTAR_EXTRACT_FLAGS moved to pkgforge.install.archive;
    pkgforge.install.Install is unchanged.
  • The API reference has one page per module, covers every public name and
    no longer shows private helpers.
  • -X matches the install path — the /-rooted path the entry has in the
    DB — on every command. install anchored an absolute pattern at the
    source directory and scan at <buildroot>/PATH; a relative pattern now
    also matches DESTINATION/PATH segments above the copied or scanned tree,
    as dbdump already did. Rewrite such patterns as install paths
    (install -X /tmp build /opt/app -> -X /opt/app/build/tmp) or as
    relative ones; an absolute pattern that can no longer match logs a
    warning.
  • install, scan and dbdump take --exclude/-X from a shared
    pkgforge.exclude.ExcludeArgs base instead of each declaring it separately;
    its position in install --help moves earlier (right after
    --buildroot), with no other visible change.
  • install -X with only file or symlink sources logs a warning (nothing to
    filter); a directory or archive source is always filtered.
  • scan logs one INFO summary; per-path Updating file entry for: lines need
    -v.
  • Commands log as pkgforge.<command> (was <command>, e.g. scan). Use the
    new name in --loglevel, e.g. --loglevel pkgforge.scan:WARNING; in
    Python, logging.getLogger("pkgforge") controls all of them.
  • scan -m applies to regular files only. Directories take the new
    --dir-mode (default: from disk with --mode=--, else -); symlinks
    always record mode -. If you relied on -m for directories, add
    --dir-mode.
  • scan and install look up user and group names only for fields set to
    --, and cache each lookup for the process's life.
  • app-1.0.tgz, .tbz2, .tbz, .txz, .zip, .iso and upper-case
    suffixes installed without -T now land at DESTINATION/app-1.0
    (previously only a literal .tar/.tar.gz/.tar.bz2/.tar.xz
    dot-segment was stripped); a directory source keeps its full name
    unchanged (conf.tar.d stays conf.tar.d, never conf). Update any
    script relying on the old paths.
  • A tar archive writing through any symlink, even one inside the
    destination, is refused (exit 1), as bsdtar already did; a hardlink
    member naming another archive member by an absolute-looking path (e.g.
    /a) links to that member, never to a real host path that happens to
    exist there; escaping members or links, and device nodes or FIFOs, exit 1
    with one message, not a traceback.
  • The yaml backend uses PyYAML's libyaml-backed loader/dumper when the
    installed PyYAML build has them; the on-disk file format is unchanged.
  • dbdump writes entries sorted by path, so a staged tree gives
    byte-identical manifests on any filesystem and backend (was insertion
    order).
  • compact (jsonl/yaml) now replaces the DB with a new inode (a temp
    file renamed into place) instead of rewriting it in place. A --db
    hardlinked elsewhere is detached from that hardlink by the next compact;
    a symlinked --db keeps its link (the link's target is what gets
    replaced), and the file's mode (and owner/group, best-effort) are carried
    over unchanged.
  • The documentation site is now rebuilt from main whenever the docs change,
    and from the release tag after each final release. A pre-release tag leaves
    it unchanged.
  • Pre-release tags (vX.Y.Z-rc.N) now produce a GitHub pre-release only and
    are no longer uploaded to PyPI. Install a pre-release from the wheel
    attached to its GitHub release.
  • Requires duho>=0.6.0,<0.7 (was >=0.5.0,<0.6). DUHO_TRACEBACK now
    follows duho's boolean tokens: n and f turn it off (they used to turn it
    on). --loglevel accepts [NAME:]LEVEL[,...] and rejects a malformed value
    with exit 2; -v/-q gain --verbose/--quiet; log output is colored only
    on a terminal and never when NO_COLOR is set.
  • pyyaml is declared as >=6.0,<7; it was unbounded.
  • Building from source requires hatchling 1.27 or later.
  • The docs extra is bounded to mkdocs 1.x, mkdocs-material 9.x and mkdocstrings below 2.
  • twine and hatchling are no longer in the dev extra; install them directly if you used them from it.
  • The dev extra installs black on Python 3.10 and later.
  • The sdist no longer includes benchmarks/.
  • benchmarks/run.py times the scan command end to end per DB backend
    (scan.cmd_jsonl, scan.cmd_yaml, scan.cmd_sqlite, scan.cmd_auto_owner).
    scan.walk, which timed only os.walk, is renamed fs.walk_baseline. The
    result schema is documented in benchmarks/README.md.

Removed

  • pkgforge.common: import PkgForgeError/UsageError from pkgforge.errors;
    FileType, FileEntry, FileEntryArgs, AUTO, DEFAULT, mode_to_octal,
    normalize_mode and the entry functions from pkgforge.entry; PkgForgeCmd,
    PkgForge, parsepath from pkgforge.command. Names in pkgforge.__all__
    still import from pkgforge.
  • pkgforge.db.register_provider(), PROVIDERS, SUFFIX_FORMATS and
    _SNIFFERS: subclass DbProvider with your own NAME (and, optionally,
    ALIASES/SUFFIXES/sniff) instead; look one up by name with
    DbProvider.lookup(name) (list every registered name with
    DbProvider.names()).
  • pkgforge.dbdump.PER_ENTRY_FORMATS, MULTI_ARTIFACT_FORMATS,
    dump_formats() and PerEntryDumper: subclass PerEntryFormat or
    MultiArtifactFormat instead; look one up with DumpFormat.lookup(name)
    (list every registered name with DumpFormat.names()).
  • pkgforge.dbdump.rpmspecfile(): use RpmSpecFiles().render_entry(path, entry) instead (from pkgforge.dbdump import RpmSpecFiles).
  • PathMatchStmt.rebased(): pass installroot= to PathMatch instead.

Fixed

  • The formats guide now states, measured, what rpm older than 4.19 does
    with a rpmspecfiles line: a dquote/backslash/non-UTF-8 name fails
    the build, a star/qmark name silently packages an extra sibling file,
    and a bracket/brace name silently packages the wrong file -- it
    previously only said rpmspecfiles "targets rpm 4.19+" with no detail
    below that version.
  • The formats guide's override_dh_fixperms example read permissions
    fields in the wrong order (it passed the group to chmod); it now runs
    the generated fixperms script.
  • The docs no longer call debian's permissions dpkg-statoverride
    input (that tool takes user group mode path and rejects -); they now
    say how to apply it from an override_dh_fixperms target, and that
    dh_install's sources need PKGFORGE_ROOT=debian/tmp or
    --sourcedir=$(PKGFORGE_ROOT).
  • The README quick start, the docs landing page and the unattended guide no
    longer run scan --missing over /usr or /etc: every directory a scan
    walks becomes an RPM %dir ownership claim, and Fedora/RHEL's
    filesystem package ships /usr/bin as 0555, so the built RPM
    conflicted with it and rpm -U refused to install. Pass scan only a
    directory your own package owns.
  • README links and badges work on PyPI and in the installed pkgforge/README.md.
  • The wheel and sdist never include files named *.local.* or CLAUDE*, even when built from a tree without .gitignore.
  • examples/stage_and_package.sh is executable, so ./examples/stage_and_package.sh runs in a fresh checkout.
  • examples/stage_and_package.sh stages its tree at /usr/share/tool and
    records its files.
  • dbdump ... - writes through sys.stdout instead of a raw file
    descriptor: it now works when sys.stdout is redirected (or otherwise not
    backed by a real file descriptor), and keeps its place after text the
    caller already printed but had not yet flushed.
  • dbdump checks -f/OUTPUT before reading the DB: an unknown format, a
    file OUTPUT for debian, or a directory OUTPUT for rpmspecfiles now
    exits 2 with one line naming the problem (was exit 1 or a raw traceback,
    possibly after loading the whole DB).
  • rpmspecfiles escapes \ and " for rpm 4.19+ and writes non-ASCII names
    as UTF-8 (was \uXXXX, which rpmbuild could not find). A control
    character in a path stops dbdump with an error naming the problem.
  • debian install lines are escaped for dh_install (\ * ? [ ] { } and a
    leading # get a backslash; space becomes ${Space} and a literal ${
    in a destination directory becomes ${Dollar}{; both need debhelper
    compat 13): before this, a space aborted dh_install, a literal ${
    aborted it too, and glob characters shipped the wrong files. A control
    character in a path, or whitespace in a mode/owner/group, now stops
    dbdump with an error. Non-UTF-8 names are written as their original
    bytes (was UnicodeEncodeError).
  • debian also writes dirs (dh_installdirs lines, one per directory
    entry), so an empty directory recorded with install -d reaches the
    package (previously only rpmspecfiles' %dir covered it). dbdump -f debian DIR now also writes (and replaces) DIR/dirs, and - gains a
    # === dirs === section.
  • debian's install destination is split with POSIX path rules on every
    platform: on Windows, a source name containing a literal backslash (a
    valid POSIX filename character) was previously split in the wrong place,
    since the native path module there also treats a backslash as a
    separator.
  • scan no longer records the file DB (or a SQLite DB's
    -journal/-wal/-shm sidecars) found inside the scanned tree, and
    warns once instead; other files there, such as an earlier dbdump
    output, are still recorded -- keep the DB and any dump output outside
    --buildroot.
  • scan of a PATH that does not exist under the build root exits 2 with
    one message, instead of a raw FileNotFoundError traceback.
  • scan records a symlink PATH as one symlink entry instead of walking
    its target: previously, scanning a link to a directory (an absolute
    target included, e.g. /etc) recorded the target's own contents under
    the link's path -- for an absolute target, the build host's
    filesystem. PATH / is unaffected and is still walked, even through a
    symlinked --buildroot.
  • install -X filters an archive source's members like a directory copy
    (every member used to be extracted regardless of -X): excluded members
    and directories never reach DESTINATION. An archive holding a device
    node, FIFO or socket is still refused even when -X matches it.
  • ** in --exclude matches any number of directories on every Python
    version: /**/*.pyc excludes .pyc files at any depth, /opt/app/**
    excludes everything below /opt/app but not /opt/app itself. Some
    patterns now exclude more than before; review any -X value containing
    **.
  • install -X and scan -X read file metadata only for a statement that
    actually has an inline test ((?type:...)/(?meta:...)), so a glob-only
    -X '*.fifo' now skips a FIFO or socket instead of failing on it; a
    special file scan cannot record any other way now stops it with a
    one-line error naming the path and the -X remedy, instead of a
    traceback or a silently truncated DB.
  • (?meta:k=v) in install -X and scan -X now sees that run's -O
    values; it never matched there before, and (?!meta:k=v) excluded every
    matching path regardless of -O.
  • PathMatch.match(path, entry, **overrides) no longer writes the
    **overrides into the caller's entry dict (reachable only from the
    Python API, e.g. filtering a loaded DB in dbdump).
  • scan -X does not descend into an excluded directory, so its contents
    are no longer recorded (as with install). To leave only the directory
    out of a manifest, exclude it in dbdump instead:
    dbdump -X '(?type:directory)/usr'.
  • A malformed -X statement (an unknown inline test, an invalid
    (?type:...)/(?meta:...) argument, or an unterminated (?...) now
    exits 2 naming the problem, instead of a traceback (unknown test) or
    silently becoming a literal glob that excluded nothing (an unterminated
    test). -O/--meta without = reports expected KEY=VALUE, got '...'
    instead of invalid <lambda> value. Every command's --help now shows
    the -X grammar.
  • A directory install whose source contains the build root, destination or
    file DB skips them (their parents are created empty) instead of
    recursing into RecursionError.
  • Tar archives holding absolute symlinks, or symlinks that climb above the
    destination, extract with their targets kept exactly as written (instead
    of tarfile.AbsoluteLinkError/LinkOutsideDestinationError), and
    re-extract cleanly over an earlier run instead of failing.
  • Re-running a directory install whose source holds symlinks no longer
    fails with shutil.Error; a stale symlink left at the destination
    (including a directory-source's own symlink retargeted between runs) is
    replaced instead, and a regular-file source is never written through a
    leftover destination symlink.
  • install and scan exit 2 when DESTINATION or PATH leaves the build root
    through a .. that climbs above it, or a symlinked path component leading
    outside it, instead of writing or recording outside --buildroot. An
    in-root .. is normalized (/usr/share/../lib/x records /usr/lib/x).
  • With no --buildroot or PKGFORGE_ROOT, install and scan run from /
    exit 2 instead of using the live filesystem as the build root; pass
    --buildroot / (or PKGFORGE_ROOT=/) to do that on purpose.
  • A file install no longer fails on macOS for a source with a BSD file flag
    set (e.g. a system binary), where copying the source's flags along with
    its content used to raise a permission error even though the source is
    only read. The staged copy carries the source's content, permission bits
    and modification time; it never carries file flags or extended
    attributes (e.g. an SELinux label).
  • -d with -t/--type exits 2 instead of -d silently overriding -t
    and turning a file source into a bsdtar-style directory extraction.
  • install checks --chown's owner/group names, the --db directory and
    (without -p) the destination directory before staging (exit 2 with
    nothing on disk); refuses --remove-source up front (exit 2) when a
    directory source contains the resolved destination or the --db file;
    and removes the source only after the entry is recorded, never when it
    is itself the staged destination (which is now skipped with a warning
    instead of deleting the only copy).
  • A failed install leaves no empty or partial file (or extracted directory)
    behind, and keeps an earlier staged copy at that destination exactly as
    it was, instead of truncating or clobbering it. Re-running a symlink
    install now replaces the link (including a dangling in-place link, and a
    target retargeted between runs) instead of failing with
    FileExistsError; a stale host symlink at the destination is replaced by
    the real staged file instead of being silently kept.
  • Sources resolving to one non-directory destination in a multi-source
    install exit 2 before staging instead of the last one silently
    overwriting the earlier ones (directory and archive sources sharing a
    destination still merge, as before). A symlink source's recorded
    meta.target no longer leaks into every entry recorded after it, in the
    same install, in a later Python-API call, or in a later CLI invocation
    built from the same process.
  • A - source on a terminal or closed stdin, or a terminal character-device
    path, exits 2 instead of staging an empty file or waiting; install keeps
    stdin open instead of closing fd 0, and only one - source is allowed per
    invocation. <(cmd), /dev/stdin and named pipes now stage as files
    (previously a dangling symlink to the pipe, or a crash for a plain FIFO).
  • -x/--decompress takes gz, xz, bz2, zst, lzma or a decompressor
    tool name (gzip/gunzip, xz/unxz, bzip2/bunzip2, zstd/unzstd,
    lzma/unlzma), matched case-insensitively; any other kind, or a bare
    -x on a source suffix that names none of them, exits 2 before anything
    is staged, instead of silently compressing the file, leaving the old
    suffix on the destination, or running an arbitrary word on PATH as a
    command. -x gzip now decompresses (it used to compress); a resolved
    kind whose tool is missing from PATH is named in one error line.
  • Install(...) built directly from Python (not through the CLI parser) and
    given no decompress= argument now stages the source unchanged, instead of
    silently inferring a decompressor from its suffix and running it as a
    subprocess (which, for a .sh source, executed it).
  • install copies file sources instead of hardlinking them: -m/--chown
    no longer change the source, and a build root on another filesystem (e.g.
    a tmpfs /tmp) or a source the caller doesn't own no longer fails.
    -o --/-g -- (AUTO) now record the staged copy's owner, not the
    source's.
  • install -m on a symlink, and any install -m on glibc older than 2.32
    (e.g. RHEL/Rocky 8), no longer fails with NotImplementedError. A
    symlink's mode is recorded but never applied on disk (Linux ignores it).
  • --chown no longer strips an existing setuid/setgid bit from the staged
    file: owner/group are now applied before mode, and a setuid/setgid/sticky
    bit already on disk is restored if the entry leaves mode at its default.
  • -m/--mode (on both install and scan) is now validated before
    anything is staged: it accepts 1-4 octal digits (normalized, so 0644 is
    stored as 644), -, -- or auto, and rejects everything else,
    including an explicit empty value, with exit 2. Previously a Python-literal
    spelling like 0o644 was recorded verbatim and made rpm silently package
    the file with mode 000, and scan -m accepted any string at all with no
    check.
  • An empty mode, owner or group is now treated like - (the documented
    default) everywhere it is read: applying the entry, and both the RPM and
    Debian dump formats. Previously the RPM format emitted %attr(,-,-), which
    rpmbuild rejects, for an empty mode.
  • install -t/--type now accepts file, directory or symlink in any
    case and rejects anything else with exit 2 instead of a Python traceback;
    --type=-- (or the Python API's FileType._AUTO) now auto-detects from
    the source instead of raising NotImplementedError. scan --type is
    hidden from --help (scan always records each path's own on-disk type)
    and now warns instead of silently doing nothing when given a value.
  • --help describes every option, names the PKGFORGE_* variables and the
    DB and dump formats, and no longer shows developer notes.
  • An unknown --db-format or PKGFORGE_DB_FORMAT exits 2 with one line
    naming the valid formats, before anything is staged (was a traceback and
    exit 1, after install had staged the file), also when --db is unset
    (was ignored). Constructing a command with an unknown db_format raises
    UsageError (a ValueError) immediately.
  • Usage, errors, --version and completion name the command pkgforge (was
    PkgForge), so completion binds. Regenerate installed ones.
  • PKGFORGE_ROOT/PKGFORGE_DB/PKGFORGE_DB_FORMAT are now read when
    pkgforge.main() (or duho.parse) runs, instead of once when the package
    is imported. A build driver or test that sets one of these after importing
    pkgforge (e.g. via monkeypatch.setenv, or a long-lived process that
    changes its own environment) is now honored; a --db/--buildroot/
    --db-format on the command line still wins over the environment. A
    command constructed directly in Python, not through main(), is
    unaffected and keeps using the value from import time, as before.
  • An empty PKGFORGE_DB_FORMAT (a common CI idiom for "unset") no longer
    makes every DB command fail with unknown db format ''; it is now treated
    as unset, same as an empty PKGFORGE_DB/PKGFORGE_ROOT already was.
  • PkgForgeCmd.localpath() accepts a str as well as a Path, and a
    relative input is now taken as already build-relative instead of silently
    dropping its first path component.
  • import pkgforge, --help and the jsonl backend no longer require
    Python's sqlite3 module; choosing the sqlite backend on an
    interpreter that lacks it fails with one clear line instead of an
    ImportError from inside the stdlib. The yaml backend now imports
    PyYAML lazily too, with the same one-line error if it's ever missing.
  • jsonl/yaml DBs are read and written as UTF-8 under any locale;
    before, a DB containing literal (non-ASCII-escaped) UTF-8 -- a hand edit
    or a third-party writer -- could be mis-decoded under a non-UTF-8 locale,
    and compact would then make that damage permanent.
  • An append to a jsonl/yaml DB whose last line lacks a trailing newline
    (a hand edit, a printf/echo -n writer, a torn write) starts a new
    line instead of fusing the new record onto the old one and silently
    corrupting the DB.
  • The yaml backend reads scalars as written: mode: 0755 is "0755"
    (previously the int 493, dumped as %attr(493,...) and packaged as
    mode 004). A missing meta loads as {}, a missing
    mode/owner/group as -, and a jsonl int mode 755 loads as
    "755". A bool or float mode, a non-octal-digit int mode, a non-string
    type, or a record that is not a mapping now raises DbError instead of
    silently mispackaging or crashing later with a raw KeyError.
  • A flow-style YAML DB ({/usr/bin/x: ...}, {}) is still read as YAML.
    Appending to one now raises DbError instead of silently corrupting it;
    pkgforge --db FILE compact rewrites it in block style first.
  • compact (jsonl/yaml) no longer truncates the DB before writing the
    compacted result: a failure partway through (a full disk, a kill) now
    leaves the original file completely unchanged instead of a torn or
    emptied one.
  • jsonl/yaml writers (install, scan, compact itself) now serialize
    through an advisory lock on the DB file, so running compact while
    another pkgforge process is appending to the same DB no longer silently
    drops the concurrent append.
  • Reading a sqlite DB never writes to it any more: pointing dbdump (or
    any read) at an empty or foreign SQLite file no longer adds an entries
    table to it. A SQLite file without an entries table now raises
    DbError naming the file, unless it has no tables at all, in which case
    it loads as an empty DB. A path, or a mode/owner/group/type field, that
    is not valid UTF-8 (e.g. an undecodable file name, or a pwd/grp entry
    containing one) now raises DbError naming it, instead of an
    interpreter-level UnicodeEncodeError traceback partway through a scan.
  • register_provider's suffixes now add a missing leading dot
    ("toml" registers the same as ".toml"), since Path.suffix always
    includes one and a dotless entry could never match a --db path before.
    A suffix with more than one dot (e.g. ".tar.gz") is now rejected with
    ValueError at registration time instead of silently never matching.
  • dbdump now logs a WARNING when --db is unset, -, or names a file
    that doesn't exist, and when the DB has entries but --exclude leaves
    none; initdb warns when --db is unset or -; compact warns for
    either of those cases, and also for a --db naming a file that doesn't
    exist yet (previously it silently created an empty jsonl/yaml file
    in that case; now nothing is created, for every backend). Exit codes and
    output are unchanged -- a missing/unset/- DB has always read as empty;
    this just makes that visible instead of silent.

Security

  • rpmspecfiles rejects a path containing %: rpmbuild expanded it, so a
    staged %(...) name ran a shell command, and no quoting is literal on rpm
    both before and after 4.19; write that %files line by hand.
  • Archives extracted with bsdtar (stdin, .zip, .iso, .cpio) no
    longer restore owners, setuid/setgid, group/other write, xattrs, ACLs or
    file flags as root; an archive holding a device node, FIFO or socket is
    refused (exit 1).
  • Without tarfile's extraction filter (Python before 3.9.17, 3.10.12 or
    3.11.4), a tar archive now routes to bsdtar, or is refused (exit 1),
    instead of extracting with no path/symlink/special-file checks at all.
  • The quick start and unattended guide build in a private mktemp -d root,
    not /tmp/stage; scripts copied from the old docs should do the same.

Full Changelog: v0.1.2...v0.2.0