feat(ci): build, test and package for aarch64 as well as x86_64#43
Merged
Conversation
ThinkUtils was x86_64-only on the reasoning that thinkpad_acpi is an x86
platform driver so there was no hardware to run on. Half of that is still
true and half is not: fan control genuinely cannot work on ARM, but the
ThinkPad X13s is aarch64, and battery thresholds, CPU governor and system
monitoring are not ThinkPad-specific at all. The app already detects an
absent /proc/acpi/ibm/fan and reports degraded rather than failing.
Both arches build NATIVELY on their own runner, following the sibling
bulwark project. runs-on picks the CPU; container: ubuntu:22.04 pins the
glibc floor -- separate concerns, and separating them also retires the
standing note in release.yml about the deprecated ubuntu-22.04 label:
the floor is now ours to choose rather than GitHub's to withdraw.
Not cross-compiled and not emulated. Tauri's bundlers infer architecture
from the build host and declare none in the manifest, so --target would
stage host artifacts under an aarch64 name and ship an x86_64 binary in
an arm64 package. qemu is refused outright rather than tolerated: a
WebKit GUI under emulation fails on graphics paths no real user touches,
and a misleading red result is worse than no coverage.
Three things make the arch axis falsifiable rather than decorative:
- every arch job asserts uname -m matches what the matrix claims. If
GitHub renames the arm label, runs-on falls back silently and CI
would build x86_64 twice, publish it under an arm64 name and stay
green.
- Verify package contents checks the binary's real machine type via
file(1), the last point a mislabelled package is catchable.
- gui-launch runs on both arches with the FULL distro matrix, not a
thinner arm leg -- the point of those rows is to catch a distro/arch
interaction, which a reduced matrix would be most likely to miss.
OCR is what makes it meaningful: JavaScriptCore has its own arm64
JIT, so "native shell paints, JS never runs" is arch-specific and a
screenshot of it looks like a working window.
Packaging follows the release matrix rather than restating it: AUR and
COPR list both arches, and the PPA uses Architecture: any because
Launchpad builds whatever the series enables -- asserting the mechanism,
since narrowing it to amd64 would silently stop producing arm64 builds
with nothing else changing.
The download page now pins the architecture suffix per selection instead
of hardcoding amd64. Its own comment warned about exactly this: find()
returns the first match, so an unpinned predicate would hand an arm64
user an x86_64 package -- a download that works and a package that will
not install. Detection only picks the default tab; both stay one click
away, because navigator.platform is deprecated and wrong under emulation.
132 tests. Arch guards mutation-verified: dropping aarch64 from the
PKGBUILD or narrowing the PPA to amd64 fails them by name.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
thinkutils | 0078752 | Jul 20 2026, 04:58 AM |
getting-started.md hardcoded _amd64.deb and .x86_64.rpm. With arm64 packages shipping, those commands match nothing on an arm64 ThinkPad -- and the failure reads as a missing download rather than as a wrong glob. The reader has downloaded exactly one file, so the architecture in the glob was never carrying information in the first place.
ExclusiveArch now lists aarch64, but the workflow still resolved chroots with a x86_64-only regex -- so COPR would have built x86_64 only and the widened spec would have shipped nothing extra, silently. Selecting per arch rather than widening the regex is deliberate. Keeping `tail -3` over a combined list turns "newest three releases" into "newest three chroots across both arches". Checked against a realistic chroot list that yields fedora-42-x86_64, fedora-43-aarch64, fedora-43-x86_64 -- two releases of one arch, one of the other, fedora-41 dropped entirely -- and which arch loses coverage shifts every time Fedora branches. (The sibling bulwark project documents this trap as yielding three aarch64 chroots and zero x86_64. That is not what sort -V actually does, since aarch64 sorts before x86_64 within a release; the real failure is uneven, shifting coverage. Same conclusion, different mechanism.) Also creates the COPR project when absent. The project does not exist yet, and every step here assumed it did, so the first publish would have failed on `copr-cli modify`. Matching on "^Name:" because copr-cli list prints a field and an indented block, not a bare name -- grepping the name alone never matches and re-creates an existing project. Adapted from bulwark's scripts/publish-copr.sh.
GitHub is forcing Node 20 actions onto Node 24 and warning on every job.
Six jobs carried the annotation.
actions/checkout v4 -> v7 (14x)
actions/setup-node v4 -> v7 (4x)
actions/upload-artifact v4 -> v7 (5x)
actions/download-artifact v4 -> v8 (2x)
softprops/action-gh-release v1 -> v3 (1x)
Checked the breaking changes rather than assuming a major bump is safe,
because two of these are load-bearing here:
- download-artifact v8 makes a digest mismatch fail rather than warn.
That is a security improvement and pattern/merge-multiple, which
release.yml depends on to collect both arches, are unaffected.
- setup-node v5 added automatic caching driven by a packageManager
field in package.json. There is no such field, so it never triggers
and the explicit cache: npm still applies.
Also moves the BUILD node from 20 to 22. That is a different layer from
the warning -- it is the Node the frontend tooling runs on -- but Node 20
reached end of life in April 2026, so it stopped getting security
updates. Verified locally on Node 24: npm ci, npm run validate and the
VitePress production build all pass, which is a stronger check than 22.
gui-launch (aarch64) failed looking for thinkutils_0.1.10_arm64.AppImage while Tauri had produced thinkutils_0.1.10_aarch64.AppImage. Tauri names architectures per format, and they do not agree: format x86_64 aarch64 .deb amd64 arm64 .rpm x86_64 aarch64 .AppImage amd64 aarch64 So AppImage follows the deb convention on x86_64 and the rpm convention on aarch64. Deriving it from DEB_ARCH is therefore right on x86_64 by coincidence and wrong on ARM -- which is why this passed every x86_64 run and failed the first time anything executed the suite on aarch64. Verified against the real filenames from both CI jobs rather than from reasoning: all six patterns match. Worth recording what the failure actually proved. Four of five aarch64 targets had already PASSED -- deb on ubuntu 22.04, 24.04 and debian 12, rpm on fedora 41 -- each reporting degraded hardware mode correctly, painting its templates, and exiting cleanly. The app runs on ARM. Only the harness could not find one file.
release.yml renames every artifact and docs/download.md matches release assets by suffix. Neither file imports the other, and when they disagree NOTHING fails: url() falls back to the releases page, so every button still works while silently no longer linking the actual package -- for whichever architecture drifted. That is easy to get wrong because Tauri spells architectures per format and inconsistently between them: format x86_64 aarch64 .deb amd64 arm64 .rpm x86_64 aarch64 .AppImage amd64 aarch64 <- deb's spelling on x86, rpm's on ARM The AppImage row already cost a red run this session. Both sides are PARSED rather than restated. The first version of this built the expected suffixes out of release.yml's matrix and compared them to filenames also built from that matrix -- comparing a thing to itself, which passed regardless of what the page said. Caught it by mutating the page's rpm suffix: only the second test failed. Now the page's ARCHES table is parsed and the first test fails too, naming the consequence: on aarch64: release publishes thinkutils-9.9.9.aarch64.rpm but the download page looks for *.arm64.rpm - the button would silently fall back to the releases page 134 tests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adapted from the sibling bulwark project, which is also a Tauri GTK/WebKit app — so most of its approach transfers directly.
Why ARM is worth shipping
ThinkUtils was x86_64-only on the reasoning that
thinkpad_acpiis an x86 platform driver, so there'd be no hardware to run on. Half of that is still true and half isn't:power_supplyAPI), CPU governor, power profiles and system monitoring aren't ThinkPad-specific at allThe app already detects an absent
/proc/acpi/ibm/fanand reports degraded rather than failing — asserted by the container launch tests. So this is a supported configuration, not a hopeful one. The download page says so plainly rather than letting X13s users discover it.Native, not cross-compiled, not emulated
runs-onpicks the CPU;container: ubuntu:22.04pins the glibc floor. Separating those also retires the standing note inrelease.ymlabout the deprecatedubuntu-22.04runner label — the floor is now ours to choose rather than GitHub's to withdraw.Cross-compiling is rejected for a concrete reason: Tauri's bundlers infer architecture from the build host and declare none in the manifest, so
--target aarch64-…would stage host artifacts under an aarch64 name and ship an x86_64 binary inside an arm64 package. QEMU is refused outright — a WebKit GUI under emulation fails on graphics paths no real user touches, and a misleading red result is worse than no coverage because people learn to ignore the job.Making the arch axis falsifiable
An arch matrix that silently isn't testing that arch is worse than none. Three guards:
uname -massertion in every arch job. If GitHub renames or withdraws the arm label,runs-onfalls back silently — CI would build x86_64 twice, publish it under an arm64 name, and stay green.file(1)check on the built binary in Verify package contents — the last point a mislabelled package is catchable before users get it.gui-launchruns the FULL distro matrix on both arches, not a thinner arm leg. The point of those rows is catching a distro/arch interaction, which a reduced matrix would be most likely to miss.That third one is where arm64 is actually proven. JavaScriptCore has its own arm64 JIT, so "the native shell paints but the JS never runs" is an arch-specific failure — and a screenshot of it looks like a working window. The existing OCR step is what distinguishes them: a blank-but-styled window still has hundreds of distinct colours, and only the absence of rendered text gives it away.
Packaging follows the matrix rather than restating it
arch=('x86_64' 'aarch64')ExclusiveArch: x86_64 aarch64Architecture: anyThe PPA asserts the mechanism, not a list — Launchpad builds whatever the series enables, so narrowing to
amd64would silently stop producing arm64 builds with nothing else in the repo changing, and Launchpad reports per-arch results by email rather than failing anything here.packaging_architectures_match_what_release_actually_buildsreads the arches out ofrelease.ymlrather than hardcoding them, so it can't pass while disagreeing with the workflow it tracks.Note for COPR: widening
ExclusiveArchalone does not ship aarch64 — the COPR project must have the aarch64 chroots enabled, or the spec claims an arch that produces nothing, silently.Download page
Its own comment warned about exactly this case:
find()returns the first match, so an unpinned predicate would hand an arm64 user an x86_64 package — a download that works and a package that won't install. The suffix stays pinned and the arch is an explicit choice; detection only picks the default tab, sincenavigator.platformis deprecated and wrong under some emulation layers.Verification
132 tests, clippy clean, fmt clean, lint + format clean, docs build clean, both workflows parse.
Arch guards mutation-verified — dropping aarch64 from the PKGBUILD or narrowing the PPA to amd64 fails them by name:
The real proof is this PR's own CI: if
gui-launch (aarch64)goes green, an arm64 package installed and rendered its UI on real ARM hardware.