Adding a package manifest¶
A manifest is a YAML file in catalog/packages/ describing one piece of
software: what it is, why an operator would want it, how it installs on each
target, and what is known to go wrong with it. It is pure data. No manifest
contains logic, and no manifest may assume our engine is the one reading it.
This page is the conventions. When a field's meaning is the question, the
authority is src/hammunition/manifest/schema.py — every field carries a
docstring and the validators say what they refuse and why. The
schema reference is that authority rendered as a
table, generated from the models so it cannot drift. This page is for the
decisions the schema cannot make for you.
Measure it. Do not recall it.¶
The rule that catches the most mistakes, including several of ours.
Writing eleven Morse manifests in one sitting, four upstream versions were
filled in from memory and all four were wrong — aldo 0.7.7 for 0.7.8,
morse 2.5 for 2.6, morse2ascii 0.1.4 for 0.2.1, xdemorse 3.3 for 3.6.7.
Every one would have validated, loaded, and shipped. They were caught by
running the sweep and looking, which takes about forty seconds:
The same applies to whether a package exists at all. not1mm and qlog are in
the Debian Hamradio Blend, which is a Debian project, and neither is in Debian
13 — both are Kali-only among our targets. cqrlog is on every target except
Kali. Neither fact is guessable and both change what the manifest has to say.
One manifest per package, not per project¶
gpsd, gpsd-clients and gpsd-tools are three manifests. So are cw,
cwcp and xcwcp, which Debian builds from one unixcw source. The unit is
the thing a person installs and looks up by name, and a manifest that installs
three packages under one of their names is not findable under the other two.
Where the packages genuinely are one thing — a library and the program that
links it — use depends rather than a second entry in packages.
version on an apt manifest is the primary target's answer¶
An apt manifest does not choose a version; apt does, and the targets disagree.
Measured on one evening, klog was 2.4.1 on Debian 13 and Parrot, 2.4.2 on
Ubuntu 26.04, 2.5.2 on Kali and 2.3.3 on Mint. The field has to mean one of
them.
It means Parrot's, because Parrot is the primary target, falling back to
Debian 13 where Parrot does not carry the package. Record the upstream part of
the candidate — 2.5.2-5 is written "2.5.2".
This was already the practice and it was already inconsistent. ubertooth is
recorded as 2020.12.R1, which is Kali's and Parrot's; Debian 13 ships
2018.12.R1, two years older. Under the rule the entry is right, and its
known_problems already names all three — but nothing said so, and the next
person had no way to tell a considered choice from a slip.
The field is a snapshot with a stated meaning, not a promise. Debian will move underneath it and that is not a defect. There is deliberately no CI check comparing it to a live archive: a check that reddens every time Debian issues a security update is the calendar-driven failure that teaches people to ignore CI.
Quote it. An unquoted 2.5 is a YAML float, and a test rejects it.
Prefer apt. Justify anything else.¶
docs/SCOPE.md puts the Debian Blend first because it is the cheapest coverage
with the best provenance. A source build is a maintenance commitment: a pinned
tarball, a checksum, a build-dependency list that rots, and a compiler that
gets stricter every release.
So the bar for not using apt is a named, measured reason — the archive does
not carry it, or the version it carries cannot do the thing the manifest claims.
"Upstream is newer" is not a reason by itself. AHRL builds xlog from source
because 2.0.25 postdates the repository's 2.0.24, and that point release is not
worth six -Wno-* flags and a build that a future GCC will break.
Where the reason is real, say it in the install block's note, with what was
measured and when.
build_depends lists what the software needs to build — libraries, headers,
Qt. The toolchain is not the manifest's job: every source or git block
pulls in build-essential, a build_system: cmake block pulls in cmake, a
git block pulls in git, autoreconf: true pulls in
autoconf/automake/libtool, and a node block pulls in nodejs and npm.
This was learned the hard way — wsjtx never listed cmake or a compiler and
built on five targets anyway because js8call in the same profile did, until
Pop!_OS resolved js8call from apt and digital-modes died at command 27
with 'cmake' is not on PATH. Measured the same day: the Debian 13 baseline
ships no gcc, 33 of 39 compiled manifests listed build-essential by hand,
and four (fldigi, glfer, mshv, wsjtx) relied on a neighbour — mshv's
own note already records that order-dependence for a header. A manifest that
relies on its neighbours for a tool works by accident. Listing
build-essential or cmake yourself is harmless (de-duplicated), just
unnecessary. qmake is the exception: every qmake manifest names qt5-qmake
or qt6-base-dev itself and the engine does not add one.
Categories come from the vocabulary¶
catalog/categories.yaml is the controlled list. Adding a tag means adding it
there, in the same commit as the manifest that needs it — tests/test_categories.py
rejects both an undeclared tag and a declared tag nothing carries.
They are flat tags (D-003). They overlap freely and never nest, so give a manifest every tag that is true of it rather than choosing the most important one.
A launcher is named for what it does, never for the tool¶
A launchers entry becomes a file in ~/.local/bin, which comes before
/usr/bin on the operator's PATH. Name it rigctl and every terminal's
rigctl is your launcher, arguments ignored (issue #174). Put the tool's name
first and what the launcher does after it — rigctl-dummy,
hackrf_info-check, yagiuda-input — and give it a title in the D-054
shape, what it does (command), which is what the menu shows. The schema
refuses a launcher named like the bare command it runs or a binary the
manifest installs; the generator refuses a name it finds on the PATH or in
the unit's apt file list; tests/test_launcher_shadowing.py checks the whole
catalog. The rule and the repair are in
the CLI reference.
The documentation block is not optional¶
CLAUDE.md makes it a hard rule and the schema enforces the shape, but the
schema cannot tell whether what_it_does is any good. The standard is that a
licensed ham with moderate Linux experience can read the entry and know whether
they want the software, without opening a browser.
Two fields do the real work and both are commonly skimped:
why_you_want_itis not a restatement of what it does. It is what the software is for, and what it is an alternative to. "Head copy is a listening skill and it improves with hours, not exercises" tells a reader why a text-to-Morse converter matters; "converts text to Morse" does not.known_problemsis where the hours are saved. Prefer the failure that looks like something else: a USB-to-serial adapter that keys perfectly at 15 wpm and falls apart at 30 is a hardware limit that reads as a software bug, and an operator who has not been told will look in the wrong place for an evening.
Write nothing you have not checked. An unverified known problem is worse than none, because it sends people to inspect something that was never wrong.
Before you open the pull request¶
python -m pytest tests/
python -m ruff check . && python -m ruff format --check .
scripts/check_doc_links.py
The catalog tests load every manifest and report all failures rather than the first (D-016), so one run tells you everything that is wrong.
Add a changelog fragment, changelog.d/<pr-or-branch>.added.md for a new
manifest, holding the entry as one bullet that names the PR and the decision
it rests on (changelog.d/README.md). Never edit CHANGELOG.md in a pull
request: every change that did conflicted with every other, and CI fails a
pull request that changes catalog/ without a fragment.