Getting started
cix-cache serves precompiled Cix package artifacts over the exact URLs an
unmodified Cix host already requests, so a new box installs packages instead
of rebuilding them from source.
Point a host at it. The binaries and the recipes are two separate URLs, deliberately:
# binaries -- this server
cixctl --host=<box> pkg artifact-config set --url=http://<this-host>:8080
# recipes -- git, separately
cixctl --host=<box> pkg repo-config set --repo-url=https://git.home.arpa/itdlabs/cix.git
cixctl --host=<box> pkg sync
Watch Server activity at the bottom of this page during the first
install. A MISS naming a URL you expected to hit is
the fastest way to spot a wrong base URL — a miss is harmless by design, so
it is otherwise invisible.
Artifact names
Canonical identity is <name>-<version>-<release>,
and an omitted release means 1.
gawk-5.3.0-7 | gawk | 5.3.0 | release 7 |
mtools-4.0.49-1 | mtools | 4.0.49 | release 1 |
cix-v2.2.0-rc6-1 | cix | v2.2.0-rc6 | release 1 |
version is upstream's, verbatim — Cix never reformats it.
release is Cix's own packaging revision of that version,
and moves when the recipe changes and upstream does not.
Once a store is stamped, a name also carries the machine it was built for
— bash-5.2.37-1-x86_64 — spelled as uname -m
spells it. A checksum cannot tell an aarch64 binary from an x86_64 one, so the
name is the only thing that can. A request without an architecture resolves
while exactly one is published, and returns 409
once two are, rather than picking one.
The Arch column and the architecture chips under the search box
both appear only once the store holds more than one — a column repeating
a single value, or a filter with a single option, teaches nothing. They show up
by themselves the moment two artifacts disagree, which is exactly when reading
a row without them would be a mistake.
A name written the old way still works. A push under one is stored
canonically, and a fetch under one is served as an ALIAS
of the canonical entry — one file, one checksum, both spellings reaching
it. Aliases are logged and counted in the status bar's totals, because a thing
that silently works is otherwise invisible; that count should fall to zero as
recipes are updated.
Reading the list
Results come back most recently published first, so the list
answers “what landed”. Finding a known name is the search box's job.
Click a column heading to sort by it. Text opens A–Z; size, release and date
open largest or newest first, because that is usually why you clicked. The choice
is remembered. Versions sort by rule rather than as strings, so
2.1.10 follows 2.1.8 and v2.2.0-rc6 comes
before v2.2.0.
A row is one artifact, not one file. An artifact published in more than
one encoding — a .tar.gz and a .cixpkg of the
same identity — is one row listing both, because a format change is not a
new artifact. Size and checksum are per encoding and stack in line with the
format they belong to, and each format links to its own URL.
Columns appear only when they tell rows apart: Format once
something holds more than one encoding, Arch once two artifacts
differ by architecture, Signed once something in view carries a
signature — and likewise the architecture chips under the search box. A
column repeating one value costs width and teaches nothing, and a filter with a
single option filters nothing.
Fetching artifacts
Every artifact lives at the root of the base URL. There is no other path shape.
| GET | /<name>-<version>-<release>-<arch>.tar.gz | the artifact — also .cixpkg, .iso, or a .minisig of any of them |
| HEAD | /<name>-<version>-<release>-<arch>.tar.gz | size and digest, no body |
curl -fsSL http://<host>:8080/bc-1.08.1-2.tar.gz | sha256sum
# equals the recipe's pkg_artifact_sha256
Pull is open unless pull_token is set, because a consumer verifies
every byte against a checksum that came from git. A 404
is an ordinary answer meaning “build it from source”.
Publishing
A push declares its digest up front. The body streams to disk and is hashed
there; the name is then checked, and only a push that will be accepted is
adopted into the store. A refused one leaves nothing behind.
curl -X PUT -T bash-5.2.37-2.tar.gz \
-H "Authorization: Bearer $TOKEN" \
-H "X-Cix-Sha256: 47f08a7eac65b59ce6e6a211ffcf7754..." \
http://<host>:8080/bash-5.2.37-2.tar.gz
| 201 | published |
| 400 | body does not match the declared sha256, or the name is malformed |
| 401 | missing or wrong token — push always needs one |
| 409 | that name already exists with different bytes, or it matches artifacts for more than one architecture |
| 411 | no Content-Length |
Re-pushing identical bytes is an idempotent 201.
The 409 only fires on a real content change: a
recipe version is frozen in git, so a published name may only ever mean one byte
sequence. To genuinely replace something, DELETE the name and push
again.
DELETE /<name>.tar.gz unpublishes a name. The bytes survive until
collected, because another name may share them.
Installer ISOs
A bootable ISO is served with a detached
minisign signature beside it:
<name>-<version>-<release>-<arch>.iso and the same
name with .iso.minisig. An ISO with no signature published is refused
with 409 — publish the signature first.
Which suffixes are refused unsigned is the server's
require_signature setting, .iso by default. A store may
be configured to demand one for packages too; a signature composes with any
artifact suffix, so .cixpkg.minisig signs a package exactly as
.iso.minisig signs an installer. It does not change which tier an
artifact is in — a signed package is still a package.
curl -fsSLO http://<host>:8080/cix-installer-2.2.0-1-x86_64.iso
curl -fsSLO http://<host>:8080/cix-installer-2.2.0-1-x86_64.iso.minisig
minisign -Vm cix-installer-2.2.0-1-x86_64.iso -p cix.pub
An ISO is counted apart from packages — the menu bar grows an
installers figure, and so does cix cache status, both only
once there is one. An ISO is not a package: nothing resolves it by
name@version and no recipe stands behind it, so folding it into the
package count would make that number mean two kinds of thing.
Verify before writing the stick, on a machine you already trust.
An installer that checks its own signature is the code being checked doing the
checking — a substituted ISO reports success. The public key does not come
from this server; if it did, it would be vouching for its own payload.
Operator API
| GET | /MANIFEST.json | generated from the tree, never stored; packages and installers are separate sections |
| GET | /api/v1/status | counts, uptime, hit/miss, config summary |
| GET | /api/v1/artifacts | everything published, one record per artifact with its formats listed |
| GET | /api/v1/log?after=<seq> | activity since a sequence number |
| GET | /api/v1/gc | dry run — what collection would remove |
| POST | /api/v1/gc | collect unreferenced blobs |
| POST | /api/v1/import | migrate a static export into the store |
| GET | /api/v1/import-status | progress of a running import |
Anything marked POST needs the push token.
Command line
cix cache [--host=H] [--port=N] [--json] <command>
or: cix-cache [...] <command> (what cix dispatches to)
status server and store summary
list every published artifact, paged
(gains ARCH and SIGNED columns only when
those tell rows apart)
manifest MANIFEST.json, generated live
log [-f] what the server has been doing
gc [--dry-run] [--token=] remove blobs no name points at
import [--token=] migrate a static export
import-status progress of a running import
publish FILE --name=N --sha256=H [--token=]
delete NAME [--token=]
ls, put and rm still work, as aliases.
cixcachectl still works too, as the tool's former name.
The tool is cix-cache, which cix finds on
PATH the way git finds git-foo — so
cix cache status and cix-cache status are the same
thing. Its former name cixcachectl is installed as a symlink and
keeps working; run interactively it says once where it moved, and says nothing
at all when its output is piped.
Verbs are plain English, per the Cix CLI grammar: list,
publish, delete. The older ls,
put and rm keep working as aliases, because an
operator's muscle memory and any script already written are the real cost of a
rename.
Exit codes: 0 success, 1 the server refused or was
unreachable, 2 a usage error. List output pages through
$PAGER when stdout is a terminal and is left alone when piped.
How the store works
cache/
blobs/<sha256> the bytes, mode 0444
packages/<name>-<ver>-<rel>-<arch>.tar.gz -> ../blobs/<sha256>
tmp/ upload staging
Published names are symlinks, and the link target is the digest. So a
name's checksum and size are known without an index that could drift, and without
re-reading the file. Identical bytes under two names cost one copy — this
store holds several revisions of most packages.
Because a symlink carries no reference count, collection builds the live set by
reading every published entry and removes the blobs outside it. Unpublishing a
name therefore does not free space until a gc.
What this is not
Not a trust boundary. Recipes come from git, artifacts come from
here, and the recipe's checksum approves the bytes. A registry that served both
would be vouching for its own payload, which is worth nothing.
Not a catalogue. This listing is for operators. No Cix host code
path may consult it — a host fetches one exact name it already learned from a
recipe. Losing this store entirely must cost rebuild time and nothing else.
Not images. An image is a recipe composed of packages, so a
whole-rootfs artifact would duplicate bytes already here. Hosts compose images from
these packages.