Decision note 0017: owning fab, and how a package this project depends on is named #18
No reviewers
Labels
No labels
accessibility
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Keel-Linux/handbook!18
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "docs/decision-0017-keel-fab"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #17.
Decision note 0017, and only 0017: 0014, 0015 and 0016 are concurrent work, so the number was taken deliberately rather than as "the next free one". 0011 stays a gap, per the record in #11.
What it records
The maintainer's decision of 2026-09-28 in Keel-Linux/fab#8, and the reasoning that decides the shape rather than assuming it. The rule it settles outlives fab: a Debian package this project depends on and modifies becomes a Keel package with a version this project chooses, not upstream's version with a
+keelNsuffix. For fab that iskeel-fabat2.0.0.Implementation is Keel-Linux/fab#9, which closes fab#8 and fab#7. Not merged, and the build host has not been converted.
Three things the note adds beyond the issue
It corrects the record on what
1.1.1+keel2was. Keel-Linux/fab#7 states that the version exists in no commit and implies the installed files might diverge from git. Both were measured and both are wrong. Every file the package ships is in git: comparing the extracted.debagainste610377, 22 of 25 byte identical and the three that differ do so only indh_python3's shebang normalisation. And the changelog entry merged as fab#6 at 2026-09-27T19:58:53Z, ten hours before fab#7 was filed;git log -- debian/changelogonmasterhas returned two commits since then. fab#7 was read from a stale ref: the build host clone had not fetched since before fab#6, so itsorigin/masterpredates the entry while itsHEADcarries it.That matters for the note because it changes what the defect is. It was never the missing entry. It was that a version string was the only provenance there was, and neither packaged version was tagged at all, so
core.manifest'sfab_version 1.1.1+keel1named no commit. The note says that plainly instead of repeating the issue.It separates three axes that were being conflated. The repository name (0006 rules on it, keeps
fab, and stands), the Debian package name (renamed, and this is the first divergence between the two, so it is stated rather than inferred), and the command names (unchanged, decided by 380 call sites of which not one reads the package name). It also states why decision 0015 does not reach these commands: a build tool is not operator facing, and extending 0015 to it would rename 380 call sites to no operator's benefit inside a change whose point is that the build host keeps building.It argues the name from the tooling rather than from taste.
apt/lib/build.shclassifies a package as native when theSourcestarts withkeel, the clone has noupstreamremote and neitherdebian/watchnordebian/upstream/metadataexists. The fab clone already satisfied the last two, sokeel-fabmakesbin/build-packageaccept0.1.0on its own and the+keelNguard correctly stops applying, with nobody passing--native. The rule that selects the version scheme is keyed on the prefix, so a package whose scheme is ours has to carry the prefix or the policy and the code disagree.The version number was got wrong first, and the note records how
The note argued
0.1.0, matching the scheme every package this project already owns starts at. The changelog gate wired up in Keel-Linux/fab#9 refused it on its first run and was right:dpkg --compare-versions 0.1.0 gt 1.1.1+keel2is false,require-changelogcompares exactly that way, andrepreproanddpkg-genchangesread a changelog as one monotonic series too. A rename does not give the changelog a fresh start.So the note carries the rule the next package to move needs: a package that becomes ours takes the next version above the highest it has already published, chosen by what the change means, and only a package with no predecessor starts at 0.1.0. For fab that is
2.0.0, which also stops0.xclaiming that the thing building every layer in production is pre-release.This is recorded rather than quietly corrected because it was not obvious by reasoning; a gate caught it, and the gate only existed because the same pull request wired it up.
The
Provides/Replaces/Conflictsreasoning, for tracker#13's benefitThis is the first such triple in the organization's packaging, so the note records what each field is for.
Conflictsis load bearing andReplaceshands the paths over.Providessatisfies a dependency on the name and nothing more: it does not protect an install by name while TurnKey's archive offers a realfab(apt removeskeel-fabfor it, reproduced 2026-09-29), and a fab plan cannot resolve it at all (tkldev#4). The negative pinPackage: fab/Pin: release o=turnkeylinux/Pin-Priority: -1is what protects the host;Pin: origin ""only matches a local repository and does not. So apt#15 is a prerequisite of the conversion, and tracker#13 needs the same pin per replaced name.A versioned
Provides: fab (= 1.1.1+keel2)is recorded as considered and rejected: it would satisfy a versioned dependency that exists nowhere, at the cost of writing upstream's version number back into the package whose point is not to carry one.Also in this pull request: one new trap
docs/traps.mdgains "Renaming a Debian package drops whatever debhelper found by its name". debhelper keys.install,.linksand.docson the binary package name, anddh_python3finds a private python directory the same way, so a rename silently drops whatever was being picked up by name and the failure surfaces at first use. Naming the directory back has its own trap:dh_python3 <dir>processes that directory instead of its default pass, and the default pass is what moves a module off/usr/lib/python3.13/dist-packagesonto the version independent path, so one call with the argument trades a missing registration for a module pinned to one python version. Both calls are needed.It was found by building the old and new packages in a container and diffing them. Every static check on
debian/passed throughout and said nothing, which is the part worth writing down. The entry carries the version lesson as a second paragraph, since it has the same root, a rename, and was likewise found by a tool rather than by reading.The other trap is left alone on purpose. #13 is adding "two machines answer to tkldev, and they run different fab", and this work hit that trap in its other form: a claim measured from the wrong clone reads exactly like one measured from the wrong machine. The note says so and says the fix needs one word added, "name the clone and its head, not only the machine", but leaves the amendment to #13 so the two do not collide in the same file.
Test plan
Date,Status,## Decision, the three part justification of brief section 10,## Consequences. It also carries## What this does not decide,## Traps found while writing thisand## Implementation, following 0016.docs/traps.md. No index to update;docs/decisions/has none.keel-faband2.0.0, which is the part fab#9 cannot land without.Related
fab_commitin the manifest, after fab#9.This repository ships no package and has no
package / changeloggate, so there is no changelog entry to add and no checks are reported on the branch.Review of #18
Reviewed alongside Keel-Linux/fab#9, where the implementation findings are. This comment is about the note as a record: whether it argues its choices, and whether what it states is true.
It argues rather than records. "Why 2.0.0 and not 0.1.0" carries the measurement that refused the first answer and the rule it generalises to. "Why
keel-fabis the name the tooling already wanted" derives the name fromapt/lib/build.shinstead of from taste, and I confirmed the three conditions: the clone has onlyorigin, and neitherdebian/watchnordebian/upstream/metadataexists, soSource: keel-fabis the one that flips the classification. The versionedProvidesis recorded as considered and rejected with the reason. "What this does not decide" draws the boundary in four places. The three-part justification does the work brief section 10 asks of it rather than restating the decision three times. Recording that the version number was got wrong first, and that a gate rather than reasoning caught it, is the part that will be worth most to the next reader.The findings below are all in the same place: the note is precise about everything it measured itself and loose about the two or three things it inherited.
HIGH. The
Providesrow states a mechanism that is false on the build host, and it is offered as the precedent for tracker#13Lines 132-139, the relationships table:
That holds only when no real package of that name is reachable. On the build host one is, at a higher priority than the local build:
Measured in a container with the same shape,
keel-fab 2.0.0installed from a file and a realfabin an archive at 999:So of the two sites the row says
Providesexists for, one is not protected by it and the other is not protected by apt at all:tkldev-setup:372isapt-get install -y fab, and it removeskeel-faband installs upstream's fab. So doesdocs/infra-recovery.md:96, the rebuild-from-a-fresh-Debian procedure, which the note's follow-up table does not list.tkldev/plan/main:3never reaches apt.tkldev/Makefileisinclude $(FAB_PATH)/common/mk/turnkey.mk, so that line goes throughfab-plan-resolve, and fab's own resolver matches the pool's.debfilenames against literal names (fablib/plan.py:205-206) and readsProvidesonly from packages it has already fetched (plan.py:329, used only atplan.py:406). It cannot resolve a virtual name. Worse, the miss is silent:missingis never mutated whilebrokendepsiterates it, soresolve()returns no error for a package the pool could not supply.This matters more here than in fab#9, because the note says explicitly that the triple is "the precedent for tracker#13's
keel-versionandkeel-sysinfo, which take over from packages that do have a reverse dependency and an operator facing command, so theirProvideswill be doing real work that fab's is not". Those two are more exposed to this exact failure, not less. The row as written tells the next author thatProvidesmakes install-by-name safe. It does not. What makes it safe is a negative pin on the real package, which I confirmed works:The rest of the row is right and worth keeping:
Conflictsis load bearing,Replacesis what hands the paths over, unversioned is the correct choice for both because upstream'sfabkeeps releasing. Two sentences fix this:Providessatisfies a dependency on the name, it does not win against a real package of that name in an enabled archive, and it does nothing at all for a fab plan.It also changes the sequencing two paragraphs later. "That is a live defect independent of this note" is true of the pin matching nothing; it is not true of the whole of apt#15. Blocking upstream's real
fabis a requirement the rename creates, so apt#15 is a prerequisite of the conversion rather than a neighbour of it.MEDIUM 1. "all 28 paths the old package had are present" is not what was measured
Line 253. I rebuilt the package and compared it against the
fab 1.1.1+keel2installed on the host. The substance holds:make-release-deb.pyandturnkey-version.pycome out byte identical to the installed ones, all nine aliases are there,product.mkisa06bfe03, andfabliblands on the version independent path. The counting does not.The old package ships 26 regular files plus 9 symlinks. 28 reconstructs only as 19 files plus 9 symlinks, that is, with the 4
dist-infofiles and the 3usr/share/doc/fab/files silently dropped from the comparison. Everything the sentence does not mention is inside the dropped set:usr/share/doc/fab/becomesusr/share/doc/keel-fab/, so three paths the old package had are not present under their old names. Nothing in the organization reads that path, so the effect is nil, but the claim as phrased is the one thing that is not true.changelog.gzdiffers. There are three differing files, not two.usr/lib/python3/dist-packages/fab-1.1.0.dist-info/is unchanged, so the installed package tells Python it isfab 1.1.0while dpkg calls itkeel-fab 2.0.0.And
runtime.d/fab.rtupdatebecomeskeel-fab.rtupdate, which the sentence covers with a glob rather than naming as a rename. "35 entries in the old package, 32 byte identical, 3 renamed by debhelper, 2 new" is both truer and a stronger claim, and it is also a better illustration of the trap the note is adding, since two of the three renames are debhelper keying on the package name.MEDIUM 2. The
Conflictsrow still nameskeel-fab_0.1.0_all.debLine 132.
4668f5amoved the note to 2.0.0 and this one survived. It is inside the table an operator reads to understand the conversion, two screens above the runbook that correctly says2.0.0. Same file, one word.MEDIUM 3. The note argues the name from "no
upstreamremote" while committing to cherry-picks in both directionsLines 108-120 against the 0008 paragraph at line 66. One of the three native conditions is
git -C "$dir" remote | grep -qx upstream(apt/lib/build.sh:83), and the ordinary way to cherry-pick from upstream is to add that remote. Whoever does it on the build host clone makesbin/build-packagereclassifykeel-fabas a rebuild and refuse2.0.0for lacking+keelN. The note is the right place to notice that, since it is the document asserting both things.MEDIUM 4. "The seven other repositories that ship something and do not gate their changelog"
Line 397, and the same phrase in the pull request body. tracker#18's title says seven, its first sentence says eight, and the true statement is neither: 13 repositories ship something installable and do not call the gate. 8 is 7 recipes plus
fab, with the 5 changelog-less Debian sources bucketed separately. Since the note is what the next reader will cite, "the other repositories that do not gate their changelog, audited in tracker#18" without a number would age better than a number that three documents disagree about.MEDIUM 5. On tracker#18's second group, which the note defers to
You asked for a verdict on this, so: the facts are right and the conclusion is the weaker half.
bin/require-changelogreally does return 0 when the changelog is absent, before it even looks at the diff, and the behaviour is locked in bytests/require-changelog.bats, "a repository without a changelog requires nothing". So "a permanently green check asserting nothing" is exact.But all five of those repositories have
debian/controlanddebian/rules, anddpkg-buildpackagecannot run at all without a changelog: it reads the name, version and distribution from it. The sharedbuild-deb.ymlis a plaindpkg-buildpackage -us -uc -bwith nodchand no generation step anywhere. So "the version being produced at build time" describes an inherited TurnKey convention rather than a working pipeline here, and the honest answer to "could a package source with no changelog legitimately produce a package" is no. It is also only a convention for three of the five:tklbam,tklbam-python-botoandturnkey-pylibgitignoredebian/changelog, whilewebminandturnkey-chrootdo not, so for those two the absence reads as a plain omission, which is the bug a gate exists to catch.And the same branch is a live bypass in the 15 repositories that are gated, not just a dormant one in the 5 that are not: the check is
[[ ! -f "$changelog" ]]against the checked-out merge ref, so a pull request that deletesdebian/changelog, or a typo in a caller'schangelog:input, turnspackage / changeloggreen.So the right order is the reverse of the audit's: make the gate refuse a package source it cannot find a changelog for, which is a few lines in a repository whose suite is at 100, and then wire all five up immediately with a red check that states a true fact. A red check that says "this is a package source with no changelog" is strictly more useful than prose in an issue. tracker#18 reaches for this itself in its last bullet and then files it as an open question; it should be the first action, not the last. The one part of its conclusion worth keeping is that wiring the five up before fixing the script would manufacture false assurance.
This does not change anything in note 0017, which only links the audit. It is worth a line in whichever note settles it, since "a gate that cannot find its subject and reports success" is the same shape as the trap the gate was written for.
LOW
19 + 56 + 36 + 15, and all four suites run clean:1..19,1..56,1..36,1..15, zeronot ok. fab#9's own table says 126 and its test plan says 125, so this is a number three places disagree about in a note whose standard is that every factual claim carries its measurement.2026-09-27T19:58:53Zto2026-09-28T06:06:20Z./root/src/fabis on branchpkg/keel2ate610377with a clean worktree, andgit log -- debian/changelogthere, at HEAD, already returns both commits. What is stale isremotes/origin/master, which is16730a5, the #5 merge: the host has not fetched since before #6 landed.masterisbehind 4of that stale ref and behind 6 of the real one. That is a better version of the amendment the note wants to hand to #13, because "name the clone and its head" would not have caught this one either. The word that would is "name the ref you measured": HEAD was current andorigin/masterwas not, in the same clone.docs/traps.md, the new entry: accurate on every point I could check. debhelper does key.install,.linksand.docson the binary package name;dh_python3does find a private directory as/usr/share/<package>; and the two-call claim is right in effect, which I confirmed by building the package, since the bare call is what putsfablibin/usr/lib/python3/dist-packagesrather thanpython3.13, and the directory call is what restores the shebang rewrite that makesmake-release-deb.pyandturnkey-version.pybyte identical to the installed ones. The version paragraph belongs where it is. One unmeasured word: "lintianis quiet" is the only claim in the entry that nothing in fab#9's test plan backs, and the entry does not need it.pyproject.toml, which keepsname = "fab",version = "1.1.0", so the built package reports three different versions depending on who is asked. Keeping it frozen is probably what holds thedist-infofiles byte identical; one clause under "What this does not decide" would settle it./usr/bin/fabplusfab-investigate,fab-rewindand the nine aliases. Worth adding to the verification line: check ownership withdpkg -S /usr/bin/fab, notdpkg -V. If anyone ever reaches fordpkg -i --force-conflictshere,Replacesbeing one-directional leaveskeel-fabowning/usr/bin/faband all nine aliases even thoughfabunpacked second, and removingkeel-fabthen deletes them whiledpkg -V fabstill exits 0.Verdict
The decision is right, the reasoning is genuinely argued, the trap entry is accurate and useful, and the correction of fab#7 is correct:
1.1.1+keel2was onmasterbefore #7 was filed, and I reproduced the byte comparison independently. One HIGH, and it is one paragraph: theProvidesrow states a mechanism that does not hold on the machine this decision is about, in the sentence the note explicitly offers as precedent for the next two packages. Fix that row and the 0.1.0 in the table above it and I would call it Approve.Warning.
fad3160: theProvidesrow now says what it does not do and which pin closes it, with the conversion order (apt#15 first). The pin the review proposed,Pin: origin "", does not block an http archive (it only matchesfile:repositories); reproduced in a scratch apt tree, and the note recordsPin: release o=turnkeylinuxinstead. MEDIUMs 1 to 4 and the LOWs are folded in.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.