The Amix Package Repository Format¶
An Amix package repository is a plain static file tree: one catalog text file plus a pkgs/
directory of ordinary SVR4 datastream packages ✅. There is no server-side software, no database and
no dynamic index — publishing is an rsync, and serving it needs nothing more than a stock web
server. That is deliberate: the client is a small C89 program running on a 1992 SVR4 box, and the
simplest thing that can possibly work is the thing that keeps working.
This page documents the format and the contract — the tree, the catalog grammar, the integrity
model, what the client does with them, and the gates on the publish path. For the local package
database that pkgadd maintains on the installed system (/var/sadm, contents(4), and the
stock-image defect that breaks pkgrm), see package management. For
building packages in the first place, see toolchain and packaging.
Licensing note (updated 2026-09-23). The format is open. The Amix distribution packages are Commodore material; since 2026-09-23 the Amix release restriction is lifted (Amiga Corporation's approval — see
AGENTS.md§5), so repackaged stock packages may be served publicly. The publish tooling still enforces its own refusals (below); everything else is on the operator.
The public repository: pkg.amigaux.org ✅¶
http://pkg.amigaux.org/ is the canonical Amix package repository, live since 2026-09-14 ✅.
- Plain HTTP on port 80, no redirect — permanently.
apkgspeaks HTTP/1.0 over the 1992 SVR4 sockets with no TLS (see HTTP specifics), so the repo URL ishttp://for good; an HTTPS twin serves the same tree to browsers ✅. - The hostname is the only supported form. The host is name-based virtual hosting: a request
whose
Host:header carries the bare IP gets a 404. A box with no working DNS resolves the name through one/etc/inet/hostsline mappingpkg.amigaux.orgto the host's address, andapkgstill sends the name inHost:✅. - The client's compiled-in default (no
repo=in/etc/apkg.conf) ishttp://pkg.amigaux.orgin the client source since 2026-09-14 ✅. - Contents (2026-10-14): 41 catalog entries — the 27 stock Amix 2.1 packages plus 14 of the
project's own (the
apkgclient and its engine,cdfs, nine cross-built add-on packages underpkgs/extra/— eight tools and libraries plusamix-benchmark— and twotestfixtures) ✅.
Repository layout¶
Uniformly ✅ (repo source + reproduced locally against a real staging tree).
<repo>/
├── catalog the index (see below)
├── catalog.md5 `md5sum`-format checksum line for `catalog`
└── pkgs/
├── base/ packages we build ourselves
│ ├── APKG-0.1.pkg
│ └── APKGENG-1.12.0.pkg
├── extra/ cross-built add-on packages (tools, libraries, amix-benchmark)
│ ├── gzip-1.3.12.pkg
│ └── zlib-1.2.13.pkg
├── stock/ repackaged Amix 2.1 distribution packages
│ ├── core-2.1.pkg
│ └── net-2.1.pkg
└── test/ fixtures for end-to-end client tests
└── DEMOLIB-1.0.pkg
Two rules make this work:
- The
pkgs/segment is part of the URL, not part of the catalog. A catalogfilenameofstock/net-2.1.pkgis served from<repo>/pkgs/stock/net-2.1.pkg✅. A hand-rolled probe that drops thepkgs/segment gets a 404 and mis-reads it as "the payloads were never published" — the client builds the correct URL, so only manual host-side checks fall into this trap. Verify against the client's own catalog, not a guessed path. (Also noted in the command cheat sheet.) - The subdirectories are pure metadata.
base/,extra/,stock/,test/carry no meaning to the client; they exist because thefilenamefield records a relative path, so the tree can be reorganised by regenerating the catalog ✅.
Package basenames must be unique repository-wide ✅ — the URL uses the full relative path but
the client's on-box cache is flat and keys on the basename. name-version.pkg naming makes this
true for free.
The catalog¶
Grammar (catalog format v1)¶
A UTF-8-clean ASCII text file. First line is the version banner; # lines are comments; one package
per line, pipe-delimited, no pipes in any field ✅:
| # | Field | Meaning | Notes |
|---|---|---|---|
| 1 | name |
Catalog name — what a user types | Lowercased PKG by default; overridable per package in names.conf |
| 2 | version |
Catalog version string | Compared segment-wise (below) |
| 3 | pkginst |
The SVR4 PKG instance name |
≤ 9 characters — stock pkgadd silently drops longer ones ✅ |
| 4 | filename |
Path under pkgs/, forward slashes |
May contain /; validated (below) |
| 5 | md5 |
32 hex digits, of the file as transported | i.e. of the .Z file when compressed |
| 6 | size |
Byte size of the same file | Second integrity check |
| 7 | deps |
Comma-separated catalog names, or none |
Resolved recursively |
| 8 | category |
Free text (system, application, test, …) |
Informational |
| 9 | description |
Free text; the only field where spaces are idiomatic | Searched by the client |
| 10 | offset |
Optional. Decimal byte offset into a raw slice | Only the raw-medium source uses it; a 9-field line leaves it unset and the raw source fails closed on that entry |
Real rows (from the reference staging tree) ✅:
#apkg-catalog 1
apkg|0.1|APKG|base/APKG-0.1.pkg|ea72218a454ae57700d254b16e1540b8|115200|apkgeng|application|Remote package client: update/list/search/install over HTTP
apkgeng|1.12.0|APKGENG|base/APKGENG-1.12.0.pkg|fb2f08f46c6921e979e265c36cece000|2436608|none|application|Heirloom SVR4 pkg tools: tolerant contents parser, removef/pkgask, man pages
Catalog name versus pkginst: names.conf ✅¶
The catalog carries two names per package: field 1, the catalog name a user types, and field
3, the SVR4 pkginst the engine hands to pkgadd/pkgrm and records under /var/sadm. By default
the catalog name is the pkginst lowercased (PKG=APKG → apkg). When the two must differ, the
override is an authored host-side file, names.conf, one mapping per line:
It is keyed on the pkginst — it is what produces the catalog name, so it cannot be keyed on its
own output — and the value must match [A-Za-z0-9][A-Za-z0-9+.-]* with no |, or the generator
refuses the package. The mapping above is the first real one: amix-benchmark is 14 characters,
over the 9-character pkginst limit stock pkgadd enforces by silently dropping a longer
instance, so the package installs as amixbench and is listed, searched and installed as
amix-benchmark. Nothing is renamed and no .pkg byte moves; a package absent from names.conf
keeps the default. Its siblings follow the same catalog-level pattern: deps.conf (the dependency
graph, below), descs.conf (description overrides) and cats.conf (the category taxonomy).
Parsing is defensive, and a bad catalog is rejected whole¶
The client validates field count, a 32-hex md5, a numeric size, and a
[A-Za-z0-9._+-]-plus-/ whitelist on filename — because a filename reaches a URL, a shell
pipeline and a cache path. A leading /, any . or .. component, a //, or a trailing /
rejects the entire catalog rather than the one row ✅. Nothing in a catalog can address anything
outside the pkgs/ tree.
Version comparison ✅: split on . and -; numeric segments compare numerically, otherwise
strcmp; a missing segment counts as 0. That is the rule upgrade and the already-installed skip
both use.
Dependencies are authored, not inherited¶
Stock Amix package metadata carries no dependency information at all — no depend files, no
install scripts ✅. The catalog's deps field is therefore authored, in a separate host-side
deps.conf the catalog generator reads. Two principles keep that graph honest ✅:
coreis the essential base OS and is never listed as a dependency — it is always present.- No "you need a compiler" dependencies. Which compiler (the stock AT&T
ccor the community GCC) is the user's choice, and installing a source package purely to read it is legitimate.
Integrity model¶
Today: md5 plus size, everywhere, checked before anything is mutated. ✅
| Artifact | Check |
|---|---|
catalog |
catalog.md5 sits beside it; the publisher refuses to publish a catalog that does not match its own checksum file, and re-fetches the served copy afterwards to compare |
| each package | the catalog's md5 and size, verified after staging and before pkgadd sees the file |
| a dependency chain | every member is staged and verified before the first pkgadd runs — a bad source cannot leave a half-installed chain |
The md5 covers the file as transported: for a .pkg.Z the digest is of the compressed file ✅.
Planned, not yet implemented 🟡: a sha256 field alongside md5, plus a detached signature over
the catalog (minisign/signify family). The reason it is on the roadmap rather than "nice to have" is
sequencing — retrofitting signatures after third-party clients exist never happens. (The scheme
was meant to land before the repository went public; the repository has been public since
2026-09-14 and the signature has not landed yet.) On the client side sha256 verification is a small
C addition; the catalog grammar already tolerates extra fields at the end.
The client (apkg)¶
apkg is a single C89 binary (MIT) that reads installed state straight from the stock
/var/sadm/pkg database and drives pkgadd/pkgrm non-interactively, so it coexists with the
native tools ✅.
apkg [-n] [-f] [-v] [-c conffile] [-R root] <verb> [args]
update fetch <repo>/catalog -> the local catalog (tmp + rename)
list [installed|all] catalog entries with an installed-version column
search <substr> case-insensitive match on name|description
info <name> full catalog entry + installed state
download <name> fetch <repo>/pkgs/<filename> into the cache, md5-verify
install <name>... resolve deps -> stage + verify ALL -> pkgadd each
upgrade <name> catalog newer than installed: pkgrm old, pkgadd new
remove <name> pkgrm wrapper
Exit codes ✅: 0 ok · 1 usage/config · 2 network · 3 verify (md5/size/catalog) · 4
pkgadd/pkgrm/zcat failure · 5 dependency cycle or missing dependency.
Configuration is /etc/apkg.conf, key=value, # comments — repo=, cachedir=, pkgadd=,
pkgrm=, admin=, timeout= ✅.
Three sources, one format¶
The repo= string is classified explicitly — no heuristics, and anything unrecognised is a hard
error ✅. The same catalog format serves all three:
repo= form |
Source | Catalog fetch | Package staging |
|---|---|---|---|
http://host[:port]/path |
HTTP | GET <repo>/catalog |
download to cache, then verify |
file:///cdrom, or a bare /cdrom |
mounted medium | copy <dir>/catalog |
a plain .pkg is verified in place and handed to pkgadd from the medium (no cache copy); a .pkg.Z is decompressed into the cache |
raw:/dev/rdsk/... |
raw slice (tape / install slice) | read a fixed metadata region at offset 0 | dd from the catalog's byte offset into the cache, then verify |
Staging is driven by read cost: a mounted plain package is read once, not three times. The raw
slice is self-describing — catalog at offset 0, catalog.md5 at 32768, packages at 512-aligned
offsets the catalog records — which is what a tape or a from-media install slice looks like ✅.
HTTP specifics¶
The client speaks HTTP/1.0 only, hand-rolled over BSD sockets ✅:
GETwith aHost:header (load-bearing — the reference repository is virtual-hosted) andConnection: close.- Headers parsed case-insensitively;
Content-Lengthused as a truncation check. - Follows exactly one 301/302 redirect.
- Whole-transfer
alarm()timeout, 60 s by default. - IPv4 literals are parsed locally, because the stock
gethostbyname()does not accept dotted quads (measured). - Plain
http://only — no TLS.
There is no FTP transport in the client ✅: apkg speaks HTTP, a mounted filesystem, or a raw
device — nothing else. No TLS means a public repository
must lean on the planned catalog signature for authenticity rather than on the transport.
Other behaviours worth knowing ✅¶
.Ztransparency: afilenameending in.Zis piped through the box's nativezcatinto the cache beforepkgaddsees it.- Installed state is
<root>/var/sadm/pkg/<pkginst>/pkginfoexisting; itsVERSION=is the installed version. Stock Amix packages predate that convention and read as unversioned, whichupgradetreats as older than any catalog version. pkgadd/pkgrmrun viafork/execv, not a shell — no quoting surface, exact argv logging. The child's stdin is/dev/nullso the stock 1991pkgadd's prompts read EOF and abort loudly instead of hanging forever.- An
adminfile with the interactive checks disabled is shipped alongside the client and passed aspkgadd -n -a <admin> -d <file> <pkginst>. -R <target>prefixes the install target (the "is it installed?" database andpkgadd -R) without moving the engine — the engine runs from the installer medium and installs into the target. Keep alternate-root paths short: stock SVR4libadmhas a fixed-size path buffer that smashes its stack past roughly 105 characters ✅.
Publishing¶
Publishing is rsync plus verification, in a deliberate order, behind two gates: a manual,
per-change publish and the licence gates ✅.
Gate 1: every publish is a manual, per-change action ✅¶
Publishing to pkg.amigaux.org is run by hand for each change; no automation uploads.
The publish sequence ✅¶
- Packages first, catalog second — a client must never see a catalog that references payloads that are not uploaded yet.
- The staged
catalogmust match its owncatalog.md5, or the publish refuses (that catches a stale generator run or a hand-edited catalog). - Removed packages are pruned from the server, so the tree cannot accumulate orphans.
- After the sync, the published catalog is fetched back over plain HTTP — the same path the client uses — and its md5 compared against the staged copy. A mismatch is a non-zero exit.
Stage from the live host, never from a mirror ✅: publish.sh syncs with
--delete --delete-excluded, so a staging root built from a stale copy would delete served packages. Pull the live tree down, prove it
against the live catalog (md5 and size of every entry), then add the change.
⚠ Never regenerate the catalog over a live-pulled staging root ✅¶
Catalog v1 holds one version per name, and the generator keys candidates on the catalog name
and sorts them. The live host still serves superseded APKG files (base/APKG-0.1.pkg, and
APKG-0.2.pkg too once a newer client is catalogued) that are no longer catalogued but must stay
in staging — otherwise --delete-excluded removes a file someone may still fetch. With two APKG versions present, both
map to apkg, the older one sorts first and wins, and the newer one is dropped with only a
stderr SKIP: a regenerate would silently decatalogue the live client and republish the old one.
The rule: generate the catalog with the superseded APKG files moved out of the root, move them
back, and leave a DO-NOT-REGENERATE.txt in the root saying so. Verify a staged catalog by
comparing it to the expected md5 — never by re-running the generator there. publish.sh itself is
safe: it only checks catalog.md5 and runs the scans, it builds no catalog. (A durable fix —
prefer the highest version, or an explicit pin — is a candidate, not implemented 🟡.)
Gate 2: the licence gates (mechanical, fail-closed, no --force) ✅¶
Two independent scans, deliberately kept in separate tools so neither can quietly absorb and weaken
the other. Both are fatal in publish.sh (exit 5).
The payload-licence gate (payload-licence-gate.py) answers does a package carry something the
public repository may not ship, and what stock Amix object code does it embed? ✅
- It refuses a Kickstart ROM image in any payload file, read from the content rather than the
filename: the
$1111/$1114magic word followed byJMP abs.l, the same in 16-bit byte-swapped (EPROM-burner) order, or a Cloanto-encrypted image (AMIROMTYPE1). On a set of 105 Kickstart, CDTV and A3000 boot-ROM images it caught 89; the 16 it missed are all split halves (hi/lo or even/odd chip images), and it matched none of 29 expansion-board ROMs. Not seen: split halves, a ROM inside an archive or disk image, a compressed ROM ✅. - It records, and since 2026-10-30 no longer refuses, a payload ELF that statically embeds
stock SVR4 object code — in practice the stock
libm.a(stock libm), which ports link statically. The detector is the source-file (STT_FILE) names a static link leaves behind: a distinctive marker (matherr.c,reduce.c) gives an "embeds stock Amix object code: libm" line; three or morelibmnames without one are recorded as libm objects of unnamed origin (another libm, such as musl's, trips that too) ✅. The change follows the licensing outcome above, which covers all of Amix, including its AT&T parts; the Kickstart ROM is not covered. DT_NEEDEDis informational only — dynamic linkage against a library that lives on the box redistributes nothing (the publishedapkglegitimately needslibnsl.so).- The
stock/packages are skipped by provenance, out loud — they are Commodore's own content, governed by the licensing decision above, not re-judged by the gate. - Honest limit: a stripped binary has no symbol table, so what it embeds is not recorded:
it reports UNKNOWN, never clean (
--strictturns that into a refusal), until binaries ship unstripped or attest their link line. The ROM test reads file contents and is unaffected.
The kernel-package gate (gen-catalog.py --scan-private) answers is this one of our own kernel
packages? It refuses, unconditionally, to push kernel packages ✅. Those packages carry
a whole /stand/unix plus the second-stage boot loader — our own kernel mints — and the gate's
contract is that those never reach the public catalog. It predates, and is independent of, the
2026-09-23 licensing outcome: publishing a kernel is a separate, deliberate decision, not something a
publish run may do by accident.
Two details make the gate trustworthy rather than decorative:
- It keys on
pkginfomarkers, not on pathnames. An earlier pathname-keyed version condemned stock packages, which legitimately ship/standmembers as original distribution content. The markers are parameters the kernel-package build mints for exactly this purpose, so the refusal matches kernel packages precisely. - It fails closed and has no
--force. The publisher syncs every package present in staging (not merely the catalogued ones), so a hand-added or stale-catalog kernel package would otherwise reach the server. If the scan cannot even run, the publish refuses rather than proceeding unverified.
See also¶
- Package management — the local side:
/var/sadm, thecontents(4)grammar, howpkgaddmutates the database, and the stock-image defect that breakspkgrm. - Toolchain and packaging —
pkgproto→pkgmk→pkgtrans→pkgadd, i.e. how the.pkgfiles inpkgs/are built. - Command cheat sheet — the
pkg*command table and thepkgs/-segment URL gotcha. - Filesystems and disks —
cdfs, which is what makes the mounted-medium source reach an optical disc.
Sources¶
- The amix-packagemanager repository (the reference implementation of everything on this page)
✅:
docs/design.md§2 (client verbs, config keys, catalog format v1, HTTP client, version compare), §2a (the three package sources and the self-describing raw layout) and §5 (repo tools);src/apkg/README.md(HTTP/1.0 details, catalog validation whitelist, flat-cache basename requirement,fork/execvinstall path,-Rsemantics);tools/repo/gen-catalog.py(catalog emission, the#apkg-catalog 1banner, the 9-characterPKGlimit,--rawoffsets);tools/repo/publish.sh(publish ordering, thecatalog.md5precondition, the marker-keyed private-content refusal with no--force, the post-publish HTTP re-fetch and md5 compare);tools/repo/deps.conf(the authored dependency graph and its two principles);tools/repo/names.conf(the<PKGINST>: <catalog-name>override, theamixbench→amix-benchmarkmapping and its 9-character reason);tools/repo/payload-licence-gate.py(what refuses,DT_NEEDEDinformational, thestock/provenance skip, UNKNOWN for stripped binaries; sincedccd3b1ande656915, 2026-10-30, embedded stock libm recorded rather than refused and Kickstart ROM images refused, with the 105-image detection count);docs/design.md§5.3 (the authoreddeps/cats/descs/namesfiles);src/apkg/conf.c(DEF_REPO=http://pkg.amigaux.org). - The amix-packagemanager
NOTES.md— the format and publish authority ✅: thepkg.amigaux.orgmove and its client contract (plain HTTP on port 80, name-based vhost, the hosts-file path; 2026-09-14); the payload-licence gate and why it exists (2026-09-16); thenames.conffirst use and the catalog-regenerate trap (2026-09-16 publish log); manual publishing (2026-09-16); the 39-entry live catalog, 27 stock + 12 own, and the stock packages' exposure posture (2026-09-26). - The 2026-09-23 licensing outcome —
AGENTS.md§5 ✅. - The live catalog
http://pkg.amigaux.org/catalog, read 2026-10-14 ✅: 41 rows —stock/(27),extra/(9),base/(3:APKG,APKGENG,cdfs) andtest/(2); thepkgs/extra/add-ons and their publication dates (2026-09-15, 09-16, 09-26) per thejusii/amix-packagesREADME. - A real staging tree inspected locally (2026-07-26): 32 catalog rows over
stock/(27),base/(3) andtest/(2), catalog md5edeba578b73fc892f3c8c5ede603aca7✅. - The stock-metadata facts the authored dependency graph rests on (no
dependfiles, no install scripts,CLASSES=none) — bench-verified on a live Amix 2.1c image, and recorded on the package management page ✅. - Roadmap items (sha256 + catalog signature) are planned, not implemented 🟡 — recorded here so the format's evolution is predictable, not as current behaviour.