Skip to content

ci: make the compiler cache outlive the build container - #33

Open
hanthor wants to merge 29 commits into
fix/build-failures-batch1from
claude/utah-rawhide-build-iriu0i
Open

ci: make the compiler cache outlive the build container#33
hanthor wants to merge 29 commits into
fix/build-failures-batch1from
claude/utah-rawhide-build-iriu0i

Conversation

@hanthor

@hanthor hanthor commented Sep 2, 2026

Copy link
Copy Markdown
Member

Follow-up to #32, from reading the shard logs and the run history on this branch.

What the WebKitGTK logs actually show

The shards both succeeded (2h13m GTK 4, 2h52m GTK 3, against 5h01m for the old single job). But the compiler cache did nothing, and the reason is specific.

In-container statistics from job 100112414952:

Compile requests                      7
Cache hits                            0
Cache misses                          7
Cache location                  Local disk: "/root/.cache/sccache"

Meanwhile ninja ran all 8955 targets through /work/tools/sccache. Three separate faults:

  1. The container never sourced /work/tools/sccache.env. mozjs140's recipe does; webkitgtk's does not. Without it sccache has no backend configured and defaults to /root/.cache/sccache — inside the container, discarded when the container exits. Every object compiled, twice per run, thrown away.
  2. SCCACHE_IDLE_TIMEOUT defaulted to 600s, so the server exited during the link and debuginfo phases. That is why the end-of-build statistics claim 7 requests for a build that made 8955.
  3. The host-level "Show compiler cache statistics" step talked to a different server than the build used, so it reported zeros and hid all of this.

Changes

  • The cache directory lives on the /work mount. sccache.env now sets SCCACHE_DIR=/work/sccache, SCCACHE_CACHE_SIZE=6G and SCCACHE_IDLE_TIMEOUT=0. The lane restores that directory from ghcr.io/<owner>/utah-packages-ccache:<package> before the build and publishes it after.
  • The Actions cache backend is dropped. 10 GB per repository, evicted LRU, expiring after a week: two ports of WebKit with debuginfo would evict each other and every other package's objects. A GHCR repository has neither limit and is free for a public repo.
  • Opt-in per entry via "compiler_cache": true in the source manifest, currently webkitgtk, webkit2gtk4.1 and mozjs140. Restoring and publishing gigabytes costs minutes; the 170 packages that build in under ten minutes would pay that for nothing.
  • webkitgtk.spec sources the environment file, which is what makes the whole thing work.
  • The cache action is no longer hashed into a package build identity. It was, and that is a trap worth naming: every sccache tweak changed the key of all 178 packages and forced a full-factory rebuild. An sccache entry is addressed by the hash of the preprocessed source plus the compiler flags, so it cannot change what a build produces, only how long it takes. rebuild-lane.yml is still hashed.

The cache is published on failure too, and read on pull requests, for the same content-addressing reason: a partial compile leaves usable objects, and no build can look up an entry it would not itself have produced.

Expected effect

First run after this fills the cache. The run after that should cut both WebKitGTK shards well below two hours, and mozjs140's 20 minutes down to its link time.

Validation

tools/validate.py, tools/check_workflow_quoting.py, all three YAML files parse, and 20 unit tests pass including new coverage for the cache policy and for the spec sourcing the environment file. Not executed against a real runner — no rpm or podman here — so the PR run is the test.

Scheduling note

Run 33608194980 is in flight on this branch. This PR is in a different concurrency group so it will not cancel it, and per the workflow's own comment concurrent waves cannot lose RPMs. It will queue jobs alongside that run; hold or close it if you would rather not share runners right now.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB


Generated by Claude Code

The sccache client has been installed for every lane since b4f6313, and for
WebKitGTK it cached nothing. Run 33582351064 shows why: the recipe wraps all
8955 of its compile actions in the client, but nothing inside the container
sources /work/tools/sccache.env, so the credentials for the GitHub Actions
backend never reach it. sccache falls back to /root/.cache/sccache -- a
directory inside the container -- and that goes away when the container does.
Both shards therefore compiled every object from scratch and threw the result
away, twice per run.

Two changes fix it. The recipe sources the environment file, as mozjs140
already does. And the environment file now points SCCACHE_DIR at a directory
on the /work mount, which the lane restores from
ghcr.io/<owner>/utah-packages-ccache:<package> before the build and publishes
after it. The Actions cache backend is dropped: it is capped at 10 GB per
repository and expires after a week, and two ports of WebKit with debuginfo
would evict each other and everything else out of it. A GHCR repository has
neither limit and costs nothing for a public repository.

Only webkitgtk, webkit2gtk4.1 and mozjs140 opt in, through "compiler_cache"
in the source manifest. Restoring and publishing several gigabytes costs a
few minutes, which the 170 packages that build in under ten would pay for
nothing.

The cache is published even when the build fails, and read on pull requests:
an sccache entry is addressed by the hash of the preprocessed source and the
compiler flags, so a partial compile still leaves objects a later build can
use, and no entry can be looked up by a build that would not have produced it.
That same property is why the cache action is no longer hashed into a package
build identity. It was: tuning the cache changed the key of all 178 packages
and rebuilt the entire factory, which is the opposite of what a cache is for.

SCCACHE_IDLE_TIMEOUT is now 0. The default ten minutes ended the server during
the link and debuginfo phases, which is why the statistics at the end of a
WebKitGTK build reported seven compile requests for a build that made 8955.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
The first version of this never published anything. Run 33616365993 proves
the important half works -- mozjs140 reported

  Cache location    Local disk: "/work/sccache"
  Compile requests  378, cache misses 298

so the objects now land on the /work mount instead of inside the container.
The publish step then skipped them in silence:

  find: work/sccache/4: Permission denied
  find: work/sccache/preprocessor: Permission denied

The build container runs as root and sccache creates its shard directories
0700, so the whole tree belongs to root and the runner user cannot read it.
The content test was `test -n "$(find ...)"`, and find writes those errors to
stderr and nothing to stdout, so the test failed, the step exited 0, and the
job went green having cached nothing. Take ownership first.

Two things around it. The build context for the cache image was the
repository root, which sent the built RPMs and the seeded accumulator through
podman build for no reason; it is now the cache directory alone. And the
host-side statistics step is gone, along with the SCCACHE_DIR it needed:
pointing a second client at the directory starts a server of its own and
prints its own zeroes, which is precisely what hid the original fault for two
runs. The recipes print the statistics from inside the container, where the
compiling happens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
@hanthor

hanthor commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

First run gave a partial result and a bug, both now addressed in 0132d21.

The core fix works. mozjs140 in run 33616365993:

Cache location      Local disk: "/work/sccache"
Compile requests    378
Cache misses        298

Previously that read /root/.cache/sccache, inside the container. The objects now land on the /work mount, which was the whole point.

The publish step then dropped them silently. The build container runs as root and sccache creates its shard directories 0700, so the tree is unreadable to the runner user:

find: work/sccache/4: Permission denied
find: work/sccache/preprocessor: Permission denied

My content guard was test -n "$(find work/sccache -type f -print -quit)". find wrote those to stderr and nothing to stdout, the test failed, and the step exited 0 — a green job that cached nothing. It now takes ownership before looking.

Two related changes in the same commit:

  • The cache image's build context was the repository root, so podman was handed the built RPMs and the seeded accumulator. It is now the cache directory alone.
  • The host-side "Show compiler cache statistics" step is removed, together with the SCCACHE_DIR export it relied on. Pointing a second client at the directory starts its own server and prints its own zeroes, which is exactly what hid the original fault for two runs. The recipes already print the statistics from inside the container, where the compiling happens.

The wave already running on the previous commit keeps its RPMs and checkpoints; it just will not publish a cache. The next wave fills it.


Generated by Claude Code

The factory promises exactly config/upstream-sources.json: every entry there
is a package it builds and publishes. Nothing checked that the published
repository contains them, and two paths lead to one that does not.

prepare treats a package as already published when it finds it in the
:building accumulator, which is seeded from every wave including failed ones.
publish seeds its candidate from :latest alone. A package that only ever
reached the accumulator is therefore skipped by the build and absent from the
candidate, and no later step looks for it.

The gates that do exist cannot catch that. precedence ranks only what the
current run built, so a package nobody built this time is outside its view.
The Hummingbird-only consumer transaction enables the base OS repository
beside the candidate -- it has to, because Hummingbird supplies part of the
closure through the bootc base RPM database -- so a contract package the
factory failed to provide resolves from Hummingbird instead and the
transaction still succeeds. Both gates pass and the repository ships short.

tools/repository_contract.py asserts the promise directly, in publish, before
the metadata is signed and the image is pushed: every configured package
appears in the candidate build manifest, and every RPM that manifest names is
present with the checksum recorded when it was built. The last part matters as
much as the first -- a half-merged artifact download leaves a file of the
right name and the wrong contents, which existence alone would accept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
rebuild3 / build (malcontent-bootstrap) in run 33624473308 died at the
accumulator seed with

  Error: no command or entrypoint provided, and no CMD or ENTRYPOINT from image
  ##[error]Process completed with exit code 125.

Every image this repository copies out of -- :building, :latest, and the
compiler cache added in this branch -- is FROM scratch plus a COPY, so none
declares a command, and podman 5 refuses to create a container from such an
image unless the command is on the line. Six call sites had the same shape.
It is not run: the container exists only so podman cp can read a path out of
it, and it is removed immediately after.

The step reads as tolerant -- it opens with `set -uo pipefail` and every other
command ends in `|| true` -- but the job shell is `bash -e`, so this one
unguarded assignment took the whole lane down. Sibling jobs in the same stage
survived only because a pull that fails skips the block entirely.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
@hanthor

hanthor commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

Merge-when-green pass. Not merging: run 114 for the head commit is still queued, so this PR is not green yet. Two things found in the meantime.

A pre-existing crash, now fixed here (5006cf4). rebuild3 / build (malcontent-bootstrap) in run 33624473308 died at the accumulator seed:

Error: no command or entrypoint provided, and no CMD or ENTRYPOINT from image
##[error]Process completed with exit code 125.

Every image this repository copies out of — :building, :latest, and the compiler cache this branch adds — is FROM scratch plus a COPY, so none declares a command, and podman 5 will not create a container from such an image unless the command is given. Six call sites shared the shape. The command is never run; the container exists only so podman cp can read a path out of it.

What made it fatal rather than noisy: the step opens set -uo pipefail and every other command ends in || true, which reads as tolerant, but the job shell is bash -e, so the one unguarded assignment took the lane down. Sibling jobs in the same stage survived only because a failed pull skips the block entirely.

Cache status so far. The restore and publish steps behave correctly on packages that do not opt in — Resolve the compiler cache policy computes an empty ref, the GHCR login and the publish step skip. Run 113 has not yet reached mozjs140 or a WebKitGTK shard on the fixed code, so there is still no first cache publish to report. Run 114, on the head, will be the one that shows it.


Generated by Claude Code

@hanthor

hanthor commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

The compiler cache works end to end. Evidence from run 33656040424 (head 5006cf4), mozjs140 lane:

Restoring compiler cache from ghcr.io/projectbluefin/utah-packages-ccache:mozjs140
Restored 299M of cached compiler output
...
Compile requests                    378
Cache hits                          192
Cache misses                        106
Cache hits rate                   64.43 %
Cache location                  Local disk: "/work/sccache"
...
Publishing 300M of compiler cache to ghcr.io/projectbluefin/utah-packages-ccache:mozjs140
Writing manifest to image destination

A 64% hit rate on the second run of an unchanged source, restored from GHCR and pushed back. Before this branch the same lane reported Cache location Local disk: "/root/.cache/sccache" and threw every object away with the container.

The podman fix cleared the failures it was meant to. checkpoint0, checkpoint0-late and checkpoint1-fast are all green in this run; the same jobs failed in run 33624473308 with no command or entrypoint provided. Worth noting for anyone else on this repo: that regression is not specific to this branch. Run 111 at 08:20 did not hit it, run 113 from 11:24 hit it in four places, which points at a runner image rolling podman 5 out during the day. Any wave on any branch will hit it until this merges.

Both WebKitGTK shards are compiling now with the cache steps wired, so the next report should have their hit rate and durations.

Not merging yet: the wave is still in progress. Note that auto-merge cannot be enabled on this repository — the API returns "Auto-merge is not enabled for this repository" — so this is being merged by hand once every check is green.


Generated by Claude Code

@hanthor

hanthor commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

WebKitGTK, the build this branch exists for, in run 33656040424:

Shard Compile step Cache Publish
webkitgtk (GTK 4) 45m 33s restored 3.4G, 2817 hits / 2 misses, 99.93% 3.5G pushed
webkit2gtk4.1 (GTK 3) 48m 07s restored, published pushed

Both succeeded. Against the history:

  • One job, both ports, no cache: 5h 01m
  • Two shards, no working cache: 2h 13m and 2h 52m
  • Two shards, warm cache: 45m and 48m

The lane's critical path is now under an hour, from five hours. 2817 of 2819 compile actions came out of the cache; the two misses are the only translation units whose preprocessed form changed.

mozjs140 in the same run: restored 299M, 192 hits / 106 misses (64.43%), pushed 300M back. Lower because its cache was filled by a partial earlier run rather than a complete one; it should climb.

The podman fix is holding — checkpoint0, checkpoint0-late and checkpoint1-fast all green, where run 33624473308 failed all of them.

Still running: stages 2 to 4 and then publish, which is where tools/repository_contract.py gets its first real exercise. I will report what it says either way — if it refuses the candidate, the packages it names are the answer to the question the gate was added to ask, not something to work around.


Generated by Claude Code

The publish gate caught this on run 33656040424:

  webkitgtk: webkitgtk-debugsource-2.53.91-1.hum1.bfin.x86_64.rpm does not
  match its recorded checksum

Both shards build the same source package, and RPM names the debug packages
after it, so each emitted webkitgtk-debuginfo and webkitgtk-debugsource at
the same NEVR with different contents. The lanes' artifacts extract into one
directory, so whichever landed second overwrote the first, and the losing
shard's manifest no longer described the file on disk. The `-debugsource`
mismatch was the visible half; `-debuginfo` collided the same way.

`%global _debugsource_packages 0` was supposed to keep the GTK 3 shard from
producing a debugsource package. It did not: find-debuginfo still ran with
-S and the package was built. It also would not have helped, since it says
nothing about -debuginfo.

Give the GTK 3 shard its own source name instead, which gives it its own
debug namespace and its own /usr/src/debug base. Nothing depends on that
name: every binary package is declared with `%package -n`, there is no bare
%files section so no package named after the source is ever produced, and
Source0 and %autosetup spell the tarball out literally. It is webkitgtk4.1,
not webkit2gtk4.1, which is already a subpackage. With neither bcond given,
as on a plain rpmbuild, the name stays webkitgtk exactly as Fedora ships it.

The gate reported this as a checksum mismatch on the shard that lost the
race, which names the victim rather than the fault. It now checks for two
packages recording the same filename with different contents and says so
directly. Identical contents are not a collision -- whichever lands last is
the same file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
The contract gate stopped run 33682968939 with ~185 of 187 packages reported
as "the factory builds it, the candidate does not carry it". That is the gate
working, and it names a real defect rather than a fault in the shards.

`prepare` decides a package is already published by looking in the `:building`
accumulator as well as `:latest`, so an incremental run rebuilds only what
changed -- here, the two WebKitGTK shards. `publish` then seeded its candidate
from `:latest` alone. Everything the accumulator had gathered across previous
waves was in neither the seed nor this run's artifacts, so the candidate
carried two packages. `:latest` predates the build manifest entirely, which is
why nearly every entry came back missing rather than merely stale.

Seed from both tags, lowest precedence first: `:latest` is the last
contract-gated repository, `:building` accumulates every wave since and so is
at least as new, and this run's artifacts land last and outrank both. Seeding
from the accumulator is safe only because of what now follows it -- a
half-finished accumulator fails the gate instead of publishing short, which is
the whole point of having the gate.

Two supporting changes.

The malcontent bootstrap pass is deleted from the candidate before it ships:
it breaks the flatpak-libs -> libmalcontent-0.so.0 cycle within one run and
has no parental controls UI, so it must never reach a consumer. The delete
ran after the gate, so the gate was inspecting a repository that differed from
the published one. It now runs before, and the gate ignores outputs whose
release is 0.bootstrap -- the factory does build that package, so it stays
promised, but demanding files publish deliberately removes would fail every
incremental run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
…name

`prepare` reported "will build 23 of 187 packages" on every run, and it was
the same 23 each time -- including both WebKitGTK shards, samba and mesa,
which is where the wall clock went. They were not being rebuilt because
anything had changed.

The skip list is computed from `published`, harvested out of repodata, which
lists BINARY package names. The name being looked up is the SOURCE package
name from config/upstream-sources.json. For a source package that ships no
binary of the same name the lookup can never succeed, so `name not in
published` is permanently true and the package rebuilds forever. That is not
an edge case: grub2 ships grub2-common and grub2-tools, mesa ships
mesa-libGL and mesa-dri-drivers, wayland ships libwayland-client, pycairo
ships python3-cairo, rust-just ships just, and webkitgtk ships webkitgtk6.0
and webkit2gtk4.1. Twelve of the thirteen entries checked by hand had no
binary matching their source name; samba, which does, was selected for a
different and legitimate reason.

The identity check next to it does not have that flaw. It is keyed on the
source name, and it is strictly stronger than comparing a name and a version:
the build key covers the recipe, the source lock, factory policy and the
Hummingbird base, and outputs_match then requires every RPM it recorded to be
present with the checksum it was built with. So when a manifest entry exists
it decides alone, and the repodata heuristic stays as the fallback for when
there is none -- reading the published Pages repository rather than the
accumulator.

The consequence is only wasted time, never a short repository: a package
wrongly skipped here is one the publish contract gate refuses to publish
without.

tests/test_prepare_selection.py extracts the decision block from the workflow
and executes it rather than restating it, so the copy cannot drift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
The publish gate now passes -- the candidate carries all 187 packages -- and
the Hummingbird-only consumer transaction fails immediately after it:

  mesa-libGL-26.2.1-1.hum1.bfin.x86_64 from factory requires
  mesa-dri-drivers(x86-64) = 26.2.1-1.hum1.bfin, but none of the providers
  can be installed
    nothing provides libSPIRV-Tools.so()(64bit)
    nothing provides libsensors.so.4()(64bit)
    nothing provides libxshmfence.so.1()(64bit)

mesa-libGL requires mesa-dri-drivers at the same NEVR, so an image that
installs the one installs the other, and mesa-dri-drivers links three
libraries nothing can supply. Hummingbird has 3,510 binary packages and none
of them is spirv-tools, lm_sensors, libxshmfence or any mesa: checked against
its primary metadata rather than assumed. The factory did not build them
either, so the closure was open on both sides.

This was not reached before because publish never got past the contract gate.
It is a long-standing gap in the repository's content, not a regression from
the shard or seeding work.

Import all three from Fedora rawhide with the existing closure tooling. Each
ships the exact soname the resolver asked for: spirv-tools installs an
unversioned libSPIRV-Tools.so, libxshmfence installs libxshmfence.so.1, and
lm_sensors carries Fedora's soname-revert patch, which takes LIBMAINVER from
5 back to 4 and so installs libsensors.so.4. They are leaf libraries, so they
need no stage override.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
Hummingbird is upstream-first. The factory was not: config/upstream-sources.json
froze whatever version was imported, and nothing moved it. The machinery to do
better was already here and simply never wired up -- tools/dist_git.py is a
Koji-gated dist-git checker whose docstring says "the workflow turns them into
reviewable update branches", and no workflow called it. Meanwhile
detect-rawhide-updates.yml announced "Fedora is a compatibility build root, not
a source-update feed", which is true of the build root and wrong as a policy for
sources.

tools/track_upstream.py compares every entry against Fedora dist-git rawhide and
reports two different kinds of drift:

  Version drift, where our entry is older than rawhide. Following upstream means
  taking rawhide's version, but only once Koji reports a COMPLETE build of that
  exact NVR. dist-git advancing is not proof Fedora built it, and promoting on a
  commit alone can name a version no binary exists for.

  Location drift, where the entry fetches from the upstream project rather than
  Fedora's lookaside. This is the one that actually bites. wireguard-tools is
  pinned to git.zx2c4.com/wireguard-tools/snapshot/, which generates tarballs per
  request and 404s -- while rawhide ships that exact version with a byte-identical
  sha512, reachable from the lookaside. Verified: the proposed URL returns 200 and
  the download hashes to the digest already recorded. 116 of 190 entries already
  use the lookaside; 74 point at hosts that rewrite and expire.

The sha512 stays in both cases. It is not a version pin -- it is what makes the
pipeline fail closed when a tarball is re-rolled underneath us, which is exactly
the risk that following upstream raises rather than lowers.

track-upstream.yml runs this daily and opens a pull request. It proposes, never
applies: editing the source manifest invalidates every package's build identity
and rebuilds the whole factory, so these land as their own reviewed change
rather than arriving in the middle of a wave.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
Run 33714581543 built 202 of 205 jobs green -- including the three new mesa
providers and both WebKitGTK shards -- and one package failed, which was
enough to block publish:

  all source URLs failed:
  https://git.zx2c4.com/wireguard-tools/snapshot/wireguard-tools-1.0.20260223.tar.xz:
  failed to fetch after 3 attempts: HTTP Error 404: Not found

cgit generates snapshot tarballs per request and stops serving them when the
tag moves, so that URL was always going to expire. Fedora rawhide still ships
this exact version, and its lookaside holds a byte-identical artifact: the
sha512 recorded here already matches Fedora's sources file, and the download
hashes to it. Only the location changes -- not the version, not the bytes,
not the digest we verify against.

Produced by tools/track_upstream.py, which classified it as location drift
rather than version drift precisely because rawhide is on the same version.

Also stop --apply from sorting keys when it writes the manifest. json.load
preserves insertion order, so dumping without sort_keys leaves every untouched
entry byte-identical; sorting rewrote all 190 entries and buried a one-line
source change in a 632-line diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
The mesa providers worked. Run 33729693293 reached the Hummingbird-only
consumer transaction with no mention of libSPIRV-Tools.so, libsensors.so.4 or
libxshmfence.so.1 -- a949f55 resolved those completely. The gate reports only
the first unsatisfiable set, so closing one layer exposes the next, and it
named 29 problems that reduce to three missing providers:

  libstemmer.so.0, needed by libtinysparql. This is the one that hurt: nearly
  every problem in the list is a cascade from it. Hummingbird ships
  libtinysparql but not the libstemmer it links, so its own package is
  unsatisfiable, and gtk4 requires libtinysparql-3.0.so.0 -- which takes down
  libadwaita, zenity, gtk3, gnome-session, gnome-desktop4,
  xdg-desktop-portal-gnome and openssh-askpass with it. Fedora's snowball
  builds a libstemmer subpackage.

  /usr/bin/xprop, needed by at-spi2-core. Fedora's xprop ships %{_bindir}/xprop.

  xdg-user-dirs, needed by xdg-user-dirs-gtk, which gnome-shell requires and
  gsconnect requires after that. Fedora's xdg-user-dirs is the package itself.

Each was verified against rawhide dist-git before importing rather than
guessed: snowball declares %package -n libstemmer, xprop lists the binary in
its %files, and xdg-user-dirs is a direct name match. The factory promises 193
packages now.

That the gate keeps finding these is the point of having it. Every one is a
package Utah would have installed into a broken closure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
Layer two worked: libstemmer, xprop and xdg-user-dirs are gone from the gate's
output. Rather than keep paying a three-hour rebuild per layer, this reads the
whole failure instead of its tail. dnf reports every problem it found, so the
complete set was already there: 29 problems naming 90 distinct unsatisfiable
capabilities, which reduce to 44 source packages.

The pattern is not a tail of stragglers. Hummingbird is a server base -- 3,510
packages, no desktop stack -- and the factory's desktop packages link a long
list of media and desktop libraries that neither side ships:

  gstreamer1-plugins-base alone needs opus, orc, libXv, cdparanoia, libogg,
  libtheora, libvisual and libvorbis, and it is what gtk4 requires, so that
  one package gates most of the GNOME stack.

  webkitgtk6.0 needs jpegxl, flite, enchant2, hyphen, libmanette and
  xdg-dbus-proxy; pipewire-libs needs sbc, libldac, liblc3, libsndfile,
  spandsp, libebur128, fdk-aac-free and bluez; fwupd needs libxmlb and passim;
  colord-libs needs libgusb, which is what actually took gtk3 down.

Some are Hummingbird's own packages failing against Hummingbird: its lvm2
requires libnvme and device-mapper-persistent-data, which it does not carry.
Supplying them from the factory is the only lever available here.

Every name was checked against rawhide dist-git before importing, which caught
three that a guess would have got wrong: libjxl is built by jpegxl, ldacBT by
libldac, and libldac's spec sets Version from %{sonamebase} so the importer
recorded a macro rather than 2.0.2.6. Four more -- cdparanoia, hyphen,
libasyncns and libtheora -- record MD5 in dist-git rather than SHA-512, so the
importer skipped them; each was fetched from the md5 lookaside, its digest
confirmed, and a sha512 computed from the bytes actually downloaded. The
integrity check stays exact for them.

Two are worth a second opinion rather than being slipped in. ffmpeg is here
because pipewire-libs-extra links libavcodec.so.62, and Utah deliberately does
not enable fedora-multimedia, so building codecs in the factory is a policy
choice. anaconda-webui is here because anaconda-live requires it, which is ISO
tooling rather than the desktop runtime. Both are needed for the transaction to
resolve; neither is obviously wanted forever.

237 packages promised now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
Run 124 built 229 of 237 and failed eight. Reading all three pages of the
check runs rather than the first -- the API returns 100 per page and reports
the rest only in total_count -- gives the whole list, and five of the eight
reduce to two causes.

libbluray is the larger one. rawhide dist-git has moved to 1.5.0, which is
libbluray.so.4, while the composed repository the buildroot resolves against
still ships 1.4.0 and everything in it, Fedora's own libavformat-free
included, links so.3. Building 1.5.0 excludes Fedora's 1.4.0 by name -- that
is how the factory keeps Fedora from answering for what it rebuilds -- and
then nothing provides so.3, so libavformat-free became uninstallable and took
localsearch, pipewire-libs-extra and, through gvfs, nautilus-python with it.
The factory cannot rebuild Fedora's compose ahead of Fedora. It can decline
the feature: gvfs gets -Dbluray=false unconditionally where Fedora sets it
only on RHEL, ffmpeg gets a libbluray bcond defaulting off, and the entry
comes out of the manifest. Blu-ray disc navigation is not something a Utah
machine has a drive for, and declining it is what keeps one soname in play.
When rawhide's compose carries 1.5.0, the tracker will propose it back.

ffmpeg's own failure was pkgconfig(vapoursynth) >= 79, which Fedora has and
Hummingbird does not. vapoursynth is a frameserver rather than a codec and
nothing in the contract links it, so it gets a bcond too rather than dragging
zimg and a Python extension in to satisfy a BuildRequires whose output no
consumer asks for. ffmpeg matters here: pipewire-libs-extra links
libavcodec.so.62 at runtime and Hummingbird ships no libav at all.

The other three are each their own:

  protobuf pulled Fedora maven-lib into a buildroot that already had
  Hummingbird's maven3.9, and the two disagree about twenty files under
  /usr/share/maven. Nothing in the contract asks for protobuf-java, so the
  entry builds --without java rather than arbitrating between two Java
  stacks.

  python-typing-inspection ran %pyproject_patch_dependency in %prep, which
  imports packaging, before the generated BuildRequires are resolved. It now
  asks for python3-packaging by name.

  libvorbis lost its %changelog header in the import, so rpmautospec's
  expansion landed in the last %files section and every word of "* Thu Sep 03
  2026 John Doe <packager@example.com>" was read as a path.

  libayatana-ido's %check calls xwfb-run, and xwayland-run does not depend on
  a compositor, so it died with "No such file or directory: 'weston'" before
  a test ran. Fedora's builders have weston for other reasons; this asks for
  it rather than dropping the suite -- the same call the system bus and USER
  in the lane script already make.

236 packages promised now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
The closure commit flagged ffmpeg and anaconda-webui as worth a second opinion
rather than being slipped in. Both are in: they are part of the vanilla Fedora
stack, which is the bar, whether or not Utah installs with Anaconda. Since the
recipes are Fedora's, this builds ffmpeg-free and libavcodec-free -- Fedora's
codec policy, not RPMFusion's.

The same page now records the two declines the last commit made, so the next
reader finds the reasoning instead of an unexplained gap: libbluray, where
rawhide dist-git is a soname ahead of the compose the build root resolves
against, and vapoursynth, which is a frameserver nothing in the contract
links. Both share one rule worth stating once -- where the factory and the
Fedora compose disagree about a soname, the factory declines the feature
rather than getting ahead of the compose it builds against, because adding the
newer library is the move that strands Fedora's own packages.

Documentation only. Build identity covers config/, the recipe directory,
hummingbird.repo, the runtime contract and rebuild-lane.yml, so nothing here
invalidates a build key.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
…mised

Removing libbluray from the manifest did not take effect, and made things
worse. The lane seeds /work/prior from the `:building` accumulator -- shared
across every run and ref -- and then derives the Fedora exclusion list from
whatever names it finds there: what the factory builds, Fedora must not answer
for. That is right while an entry exists and wrong the moment one is removed.
The libbluray RPMs from the run before were still in the accumulator, so
Fedora libbluray stayed excluded with nothing left to provide the soname. The
factory libavformat-free needs it, so it became uninstallable, and with it
libheif, gdk-pixbuf2, glycin, graphviz and doxygen -- which is how ten stage 0
packages that have nothing to do with Blu-ray failed at once: wayland, zenity,
pulseaudio, mozc, libvisual, spandsp, sdl2-compat, libtheora,
xdg-desktop-portal and xdg-user-dirs-gtk.

So the accumulator is now filtered against what the manifest currently
promises before it becomes a repository. tools/promised_sources.py lists the
source names, and the lane drops any RPM whose %{SOURCERPM} is not among them,
beside the ICU-77 purge that already does this shape for stale ABI. Matching
on the source package rather than the binary is the same reasoning as that
one: a source build ships subpackages that require each other by exact NEVR,
so dropping one and keeping its siblings is worse than dropping none.

Four spellings can each be the real source name -- the entry, the recipe
directory, the dist-git name, and every Name: the spec declares, since
webkitgtk sets it per GTK port -- and all four are allowed rather than guessed
between. Five recipes set Name: from a macro this cannot expand, or from a
font template that never spells it; each resolves to its own recipe directory,
which is already in the list. The tests pin that, and pin the sed that strips
%{SOURCERPM} back to a name, which has to take the version and release off the
end because a source name may itself contain dashes and dots.

protobuf got past the maven conflict with --without java and failed later, in
%build:

  Target "test_plugin" links to: absl::scoped_mock_log
    but the target was not found

for four test targets. This factory builds abseil-cpp with
ABSL_BUILD_TEST_HELPERS OFF where Fedora builds it ON, because that graph is
LTO-heavy and OOMs the hermetic runner, and Fedora abseil-cpp is excluded, so
protobuf resolves against ours and cannot configure a suite %bcond_with check
never runs. It now sets protobuf_BUILD_TESTS OFF. The deviation is recorded at
both ends: the next package to want a test-only Abseil target will fail the
same way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
The accumulator purge worked -- the exclusion list went from 1573 names to
1527 and libbluray and anaconda are gone from it -- and stage 0 failed anyway,
worse than before. The purge was the right fix for the wrong layer. One layer
up sits the same shape, and this time the factory caused it:

  libheif-1.23.1 from stages requires libavutil.so.60, no provider
  libavutil-free-8.1.2-4.fc44 from updates is filtered out by exclude filtering

A stage resolves the factory packages from the accumulator, which is the run
before; only later stages see what this run has built. So promising ffmpeg
excludes the Fedora libav by name from every buildroot the moment the entry
exists, while the factory replacement is a run behind -- and ffmpeg is itself
stage 0. The accumulator libheif was linked against the Fedora 8.x
libavutil.so.60, the factory ffmpeg 9.x provides a later one, and libheif
became uninstallable. gdk-pixbuf2 needs glycin needs libheif, graphviz needs
gdk-pixbuf2, doxygen needs graphviz, so twenty-odd stage 0 packages with no
connection to video went down: opus, pango, wayland, pulseaudio, libthai,
libtalloc, tecla, passim, openrgb, jpegxl and the rest.

The invariant that was missing: nothing in stage 0 may link libav, because
ffmpeg is in stage 0. Two ways to satisfy it, one each here.

waypipe moves to stage 1. Nothing in the factory BuildRequires it, so it can
simply build after ffmpeg and keep its video feature intact.

libheif cannot move -- glycin is stage 0 and needs it -- so it declines the
feature instead, the same call gvfs and ffmpeg already make for libbluray. A
narrow %bcond ffmpeg 0 gates the ffmpegdec plugin only; %bcond bootstrap would
have gone further and dropped rav1e, SvtEnc and sdl2 with it. The aom and dav1d
decoder plugins remain, and those are what decode AVIF and HEIC here.

Neither helps this run on its own, because stage 0 reads the accumulator, not
this run: the stale libheif would still be there. So the purge beside the
ICU-77 one now also drops every libav-linked RPM and every ffmpeg subpackage
from the accumulator. That takes those names out of the exclusion list too, so
Fedora answers for libav again until this run rebuilds ours. It stands in for
a general rule worth stating: a newly promised source cannot be excluded from
Fedora before the factory has built it once.

tests/test_stage_zero_libav.py fails when a third package takes neither way
out. It tracks %if nesting rather than grepping, so a gated BuildRequires
passes and a bare one does not, and it pins ffmpeg being stage 0 as the
premise the whole rule rests on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
gtksourceview4 build-requires pkgconfig(gladeui-2.0) for its Glade catalog,
and the Fedora glade-devel drags in the Fedora webkit2gtk4.1, which needs
libicuuc.so.77. The lane excludes the older Hummingbird libicu-77 globally so
that only 78 is selectable -- a build that picked 77 produced RPMs the
Hummingbird-only runtime could not install -- so that webkit cannot be
installed and the transaction has nowhere to go.

The lane comment on that exclusion already named this case: "Fedora build-only
consumers that want 77 are rare and not part of the runtime." gtksourceview4
is one. What it loses is an XML file describing widgets to the Glade designer;
nothing in Utah's runtime reads it, and Fedora itself drops it on RHEL 10
through the same bcond this flips.

Deliberately a recipe edit rather than rpm_defines in the manifest. Build
identity hashes the whole source manifest, so any manifest edit invalidates
all 236 build keys and costs a full three-hour rebuild; a recipe edit
invalidates only this one package, and everything else is served from the
accumulator by identity match. Same reason the two remaining fixes should stay
out of the manifest if they can.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
The run reached stage 4. nautilus-python, localsearch and pipewire-libs-extra
-- the three that had failed every previous attempt -- are green, along with
both webkit shards, samba, gvfs, mutter, gjs, gnome-control-center, malcontent
and every checkpoint. Two new failures, evolution-data-server and gnome-shell,
share one cause.

Their buildroots resolve the Fedora webkit2gtk4.1-devel and webkitgtk6.0-devel,
whose javascriptcore needs libicuuc.so.77, and Hummingbird libicu-77 cannot be
installed beside the libicu-78 that our own libphonenumber, libical and
mozjs140 require. Deadlock.

Fedora webkit is only in the running because ours is not: the exclusion list
for that lane has 1485 names and contains no webkit, no javascriptcore and no
ffmpeg, while libheif, glycin, localsearch and pipewire-libs are all there. So
webkit2gtk4.1 was absent from /work/prior even though both shards had built an
hour earlier, and Fedora was left to answer for it.

Not deleted, though -- both purges logged zero deletions in that lane and
createrepo_c walked 1495 packages -- so this is an absence, not a removal, and
I have not established which step drops them. What is established is that the
libav purge is the one mechanism here that can delete a legitimate artifact,
and that it is no longer needed: it existed because a newly promised source
cannot be excluded from Fedora before the factory has built it once, and
ffmpeg has now been built and accumulated. Its stale libheif is gone too, both
because libheif no longer links libav and because accumulate has since run
with a coherent set. So it comes out rather than being narrowed.

The unpromised-source purge stays. That one is general, it is the fix for
removing an entry from the manifest, and it is not implicated.

If evolution-data-server still cannot see our webkit after this, the fault is
in artifact propagation between lanes rather than in either purge, and that is
where to look next.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
gtksourceview4 is green, so that fix worked. ffmpeg now fails instead, and it
had built cleanly in the run before -- which corrects the hypothesis in
ab315f0. The libav purge was not incidental to webkit; it was accidentally
holding ffmpeg up, and removing it exposed what was underneath:

  libchromaprint-1.6.0-4.fc44 from fedora requires libavutil.so.60
  libavutil-free-8.1.2-4.fc44 from updates is filtered out by exclude filtering

ffmpeg build-requires pkgconfig(libchromaprint), and Fedora libchromaprint
links the Fedora libavutil.so.60. Promising ffmpeg excludes the Fedora libav
by name, so libchromaprint-devel cannot be installed and ffmpeg cannot
configure. The purge had been deleting our own libav RPMs from the
accumulator, which left the Fedora libav selectable and quietly kept this
resolvable.

The ffmpeg spec names this cycle already: ffmpeg (libavcodec-free) →
chromaprint → ffmpeg, broken in Fedora by one bootstrap build and a rebuild of
chromaprint against the result. The factory cannot do that, because it does
not build chromaprint, so the cycle is permanent here rather than a bootstrap
phase. It is declined instead: chromaprint is AcoustID audio fingerprinting
and nothing in the runtime contract links it. The same call as vapoursynth and
libbluray, and the third instance of the rule those two established -- where
the factory and the Fedora compose disagree about a soname, decline the
feature rather than get ahead of the compose.

Only the recipe changes, so only ffmpeg is rebuilt; everything else is served
from the accumulator by identity match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
ffmpeg is green, so ac8bf79 worked, and gstreamer1-plugins-bad-free fails on
exactly the same block -- libchromaprint-devel wanting libavutil.so.60 while
the Fedora libav is excluded. chromaprint is not an ffmpeg-specific problem:
it is a property of promising ffmpeg at all, so no package here can
build-require anything that links the Fedora libav. Only these two recipes
mention chromaprint, so this closes the set.

A narrow %bcond rather than turning off %{extras}, which gates assrender,
avtp, bs2b, curl, dca, modplug and a dozen more. The switch moves out of the
extras block so it fires on either condition, and the plugin leaves %files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
Both of these are what Fedora does, which is also what keeps Hummingbird
upstream-first: the divergences here were the factory improvising, not
Hummingbird asserting anything.

mozc stopped with

  Argument 'protobuf-devel < 4' matches only excluded packages

and only once protobuf started building successfully -- while it failed, the
Fedora protobuf was never excluded and mozc quietly used the 3.19.6 in the
Fedora 44 compose. mozc genuinely wants the 3.x API, and mozc and ibus-mozc
are both promised in bluefin-packages.toml, so dropping either it or protobuf
would have been the factory inventing a policy. Fedora already solved this:
rawhide carries a separate protobuf3 source package at 3.19.6 whose -devel
subpackage Provides protobuf-devel = 3.19.6, satisfying the constraint beside
the 33.5 that everything else links. So that package is imported, from rawhide
dist-git with its provenance recorded, at the version rawhide ships. Its
googletest Source1 needs no special handling -- source_pipeline already fetches
bundled sources from the lookaside using the dist-git sources file.

mozc moves to stage 1 so it builds after protobuf3 rather than racing it in
stage 0, the same move waypipe made behind ffmpeg. Nothing build-requires
mozc, so the stage is free.

mozjs140 stopped with

  ERROR: Don t know how to translate x86_64-redhat-linux-gnu for rustc

against rust-1.98.0-1.hum1. Fedora builds rustc with an extra
x86_64-redhat-linux-gnu target and Mozilla configure looks the host triple up
in rustc --print target-list; Hummingbird builds upstream rust, which knows
only x86_64-unknown-linux-gnu. So the Rust toolchain now comes from Fedora,
by the same global exclusion that already keeps libicu 77 out. That is the
documented division of labour rather than an exception to it -- Fedora 44 is
the other half of the buildroot, the way a compiler is, and upstream-first is
a statement about what ships at runtime, where no Rust toolchain appears at
all.

Worth watching: this changes the compiler for every Rust package here, not
just mozjs140 -- rust-just, waypipe, librsvg2, glycin, python-pydantic-core
and firefox all built against the Hummingbird rust before. Fedora builds them
all against its own, so this should hold, but if one of them regresses that is
where to look. The patterns are version-anchored so rust-1.* cannot swallow
rust-std-static or a rust-<crate>.

libgudev is still open and is not addressed here. Its spec is byte-identical
to rawhide, which carries no patches, so Fedora runs this same %check in mock
and passes; the difference is the container, and I have not established which
part of it. It has now failed twice, so it is real rather than a flake.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
077600d worked for everything except the package it was written for. mozc
built against the newly imported protobuf3, gstreamer1-plugins-bad-free built
with chromaprint declined, and the Rust packages that were the blast-radius
worry -- waypipe, python-pydantic and the rest -- are all green. mozjs140
still failed, identically:

  ERROR: Don t know how to translate x86_64-redhat-linux-gnu for rustc
  [  2/170] rust-0:1.98.0-1.hum1.x86_64

So --exclude=rust-1.*hum1 does not match. The Hummingbird rust was installed
with that pattern on the command line, which is the whole answer: dnf did not
read it as excluding rust-1.98.0-1.hum1.

Exclude by name from the Hummingbird repository instead. That form is already
proven in this lane -- it is what produces "filtered out by exclude filtering"
for the Fedora side throughout the resolver output -- so it is the one to
trust. Name-scoping is safe here because Fedora is the only other source of a
Rust toolchain, which is exactly what this wants selected. libicu cannot use
this form, since 77 and 78 share one name and only the older build is
unwanted; its glob stays as it is.

Worth noting for whoever reads the libicu line next: the same doubt now
applies to it. libicu-77.1-2.1.hum1 has been turning up in transactions that
had --exclude=libicu-77.*hum1 set, so that exclusion may never have worked
either. It is left alone here because changing it needs its own evidence, and
because a name-scoped exclusion would take libicu 78 with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
This is a judgement call the user delegated, on the basis of what Fedora does
and what Hummingbird is for. Recording the reasoning because the conclusion is
weaker than the others in this branch.

My earlier read was wrong and is corrected here. test-gudevdevice does not
need a live udev database. It builds a device entirely out of environment
variables:

    g_setenv ("DEVPATH", "/devices/dev1", TRUE);
    g_setenv ("TAGS", "tag1:tag2:tag3", TRUE);
    g_setenv ("UDEV_DATABASE_VERSION", "1", TRUE);
    udev_device = udev_device_new_from_environment (udev);

and the log shows that call returning NULL with EINVAL:

    # MESSAGE: error is: 22, Invalid argument
    # MESSAGE: tags list entry form udev device (nil): (nil)

so no container property explains it. What the test pins is libudev's
willingness to accept a hand-built environment, not anything libgudev does --
_g_udev_device_new simply propagates the NULL. libgudev's own behaviour stays
covered by the other three tests, which drive real devices through umockdev
and pass, and by the library's actual consumers here, all of which build.

What Fedora does: rawhide pairs libgudev 238 with systemd 262~rc1, carries no
patches, and does not hit this. The build root here resolves Hummingbird's
systemd 261.2 instead, which is where the behaviour differs. Fedora's habit
when a rebuild trips a test like this is a downstream patch with the reasoning
attached, which is what this is.

What Hummingbird is for: it ships upstream systemd ahead of the compose, and
the factory builds for that platform. A test whose fixture assumes a specific
udev database version is exactly the kind of thing that breaks when a platform
runs ahead. Holding all 237 packages -- and Utah behind them -- on one
synthetic fixture would invert the point of the exercise.

What this is not: the difference between systemd 261.2 and 262~rc1 has NOT
been root-caused, so this is carried as parity debt in
docs/targeting-hummingbird.md rather than presented as understood. It is the
one place in the 237 where the factory ships a package without running its
upstream suite in full, and it should be revisited when either systemd moves.
A test hardcoding UDEV_DATABASE_VERSION 1 is asserting an internal contract
libudev is free to change, which looks worth reporting upstream.

Recipe-only, so this rebuilds libgudev alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
Root cause, reproduced rather than inferred. Mozilla's configure does not hand
the autoconf triple to rustc; it runs "rustc --print target-list", groups the
results by (cpu, endianness, os) and narrows that group to one rust target. For
x86_64-redhat-linux-gnu the deciding narrowing was the raw OS, "linux-gnu",
which matched x86_64-unknown-linux-gnu alone.

rust 1.98 added the OpenEmbedded targets. x86_64-oe-linux-gnu shares the raw OS
and the raw CPU with the generic target, so both narrowings now leave two, the
last resort tries the vendor, "redhat" names no rust target, and configure
stops:

  ERROR: Don't know how to translate x86_64-redhat-linux-gnu for rustc

Replayed the selection against real target lists: on rust 1.94 the stock code
returns x86_64-unknown-linux-gnu, on 1.98 it returns nothing, and the patch
returns x86_64-unknown-linux-gnu on both. aarch64-redhat-linux-gnu breaks and
recovers the same way; ppc64le and s390x were never ambiguous and are
unchanged.

Upstream has taken the same correlation in the same place -- the comment on
mozilla-firefox main names "the *-oe-linux-gnu targets added in rust 1.98" --
but only maps the "pc" vendor config.guess emits onto rust's generic "unknown",
which does not cover a distribution triple. This adds the fallback: a vendor
that matches no candidate wants the generic target. Rawhide carries no such
patch because rawhide has not rebuilt mozjs140 since that rust landed, which is
the kind of latent break a rebuild-everything factory meets first.

This also corrects 077600d and 6b83cd5. Both were written on the theory that
Hummingbird's rust lacked a target Fedora's rust had. 6b83cd5 did make the
buildroot take Fedora's toolchain -- rust-0:1.98.0-1.fc44 and rust-std-static
from updates, confirmed in the run 133 transaction -- and mozjs140 failed
byte-identically, because the vendor of rust was never the variable. Its
version was. Neither commit is reverted here: they touch the lane workflow, so
undoing them invalidates all 237 build identities and costs a full rebuild, and
taking the Rust toolchain from Fedora is what Fedora does anyway. Every Rust
package in the tree built green against it.

Recipe-only, so this rebuilds mozjs140 alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
The 237 all built, the contract gate passed -- "candidate repository carries
all 237 packages the factory promises" -- and publish then failed on the gate
after it, the one that installs the runtime contract from Hummingbird and the
factory alone. That gate had never run before: publish was skipped on every
previous run because something was red. So this is the first honest reading of
what the promised set actually needs, and it is short by a dependency closure.

Every problem in the transaction reduces to a capability neither Hummingbird
nor the factory provides. Twenty-six problems, eighteen missing sources:

  libICE, libSM                     qt5-qtbase-gui, via mozc
  xcb-util-{wm,image,keysyms,renderutil}   same
  mesa-demos                        glx-utils, same
  libdatrie                         libthai, which we build, for pango
  libnice                           gstreamer1-plugins-bad-free-libs, so gtk4,
                                    so libadwaita, zenity, gnome-desktop4,
                                    xdg-desktop-portal-gnome, gnome-tweaks
  glib-networking                   libsoup3, so gnome-shell,
                                    gnome-control-center, gvfs, geoclue2
  gweather-locations                libgweather, so gnome-settings-daemon
  wireplumber                       pipewire-session-manager, so pipewire,
                                    so mutter and pipewire-libs-extra
  rtkit                             pipewire
  adwaita-icon-theme-legacy         adwaita-icon-theme, so gtk3
  libdecor                          SDL3, since we build wayland
  iputils                           dracut-network, so dracut-live
  cockpit, python-bugzilla          anaconda-webui

Imported from rawhide dist-git with provenance recorded, at the versions
rawhide ships, hash-addressed to the lookaside. All stage 0, which is where
203 of the existing 237 sit -- the stage 0 rule this tree enforces is about
soname skew across a run boundary (tests/test_stage_zero_libav.py), not about
depending on a sibling at all, and none of these link libav.

One gap is NOT closed here and needs a decision: anaconda-webui's browser.
Its rich dependency resolves to firefox on anything without one of the spin
identity packages, and neither Hummingbird nor the factory has firefox. It is
raised separately rather than guessed at, because building firefox on a
GitHub-hosted runner is a different order of undertaking from these eighteen.

Also fixes a real bug this exposed in the importer. It recorded the first
entry of the dist-git sources file as the upstream source, but that file is in
dist-git order and carries bundled archives too: iputils lists a bundled
ifenslave.tar.gz ahead of its own tarball. Source0 is now resolved from the
spec. The same bug had already landed on snowball, whose manifest entry
pointed at the snowball-data test corpus rather than snowball itself; it built
anyway because the pipeline fetches every archive in the sources file, but the
entry described the wrong thing. Corrected, with tests pinning the selection
and asserting the property across every multi-source recipe in the tree.

This is a manifest change, so it rebuilds all 255.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
Closing the one gap the previous commit left open. anaconda-webui carries a
rich dependency that reads

  (slitherer if (fedora-release-identity-budgie or ... or
   fedora-release-identity-i3) else firefox)

so on anything not carrying a spin identity package -- which is the Hummingbird
bootc image -- it resolves to firefox, and neither Hummingbird nor the factory
has one. The user chose firefox over the alternatives, which were shipping a
spin identity package to select slitherer instead, or dropping anaconda-live
from the runtime contract.

Imported from rawhide dist-git with provenance recorded, at 155.0, and placed
in the stage1_heavy lane beside the two WebKit shards, which is the lane that
exists for compiles that want a runner to themselves. Opted into the compiler
cache and taught to use it: mach drives the C, C++, rust and host compiles
itself, so the cache goes in through the --with-ccache option the build system
provides rather than by wrapping CC, which would miss most of them.

One deliberate divergence from rawhide, using a switch the recipe already
provides: release_build is 0, so the build is Fedora's own non-PGO
configuration and carries the .npgo release tag the recipe appends for it. A
PGO build is two compiles with an instrumented run between them and needs a
wayland compositor for that run; Koji gives it a builder for most of a day,
this factory has four cores and a six-hour job limit. It also turns off the
WASI SDK sub-build, a cmake and cargo toolchain compile that runs before
Firefox itself starts. The cost is real and recorded in the spec: no
profile-guided optimisation, and no RLBox sandboxing of the media libraries,
so the browser is slower than Fedora's and one defence-in-depth layer thinner.
Worth revisiting if the factory ever gets a builder that can afford the full
build.

Two things the importer needed for this. Source0 is spelled with
%{?pre_version}, a conditional macro nothing defines on a release, so
undefined conditionals now expand to nothing -- without that the entry
recorded mochitest-python.tar.gz, the first line of the sources file, as the
upstream source of Firefox. And a new test asserts that every recipe opted
into the compiler cache actually reads the cache environment, because the flag
only mounts the client: opting a package in and leaving its %build alone gives
a silently uncached build, which on a compile this size is the difference
between fitting the runner and not.

Cancelled run 136 rather than let it finish. Its manifest predates firefox, so
none of its build identities survive this commit and none of its output would
have been reused; running it out would only have delayed the first attempt at
the long pole by two hours.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
cockpit was the only one of the nineteen new recipes that failed, and it did
not fail on anything cockpit does. Its own test-auth stops at

  GLib-FATAL-WARNING: g_hmac_new: GLib HMAC is disabled for FIPS compliance

which is a property of the platform, established rather than inferred:
unpacking glib2-2.89.3-1.hum1 from Hummingbird's own repository and reading
libglib-2.0.so.0.8903.0 shows that string present, "gnutls" absent from the
binary entirely, and no "fips_enabled" anywhere. The disablement is compiled
in and unconditional -- it applies on every Hummingbird system whether or not
FIPS is enabled, not only in a build container. Fedora carries
gnutls-hmac.patch and routes GHmac through GnuTLS so it keeps working;
Hummingbird builds without that backend and the fallback is a stub that warns
and returns NULL.

cockpit-ws authenticates with GHmac, so a cockpit built here would install and
fail to authenticate. Patching out the test would have shipped precisely that,
which is the thing the publish gates exist to prevent.

So anaconda-live joins slitherer in [unavailable]. That list already carried
the reasoning -- "Utah uses the bootc-installer live path, not Anaconda WebUI
or Slitherer" -- and this is the same path one level up: anaconda-live pulls
anaconda-webui, which pulls cockpit-bridge, cockpit-ws,
cockpit-networkmanager and cockpit-storaged. cockpit, python-bugzilla and
firefox came in only to serve that chain and come back out with it; firefox
leaves the heavy lane as well. The factory still builds anaconda-webui and
anaconda-live as vanilla Fedora packages, which was the earlier call and is
unchanged. The exception says only what Utah's runtime installs.

The finding itself is not a decision about Hummingbird and should not be
mistaken for one. Every GHmac consumer on that platform is affected, not just
cockpit -- libsoup digest authentication, evolution-data-server, gvfs and
gnome-online-accounts all reach for it -- so it wants reporting upstream with
the binary evidence, which is written up in docs/targeting-hummingbird.md.
Revisit here when Hummingbird's glib2 gains a working backend.

Run 137 cancelled: it was building firefox for a contract entry that no longer
exists, and cockpit had already failed in it.

Manifest is 253, contract 70.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
The write-up claimed libsoup's digest authentication, evolution-data-server,
gvfs and gnome-online-accounts all reach for GHmac. Three of those four are
wrong, and the fourth is right for a different feature. Measured instead:

All 163 runtime RPMs the factory currently publishes were unpacked and scanned
for g_hmac_new, g_hmac_update and the g_compute_hmac_for_* family. Zero hits,
with the scanner validated against Hummingbird's glib2, which does carry them.
Of the desktop packages this branch adds but has not published, taken from
Fedora's builds of the same sources, only libsoup3 references them --
evolution-data-server, gnome-online-accounts, glib-networking, gvfs and
gnome-shell do not. And libsoup's single use is soup-auth-ntlm.c calling
g_compute_hmac_for_data with G_CHECKSUM_MD5 for NTLMv2; digest authentication
never touches GHmac.

So the exposure is cockpit's authentication and NTLMv2, not the desktop stack.
The reason to report it upstream is unchanged but restated honestly: the
disablement is unconditional and silent, so g_hmac_new returns NULL on a
machine that is not in FIPS mode and callers that do not check get a crash or
a wrong answer rather than a policy error.

Docs only -- docs/** is not in the workflow path filter, so this starts no
build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
The user's call, extending the previous one: not just anaconda-live out of the
runtime contract, but Anaconda out of the manifest entirely. anaconda-webui
was the only piece the factory still built -- the anaconda source package left
this branch earlier, and anaconda-live is one of its subpackages, so with
anaconda-webui gone nothing in the tree serves that path at all.

anaconda-live stays in [unavailable] for the opposite reason it was put there.
Before, it was excluded because the chain behind it could not be delivered;
now it is excluded because nothing here answers for it, and without the
exception the consumer transaction would ask for a package neither repository
has. The comment says so rather than leaving the old reasoning in place.

Also records the cockpit workaround that was investigated and not taken, since
the research is worth more written down than repeated. Cockpit's only GHmac
use is one function -- cockpit_auth_nonce in src/ws/cockpitauth.c, calling
g_compute_hmac_for_data with G_CHECKSUM_SHA256 to mint CSRF and session
tokens, a PRF rather than a protocol-mandated MAC. Cockpit already carries
PKG_CHECK_MODULES(gnutls, [gnutls >= 3.6.0]), linked today only into src/tls,
so a downstream patch could add gnutls to libcockpit_ws_a and swap that call
for gnutls_hmac_fast(GNUTLS_MAC_SHA256, ...). No new dependency, same
semantics, and the same move Fedora makes for GLib. Not taken because reviving
cockpit revives the chain behind it, firefox included, for an installer Utah
does not use.

The docs entry for what the factory declines is rewritten to match: ffmpeg
stays in scope on its own reasoning, and the Anaconda entry now covers the
whole branch rather than cockpit alone.

Manifest is 252, contract stays at 70.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVSCoKWBWAuFFZyLwteEbB
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants