How pkg is built and tested, on the host, on hosted AROS and on native AROS, how it is used as a library, and the state of its pieces. For using or publishing with pkg, start at the README. Related: the container, AROS defects found while building pkg, how pkg was established, GOAL.md, OPEN.md.
On the development machine, once:
make install # pkg, pkg.h, libpkg.a and the skill into ~/.local
pkg KEYGEN FILE ~/.pkg-dev.key # the publisher key, once per publisherTo put pkg on AROS machines, build it for their CPUs (sh tools/build-aros.sh
for aarch64, sh tools/build-aros-x86_64.sh for x86_64) and make a channel
that carries it. Both compile Mbed TLS in, from third_party/mbedtls with the
configuration beside it (pkg_mbedtls_config.h: a TLS 1.2 and 1.3 client,
nothing else), which is what lets AROS read and push over https; nothing is
fetched or built apart, and the same recipe serves every CPU.
PKG_SIGNKEY=~/.pkg-dev.key make aros-channel CHANNEL=<dir>On the AROS machine, with that directory reachable (a shared folder, a disk, an image, a network share) under the name that machine gives it, one line:
Execute <ch>/Install-Pkg <ch> ; Work:channel, say
Execute DEPOT:Install-Pkg DEPOT: ; a volume that is the channel
The script finds the build that runs on that machine and it installs the
signed pkg package into SYS: (or a root given after the channel); from
then on pkg is a package like any other, upgraded with Pkg UPGRADE pkg.
tests/native-x86_64.sh does exactly this on native AROS, where the aarch64
build is tried first and does not run.
make testcc -std=c99 -Wall -Wextra -Werror. make check runs everything: the
portability grep, the unit tests, the end-to-end run, all of them again under
-fsanitize=undefined,address, and a big-endian compile of the portable core.
make test removes the binaries before rebuilding, deliberately: see the
comment in the Makefile.
tests/e2e.sh publishes a drawer, installs it into a root, lists, verifies,
edits a file, verifies again, removes, and then runs the refusals: a republished
version with different bytes, a payload with one byte flipped, a second install,
an overwrite of a file already in the root, an unknown package, an absent
version, and three channel entries forged in Python rather than by pkg: a
traversal in the manifest, a container disagreeing with its manifest, and a
payload aimed at the package database.
Its oracles are independent of the code: shasum -a 256 for every digest, cmp
for installed bytes, and Python for the forged entries.
The suite was run against three deliberate defects, each built separately:
| Defect | Result |
|---|---|
| Whole-payload digest check disabled | 1 check fails, the one pinning that refusal's message. The tampered payload is still refused, by the per-file digests, so the two layers are independent and each is exercised |
| Unsafe-path refusal disabled | 4 checks fail, including "nothing written outside the root": with the guard gone, ../evil was written outside it |
| macOS metadata filter disabled | 5 checks fail, exactly the metadata ones |
| Signature verification disabled | The altered signature installs, and its check fails. The unsigned refusal still holds, correctly, since it comes from the missing file |
| Key pinning disabled | 6 checks fail, all in the substituted-key section |
| Edited-file protection disabled | 3 checks fail, exactly the edited-file ones |
tests/status.sh (STATUS and UPGRADE ALL, 66 checks at the time; 67 since UPGRADE ALL goes as far as possible) was run the same way
against 28 deliberate defects, each on a copy of the tree: every state
misjudged in turn, the root's CPU ignored, withdrawn versions picked,
dependency order lost, the refusal handling broken,
the items not sent, DRYRUN ignored, a getchar() added, downgrades let
through, STATUS writing into the root or failing when updates exist, and
each usage refusal removed. Each made its own checks fail. The stdin check
reads the offset of a descriptor the shell shares with the command: a
program that reads its stdin moves it.
A first run of that last control reported 38 failures. The mutated binary had
been built without -Werror and its compiler output cut off, so the 38 measured
a broken build and said nothing about the filter. Rerun with the build verified
first, it gave the 5 above. Recorded because a control that fails for the wrong
reason looks exactly like one that works.
make
./build/pkg KEYGEN FILE ~/.pkg-dev.key # once
export PKG_SIGNKEY=~/.pkg-dev.key
./build/pkg PUBLISH ~/dev/MyTool CHANNEL ~/pkg-channel KIND image
./build/pkg INSTALL mytool ROOT ~/aros-root CHANNEL ~/pkg-channel
./build/pkg LIST ROOT ~/aros-root
./build/pkg VERIFY mytool ROOT ~/aros-root
./build/pkg UPGRADE mytool ROOT ~/aros-root CHANNEL ~/pkg-channel
./build/pkg STATUS ROOT ~/aros-root CHANNEL ~/pkg-channel
./build/pkg UPGRADE ALL ROOT ~/aros-root CHANNEL ~/pkg-channel
./build/pkg ROLLBACK mytool ROOT ~/aros-root CHANNEL ~/pkg-channel
./build/pkg REMOVE mytool ROOT ~/aros-rootEvery package is signed; there is no development mode. A development key
is a real Ed25519 key, written readable by its owner alone. The signature covers
the manifest, and the manifest names the payload digest, so one signature covers
every byte installed. The first install of a package into a root pins the key
that signed it, in .pkg/keys; a later install or upgrade signed by another key
is refused with both keys printed, and goes through only with ACCEPTKEY
followed by the new key in full.
UPGRADE with no VERSION takes the highest published version; with VERSION
it takes exactly that one. An older version needs DOWNGRADE. ROLLBACK returns
to the version installed before the last change, fetched again from the channel,
which never changes a published version. An upgrade that would overwrite a file
the user edited is refused before anything moves.
KIND is required on the first PUBLISH of a package; later versions take
KIND and DEPENDS from the last one published, and say so with
kind-from: and depends-from: (DEPENDS none for a version that needs
nothing). The kinds: image for a program people run, one
volume to mount; application for a program as loose files; library,
device (handlers too), class, font, catalog, startup, boot,
data, sdk, slave. A missing or unknown kind is refused with the list,
and a near miss (handler, tool) with the kind to use.
Name and version come from the $VER: cookie when NAME and VERSION are
not given. Keywords are case-insensitive, AmigaDOS style.
A channel is a directory, created by the first PUBLISH into it: index
holds one name version arch digest line per published version and CPU,
where the digest is the SHA-256 of that version's manifest. objects/
holds, per version, <manifest digest>.manifest (named by its own
SHA-256), .sig (its signature) and, once withdrawn, .withdrawn and
.withdrawn.sig; and per payload <payload digest>.pkg, named by its own
SHA-256, which the manifest's Payload: line gives. A package whose files
stay in an archive has no .pkg: its manifest's Source: names
archives/<name> in the channel. A root keeps its own database in .pkg/db, so a machine can
hold several roots without interference.
Host metadata the Amiga side has no use for, .DS_Store and AppleDouble ._
files, is left out of every package and counted in the publish report. Without
that, the same drawer would give different manifests on two Macs.
sh tools/build-aros.sh cross-builds Pkg for aarch64 AROS, and PkgHandlerRev,
a test probe that asks a mounted AFS+ volume which handler revision serves it.
make check-aros boots hosted AROS once: a package published and signed on
macOS is installed through the MacRW: share, verified, listed, removed, and a
tampered payload is refused with AmigaDOS seeing the error.
tests/aros-handler.sh is M3. It installs the real AFS+ handler with pkg, over
four boots, so that a restart is a restart:
| Boot | What happens | What the handler itself reports |
|---|---|---|
| 1 | Install revision 14, mount; upgrade to 15 while mounted | 14, then still 14: the loaded handler runs on until restart |
| 2 | Restart | 15 |
| 3 | Upgrade to a 16 that cannot load; it fails to start, and the startup sequence runs Pkg ROLLBACK |
the start fails, AmigaDOS sees it |
| 4 | Restart after the fallback | 15 |
It needs two handler packages built by AFS+'s own tools/package-aros-alpha0.sh,
at interface revisions 14 and 15:
PKG_HANDLER_V14=<dir> PKG_HANDLER_V15=<dir> sh tests/aros-handler.shFour things the hosted runs established, none of them guessed beforehand:
- Output. posixc's
stdoutreached nothing a shell redirection could see; output goes throughdos.librarytoOutput(), refusals included. - Exit codes. A refusal is
RETURN_ERROR(10), sinceIf ERRORtests for 10 and a POSIX 1 would pass as success. - errno. posixc reports no
EEXISTfor an existing directory and noENOENTfromopendiron an absent one; existence is tested, never inferred. - Formats. AFS+ changes its on-disk format without keeping legacy readers,
so a revision 14 handler refuses an image written by revision 15 tools. For
the tool this is the concrete case behind
[PKG22]item 5: rolling a handler back across a format change leaves volumes unreadable, so a package that owns an on-disk format has to declare whether its state survives a downgrade, and ROLLBACK has to honour that declaration. Not built yet; recorded as the next piece of the version model.
sh tools/build-aros-x86_64.sh builds pkg for native AROS on x86_64 with the
Homebrew LLVM (which knows the x86_64-unknown-aros triple but predefines none
of the AROS macros, so the script does) against the SDK of a nightly
linux-x86_64 system, linking with a collect-aros it builds for x86_64 from the
AROS sources.
tests/native-x86_64.sh runs goal 2 on an unmodified nightly pc-x86_64 AROS in
QEMU, nothing hosted and no host share: Guru, Function and identify.library are
taken out of the ISO itself, published on the host (the architecture read from
their ELF headers: x86_64), and the ISO is rebuilt without them, with the
channel, pkg and the sequence in S:User-Startup. From its own startup AROS
installs Guru with its dependency, runs Guru from the image mounted through pkg
MOUNTLIST with the system's own FFS (no handler component: the native system
has one), upgrades, rolls back, verifies, refuses to remove identify while Guru
needs it, and removes Guru and the orphan. Results leave through the second
serial port. 27 checks; a control shows Guru cannot open identify.library
before the root's libraries are reachable.
make build/pkg.exe cross-builds pkg for x86_64 Windows with mingw-w64;
src/pkg_fs_win32.c is the host layer. Paths stay UTF-8 inside pkg and go
through the wide API, so names outside the ANSI code page work, and the
command line is read back as UTF-16 for the same reason. Replacement is
MoveFileExW with write-through, a signing key is created with a DACL for
its owner alone from the first instant, randomness comes from
BCryptGenRandom, and output is in binary mode so a newline stays one byte.
make build/pkg-macos builds a universal binary, and
make build/pkg-linux-x86_64 or -aarch64 a static Linux one with zig.
sh tools/make-test-kit.sh writes build/pkg-test-kit.zip: pkg for the
four targets, the contract channel, tests/contract-steps.txt (the sequence
hosted AROS runs too), the reference answers, and run.ps1 for PowerShell
5.1 on Windows or PowerShell 7 anywhere. It compares every exit code and
machine output byte for byte, then checks the key's permissions (ACL on
Windows, mode 0600 elsewhere), a root named outside ASCII, and an image
written on the host against the reference bytes, and writes report.txt.
Run here with PowerShell 7 on macOS: 87 checks, 0 failures, and one altered
expectation is caught.
Not run yet: Windows and Linux. No machine of either here, and Wine's Homebrew casks were withdrawn on 2026-09-01. The owner runs the kit.
tools/aros/publish-system.sh <tree> <channel> <nightly date> publishes a
pc-x86_64 boot ISO's tree as the nine packages of
tools/aros/split-system.txt (boot, base, prefs, fonts, locale, tools,
demos, extras, sdk), versioned 0+<date>. Two files matter more than they
look: AROS.boot, which goes with aros-boot, since dos.library boots only
from a volume whose AROS.boot names its CPU; and
boot/grub/i386-pc/core.img, a configuration file, since Install-grub2
writes into it where GRUB lies on the disk: REPAIR must never put the
unpatched one back.
tests/native-system.sh checks it in QEMU from an empty disk, all of it in
a Shell window on the Workbench screen: partition, format and copy as
InstallAROS does, pkg adopting each package, GRUB, a reboot from the disk;
there VERIFY ALL, a program deleted and one overwritten by accident and
Shell-Startup edited, VERIFY ALL naming each, REPAIR ALL, and the restored
program running. PKG_DISPLAY=cocoa shows the screen while it runs; it is
recorded either way (tools/qemu-frames.py), and tools/qemu-video.sh
makes the video.
How a channel holds one version for several CPUs and how a root picks its
build is in Distributing builds for several platforms;
tests/crossarch.sh, 20 checks, proves it.
include/pkg.h is the interface; the operations live in src/pkg_lib_*.c,
one file per part of the work (output and refusals, keys, building a package,
channels, the root, the verbs, RESOLVE, STATUS, SEARCH, the interface, PUSH),
and what those files share with each other, and with nothing else, is declared
in src/pkg_internal.h; src/pkg_main.c, the pkg command, only turns words
into pkg_options and prints what comes back. The sources are listed once, in
sources.mk, for every build.
What each verb takes is written once, as a template in the manner of
AmigaDOS's ReadArgs, in the verb table of src/pkg_main.c; src/pkg_args.c
reads a command's words against it. A word a verb does not take is refused by
name. pkg HELP draws the usage from the templates, pkg HELP MACHINE prints
every verb's verb:, syntax: and template: as records, pkg <verb> ? shows
one, and PKG_CHECK_WORDS=1 judges a command's words without running it.
tests/grammar.py holds docs/reference.md and each docs/commands/ page to
the templates, and tests/test_args.c holds the reader to its rules. A graphical front end, an installer or an agent's tool
adapter links the library (make build/libpkg.a) and calls pkg_install,
pkg_show and the rest with a sink: structured, it receives the same fields
the command line prints with MACHINE; otherwise sentences for a person; and,
if it asks, a trace of every step and its progress events. The
library writes nothing to stdout and reads no environment. tests/test_api.c
drives it as a front end would; tests/test_events.c checks every event of
every operation it runs, through tests/events.sh.
examples/basic.c is the shortest useful program on libpkg: a key, a
publish, an install, a listing, and a refusal read with its next.
examples/browse.c is a package browser in the shape a graphical front end
takes: items with their fields apart, whether each version is installed,
suggestions for a mistyped name, a Cancel button, the trace. It was first
written by an agent from pkg.h alone, as a test of that header, then
adopted. examples/events.c runs an install and an upgrade on a worker
thread and shows their progress events on the main one, through a queue, as
a window would. All three build against build/libpkg.a and run in
make test (tests/examples.sh).
TRACE <file> on any command, or PKG_TRACE=<file> (- for stderr), writes
the operation's own account: every file read, written, moved or deleted,
every check with what was expected and found, every choice with its reason,
and each refusal as it happens. It never mixes into stdout.
Goals and milestones: GOAL.md. What remains: OPEN.md. Goal 1 is met: all four milestones, and the whole sequence passes as one run, tests/goal.sh, 25 checks. Goal 2 is met within its agreed line: an application arrives with its dependencies and runs, checked from outside, with no ARexx anywhere (tests/goal2.sh, 51 checks), the contract is identical on AROS, macOS and through the ARexx port (tests/aros-contract.sh, 152 checks), and the Windows, macOS and Linux kit is ready; its Windows run is the owner's.
| Piece | State |
|---|---|
.pkg container, reader and writer |
Built, with its test |
| Byte-order discipline and its checks | Built |
| SHA-256 | Built, checked against the NIST vectors |
| Text manifest | Built, strict parser, with its test |
| Host filesystem layer | POSIX, used on macOS, Linux and AROS through its posixc library |
MANIFEST, PUBLISH, INSTALL ROOT, LIST, VERIFY, REMOVE |
Built, end-to-end test on macOS |
| SHA-512 and Ed25519 | Built, checked against FIPS and the RFC 8032 vectors |
KEYGEN, SIGN, signed PUBLISH, key pinned per package in the root |
Built, end-to-end test |
UPGRADE, ROLLBACK, DOWNGRADE, EXACT and COMPATIBLE selection |
Built, end-to-end test |
| AROS client | Built with tools/build-aros.sh; make check-aros and tests/aros-handler.sh on hosted AROS |
ARexx port PKG |
Built: Pkg PORT on AROS, every verb, RESULT on success, RC the class code and LASTERROR on refusal |
Machine contract, MACHINE or PKG_OUTPUT=machine |
Built: tests/e2e.sh on macOS, tests/aros-contract.sh compares macOS and hosted AROS line for line |
Image route, KIND image and IMAGE |
Built: FFS images, validated by amitools in tests/image.sh, mounted by the AROS FFS handler in tests/goal2.sh |
Depends, resolution, orphans, REMOVE ORPHANS |
Built: tests/deps.sh on macOS, tests/goal2.sh on hosted AROS |
| Removing pkg itself, and moving to an incompatible pkg | Built: Removing pkg, proven by tests/aros-remove-pkg.sh on hosted AROS |
STATUS, UPGRADE ALL: checking and updating a root, unattended |
Built: tests/status.sh on macOS, 67 checks; on hosted AROS in the contract, AmigaDOS and ARexx |
PKG_VERSION_RELEASE in include/pkg.h is the release, and stays put until
one is declared. What a binary reports is that release, a patch number and the
day it was built: 1.8.0+20260923. The build passes the last two in, so a
binary always says which day it came from, and pkg orders them as it orders any
version: 1.8.0+20260923 comes after 1.8, a later day after an earlier one,
and a raised patch after both.
make build/pkg && ./build/pkg VERSION # pkg 1.8.0+20260923 (23.09.2026)Raise PATCH when a release changes in a way people should be able to ask for
by version; the day moves on its own. Both can be pinned, for a build that must
come out byte for byte the same as an earlier one:
make PATCH=1 BUILD=20260923 BUILDDAY=23.09.2026 build/pkg
PATCH=1 BUILD=20260923 BUILDDAY=23.09.2026 sh tools/build-aros.shA build that says nothing carries +00000000, and nothing published ever
should: tools/make-aros-channel.sh refuses it.