Repository navigation
v0.2.0
Upgrading from 0.1.2
pkgforge.commonand its dict-based registries are removed. Import
PkgForgeError/UsageErrorfrompkgforge.errors;FileType,FileEntry,
FileEntryArgs,AUTO,DEFAULT,mode_to_octal,normalize_modeand the
entry functions frompkgforge.entry;PkgForgeCmd,PkgForge,parsepath
frompkgforge.command(or straight frompkgforge, which still exports
every name inpkgforge.__all__).pkgforge.db.register_provider()/
PROVIDERS/SUFFIX_FORMATS/_SNIFFERSandpkgforge.dbdump.PER_ENTRY_FORMATS/
MULTI_ARTIFACT_FORMATS/dump_formats()/PerEntryDumperare gone too --
subclassDbProvider/PerEntryFormat/MultiArtifactFormatwith your own
NAMEinstead, and look one up withDbProvider.lookup(name)/
DumpFormat.lookup(name).pkgforge.dbdump.rpmspecfile()is gone -- call
RpmSpecFiles().render_entry(path, entry).pkgforge.dbandpkgforge.dbdumpare now packages (one module per
backend/format), and so ispkgforge.install(TAR_SUFFIXES/
ARCHIVE_SUFFIXES/BSDTAR_EXTRACT_FLAGSmoved topkgforge.install.archive);
only a directimport pkgforge.db/.dbdumpexpecting a plain module needs
updating.DbProvider.formatis renamed toDbProvider.NAME.-X/--excludeon 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/scanlog a warning for a pattern that can no
longer match.PathMatchStmt.rebased()is removed -- passinstallroot=to
PathMatchinstead.**in--excludenow matches any number of directories on every Python
version; some patterns now exclude more than before -- review any-Xvalue
containing**.installcopies file sources by default instead of hardlinking them, so
-m/--chownno longer mutate the source and a build root on another
filesystem no longer fails. For the old hardlink behavior, or to have
installconsume the source, pass--method linkor--method move(env
PKGFORGE_INSTALL_METHOD).scan -mnow applies to regular files only; directories take the new
--dir-mode. Add--dir-modeif you relied on-mfor directories.- Suffix stripping without
-Tis wider:.tgz,.tbz2,.tbz,.txz,
.zip,.isoand upper-case suffixes now land atDESTINATION/app-1.0
(previously only a literal.tar/.tar.gz/.tar.bz2/.tar.xzdot-segment
was stripped). Update any script relying on the old paths. - Commands now log as
pkgforge.<command>(was<command>) -- update any
--loglevelselector, e.g.--loglevel pkgforge.scan:WARNING. Usage,
errors,--versionand shell completion now name the commandpkgforge
(wasPkgForge) -- regenerate installed completions. install,scananddbdumpnow 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_ROOTat all (pass
--buildroot /to do that on purpose), an unknown--db-format/-t/-m
value, a malformed-Xstatement, or a baddbdump -f/OUTPUT combination.
Scripts that depended on exit 1 or a traceback for these should check for
exit 2 instead.dbdump -f debianoutput gains two new sections:dirs(dh_installdirs
lines) andfixperms(a script to run fromoverride_dh_fixperms, after
dh_fixperms, applying every pinned owner/group/mode). Wire both into
debian/rulesif your packaging pipeline parsesdbdump -f debian -
directly.- Requires
duho>=0.6.0,<0.7(was>=0.5.0,<0.6) andpyyaml>=6.0,<7(was
unbounded); building from source now requireshatchling>=1.27; thedocs
extra is bounded to mkdocs 1.x/mkdocs-material 9.x/mkdocstrings below 2;
twineandhatchlingare no longer in thedevextra (install them
directly if you used them from it). DUHO_TRACEBACKnow follows duho's own boolean tokens:n/fturn it off
(they used to turn it on).
Added
dbdump -f rpmspecfiles-pre419(aliasrpm-pre419) writes%fileslines
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 -facceptsrpm/rpmspecas aliases forrpmspecfiles, and
debas an alias fordebian.debianalso writesfixperms, a POSIXshscript that applies every
pinned owner, group and mode to a package directory (sh debian/fixperms debian/<pkg>, afterdh_fixperms);dbdump -f debian -gains a
# === fixperms ===section.permissionsis unchanged.dbdump --stdinreads 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 -(orPKGFORGE_DB=-) is still write-only and
reads as an empty DB.--stdinignores--db/--db-formatentirely. 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-stalerecords a removal for each DB entry below PATH whose
file is gone from the build root, sodbdumpstops listing deleted
files. Entries matching-Xare kept. Needs a--dbfile.install --record-tree(envPKGFORGE_INSTALL_RECORD_TREE) also records
every path below a directory or archive DESTINATION that the DB does not
already hold: owner/group/-Ofrom the install, mode and type from disk
(symlinks-), honouring-X. It replaces thescan --missingstep after
a tree install; without it an install still records only DESTINATION.installgains--method {copy,link,move}(envPKGFORGE_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/--chownchange the
source too; a file/entryos.linkcan't span falls back to a copy
automatically.moveconsumes 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-sourceis a no-op withmove(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 symlinktarget) andscan's entry options. PKGFORGE_MCP=stdioserves 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(aValueError). 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=1adds the
traceback back); a closed output pipe now exits 1 silently. An unknown
--chownowner or group now raisesUsageErrorinstead of a raw
KeyError.PathMatch(stmts, root=None, installroot="/")andPathMatch.unreachable():
the former's newinstallrootkeyword names the install path a matched
entry will have; the latter lists anchored statements that can never match
under it.pkgforge.db.DbProviderandpkgforge.dbdump.DumpFormat(plus its two
subclassesPerEntryFormatandMultiArtifactFormat) are now the
documented extension points: add a backend or a dump format by
subclassing one of these with your ownNAME(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 aUsageError(exit 2,
unchanged) and, per its name, aNotImplementedError.pkgforge.dbdump.DumpError, raised for a DB entry a dump format's own
tooling cannot represent.pkgforge.DbError: a malformedjsonl/yamlfile DB fails with one
clear line naming the file (and, forjsonl, the line) instead of a
traceback.pkgforge.exclude.ExcludeSyntaxError, aUsageErrorand
argparse.ArgumentTypeError, raised for a malformed--excludestatement.entry_from_args,entry_from_path,resolve_entry,apply_entry, typed
forms of theFileEntryhelpers (entries are dicts;.resolve_for/.apply
on an entry never worked).normalize_mode(): normalizes amodevalue (anint, an octal string, or
the-/--/autosentinels) 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.scaninto asqliteDB 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.dbandpkgforge.dbdumpare now packages, one module per
backend/format (db/{jsonl,yaml,sqlite}.py,dbdump/{rpm,debian}.py),
instead of one flat module each. Import frompkgforge.db/pkgforge.dbdump
as before; only a directimport pkgforge.db(or.dbdump) expecting a
plain module, rather than a package, would need updating.DbProvider.formatis renamed toDbProvider.NAME.pkgforge.installis a package:TAR_SUFFIXES,ARCHIVE_SUFFIXESand
BSDTAR_EXTRACT_FLAGSmoved topkgforge.install.archive;
pkgforge.install.Installis unchanged.- The API reference has one page per module, covers every public name and
no longer shows private helpers. -Xmatches the install path — the/-rooted path the entry has in the
DB — on every command.installanchored an absolute pattern at the
source directory andscanat<buildroot>/PATH; a relative pattern now
also matches DESTINATION/PATH segments above the copied or scanned tree,
asdbdumpalready 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,scananddbdumptake--exclude/-Xfrom a shared
pkgforge.exclude.ExcludeArgsbase instead of each declaring it separately;
its position ininstall --helpmoves earlier (right after
--buildroot), with no other visible change.install -Xwith only file or symlink sources logs a warning (nothing to
filter); a directory or archive source is always filtered.scanlogs one INFO summary; per-pathUpdating 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 -mapplies to regular files only. Directories take the new
--dir-mode(default: from disk with--mode=--, else-); symlinks
always record mode-. If you relied on-mfor directories, add
--dir-mode.scanandinstalllook 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,.isoand upper-case
suffixes installed without-Tnow land atDESTINATION/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.dstaysconf.tar.d, neverconf). Update any
script relying on the old paths.- A tar archive writing through any symlink, even one inside the
destination, is refused (exit 1), asbsdtaralready 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
yamlbackend uses PyYAML's libyaml-backed loader/dumper when the
installed PyYAML build has them; the on-disk file format is unchanged. dbdumpwrites 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--dbkeeps 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
mainwhenever 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_TRACEBACKnow
follows duho's boolean tokens:nandfturn it off (they used to turn it
on).--loglevelaccepts[NAME:]LEVEL[,...]and rejects a malformed value
with exit 2;-v/-qgain--verbose/--quiet; log output is colored only
on a terminal and never whenNO_COLORis set. pyyamlis declared as>=6.0,<7; it was unbounded.- Building from source requires
hatchling1.27 or later. - The
docsextra is bounded to mkdocs 1.x, mkdocs-material 9.x and mkdocstrings below 2. twineandhatchlingare no longer in thedevextra; install them directly if you used them from it.- The
devextra installsblackon Python 3.10 and later. - The sdist no longer includes
benchmarks/. benchmarks/run.pytimes thescancommand end to end per DB backend
(scan.cmd_jsonl,scan.cmd_yaml,scan.cmd_sqlite,scan.cmd_auto_owner).
scan.walk, which timed onlyos.walk, is renamedfs.walk_baseline. The
result schema is documented inbenchmarks/README.md.
Removed
pkgforge.common: importPkgForgeError/UsageErrorfrompkgforge.errors;
FileType,FileEntry,FileEntryArgs,AUTO,DEFAULT,mode_to_octal,
normalize_modeand the entry functions frompkgforge.entry;PkgForgeCmd,
PkgForge,parsepathfrompkgforge.command. Names inpkgforge.__all__
still import frompkgforge.pkgforge.db.register_provider(),PROVIDERS,SUFFIX_FORMATSand
_SNIFFERS: subclassDbProviderwith your ownNAME(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()andPerEntryDumper: subclassPerEntryFormator
MultiArtifactFormatinstead; look one up withDumpFormat.lookup(name)
(list every registered name withDumpFormat.names()).pkgforge.dbdump.rpmspecfile(): useRpmSpecFiles().render_entry(path, entry)instead (from pkgforge.dbdump import RpmSpecFiles).PathMatchStmt.rebased(): passinstallroot=toPathMatchinstead.
Fixed
- The formats guide now states, measured, what rpm older than 4.19 does
with arpmspecfilesline: adquote/backslash/non-UTF-8 name fails
the build, astar/qmarkname silently packages an extra sibling file,
and abracket/bracename silently packages the wrong file -- it
previously only saidrpmspecfiles"targets rpm 4.19+" with no detail
below that version. - The formats guide's
override_dh_fixpermsexample readpermissions
fields in the wrong order (it passed the group tochmod); it now runs
the generatedfixpermsscript. - The docs no longer call
debian'spermissionsdpkg-statoverride
input (that tool takesuser group mode pathand rejects-); they now
say how to apply it from anoverride_dh_fixpermstarget, and that
dh_install's sources needPKGFORGE_ROOT=debian/tmpor
--sourcedir=$(PKGFORGE_ROOT). - The README quick start, the docs landing page and the unattended guide no
longer runscan --missingover/usror/etc: every directory a scan
walks becomes an RPM%dirownership claim, and Fedora/RHEL's
filesystempackage ships/usr/binas0555, so the built RPM
conflicted with it andrpm -Urefused to install. Passscanonly 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.*orCLAUDE*, even when built from a tree without.gitignore. examples/stage_and_package.shis executable, so./examples/stage_and_package.shruns in a fresh checkout.examples/stage_and_package.shstages its tree at/usr/share/tooland
records its files.dbdump ... -writes throughsys.stdoutinstead of a raw file
descriptor: it now works whensys.stdoutis 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.dbdumpchecks-f/OUTPUT before reading the DB: an unknown format, a
file OUTPUT fordebian, or a directory OUTPUT forrpmspecfilesnow
exits 2 with one line naming the problem (was exit 1 or a raw traceback,
possibly after loading the whole DB).rpmspecfilesescapes\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 stopsdbdumpwith an error naming the problem.debianinstalllines are escaped fordh_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 aborteddh_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
dbdumpwith an error. Non-UTF-8 names are written as their original
bytes (wasUnicodeEncodeError).debianalso writesdirs(dh_installdirslines, one per directory
entry), so an empty directory recorded withinstall -dreaches the
package (previously onlyrpmspecfiles'%dircovered it).dbdump -f debian DIRnow also writes (and replaces)DIR/dirs, and-gains a
# === dirs ===section.debian'sinstalldestination 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.scanno longer records the file DB (or a SQLite DB's
-journal/-wal/-shmsidecars) found inside the scanned tree, and
warns once instead; other files there, such as an earlierdbdump
output, are still recorded -- keep the DB and any dump output outside
--buildroot.scanof a PATH that does not exist under the build root exits 2 with
one message, instead of a rawFileNotFoundErrortraceback.scanrecords a symlink PATH as onesymlinkentry 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 -Xfilters 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-Xmatches it.**in--excludematches any number of directories on every Python
version:/**/*.pycexcludes.pycfiles at any depth,/opt/app/**
excludes everything below/opt/appbut not/opt/appitself. Some
patterns now exclude more than before; review any-Xvalue containing
**.install -Xandscan -Xread 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 filescancannot record any other way now stops it with a
one-line error naming the path and the-Xremedy, instead of a
traceback or a silently truncated DB.(?meta:k=v)ininstall -Xandscan -Xnow 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
**overridesinto the caller'sentrydict (reachable only from the
Python API, e.g. filtering a loaded DB indbdump).scan -Xdoes not descend into an excluded directory, so its contents
are no longer recorded (as withinstall). To leave only the directory
out of a manifest, exclude it indbdumpinstead:
dbdump -X '(?type:directory)/usr'.- A malformed
-Xstatement (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/--metawithout=reportsexpected KEY=VALUE, got '...'
instead ofinvalid <lambda> value. Every command's--helpnow shows
the-Xgrammar. - A directory install whose source contains the build root, destination or
file DB skips them (their parents are created empty) instead of
recursing intoRecursionError. - Tar archives holding absolute symlinks, or symlinks that climb above the
destination, extract with their targets kept exactly as written (instead
oftarfile.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 withshutil.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. installandscanexit 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/xrecords/usr/lib/x).- With no
--buildrootorPKGFORGE_ROOT,installandscanrun from/
exit 2 instead of using the live filesystem as the build root; pass
--buildroot /(orPKGFORGE_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). -dwith-t/--typeexits 2 instead of-dsilently overriding-t
and turning a file source into a bsdtar-style directory extraction.installchecks--chown's owner/group names, the--dbdirectory and
(without-p) the destination directory before staging (exit 2 with
nothing on disk); refuses--remove-sourceup front (exit 2) when a
directory source contains the resolved destination or the--dbfile;
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
installexit 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.targetno 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;installkeeps
stdin open instead of closing fd 0, and only one-source is allowed per
invocation.<(cmd),/dev/stdinand named pipes now stage as files
(previously a dangling symlink to the pipe, or a crash for a plain FIFO). -x/--decompresstakesgz,xz,bz2,zst,lzmaor a decompressor
tool name (gzip/gunzip,xz/unxz,bzip2/bunzip2,zstd/unzstd,
lzma/unlzma), matched case-insensitively; any other kind, or a bare
-xon 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 onPATHas a
command.-x gzipnow decompresses (it used to compress); a resolved
kind whose tool is missing fromPATHis named in one error line.Install(...)built directly from Python (not through the CLI parser) and
given nodecompress=argument now stages the source unchanged, instead of
silently inferring a decompressor from its suffix and running it as a
subprocess (which, for a.shsource, executed it).installcopies 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 -mon a symlink, and anyinstall -mon glibc older than 2.32
(e.g. RHEL/Rocky 8), no longer fails withNotImplementedError. A
symlink's mode is recorded but never applied on disk (Linux ignores it).--chownno 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 bothinstallandscan) is now validated before
anything is staged: it accepts 1-4 octal digits (normalized, so0644is
stored as644),-,--orauto, and rejects everything else,
including an explicit empty value, with exit 2. Previously a Python-literal
spelling like0o644was recorded verbatim and made rpm silently package
the file with mode 000, andscan -maccepted any string at all with no
check.- An empty
mode,ownerorgroupis 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/--typenow acceptsfile,directoryorsymlinkin any
case and rejects anything else with exit 2 instead of a Python traceback;
--type=--(or the Python API'sFileType._AUTO) now auto-detects from
the source instead of raisingNotImplementedError.scan --typeis
hidden from--help(scanalways records each path's own on-disk type)
and now warns instead of silently doing nothing when given a value.--helpdescribes every option, names thePKGFORGE_*variables and the
DB and dump formats, and no longer shows developer notes.- An unknown
--db-formatorPKGFORGE_DB_FORMATexits 2 with one line
naming the valid formats, before anything is staged (was a traceback and
exit 1, afterinstallhad staged the file), also when--dbis unset
(was ignored). Constructing a command with an unknowndb_formatraises
UsageError(aValueError) immediately. - Usage, errors,
--versionand completion name the commandpkgforge(was
PkgForge), so completion binds. Regenerate installed ones. PKGFORGE_ROOT/PKGFORGE_DB/PKGFORGE_DB_FORMATare now read when
pkgforge.main()(orduho.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. viamonkeypatch.setenv, or a long-lived process that
changes its own environment) is now honored; a--db/--buildroot/
--db-formaton the command line still wins over the environment. A
command constructed directly in Python, not throughmain(), 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 withunknown db format ''; it is now treated
as unset, same as an emptyPKGFORGE_DB/PKGFORGE_ROOTalready was. PkgForgeCmd.localpath()accepts astras well as aPath, and a
relative input is now taken as already build-relative instead of silently
dropping its first path component.import pkgforge,--helpand thejsonlbackend no longer require
Python'ssqlite3module; choosing thesqlitebackend on an
interpreter that lacks it fails with one clear line instead of an
ImportErrorfrom inside the stdlib. Theyamlbackend now imports
PyYAML lazily too, with the same one-line error if it's ever missing.jsonl/yamlDBs 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,
andcompactwould then make that damage permanent.- An append to a
jsonl/yamlDB whose last line lacks a trailing newline
(a hand edit, aprintf/echo -nwriter, a torn write) starts a new
line instead of fusing the new record onto the old one and silently
corrupting the DB. - The
yamlbackend reads scalars as written:mode: 0755is"0755"
(previously the int493, dumped as%attr(493,...)and packaged as
mode004). A missingmetaloads as{}, a missing
mode/owner/groupas-, and ajsonlint mode755loads as
"755". Aboolor float mode, a non-octal-digit int mode, a non-string
type, or a record that is not a mapping now raisesDbErrorinstead of
silently mispackaging or crashing later with a rawKeyError. - A flow-style YAML DB (
{/usr/bin/x: ...},{}) is still read as YAML.
Appending to one now raisesDbErrorinstead of silently corrupting it;
pkgforge --db FILE compactrewrites 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/yamlwriters (install,scan,compactitself) now serialize
through an advisory lock on the DB file, so runningcompactwhile
another pkgforge process is appending to the same DB no longer silently
drops the concurrent append.- Reading a
sqliteDB never writes to it any more: pointingdbdump(or
any read) at an empty or foreign SQLite file no longer adds anentries
table to it. A SQLite file without anentriestable now raises
DbErrornaming 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 apwd/grpentry
containing one) now raisesDbErrornaming it, instead of an
interpreter-levelUnicodeEncodeErrortraceback partway through a scan. register_provider'ssuffixesnow add a missing leading dot
("toml"registers the same as".toml"), sincePath.suffixalways
includes one and a dotless entry could never match a--dbpath before.
A suffix with more than one dot (e.g.".tar.gz") is now rejected with
ValueErrorat registration time instead of silently never matching.dbdumpnow logs a WARNING when--dbis unset,-, or names a file
that doesn't exist, and when the DB has entries but--excludeleaves
none;initdbwarns when--dbis unset or-;compactwarns for
either of those cases, and also for a--dbnaming a file that doesn't
exist yet (previously it silently created an emptyjsonl/yamlfile
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
rpmspecfilesrejects 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%filesline 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 tobsdtar, 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 -droot,
not/tmp/stage; scripts copied from the old docs should do the same.
Full Changelog: v0.1.2...v0.2.0