Skip to content

feat(ci): build, test and package for aarch64 as well as x86_64#43

Merged
vietanhdev merged 6 commits into
mainfrom
feat/multiarch-aarch64
Jul 20, 2026
Merged

feat(ci): build, test and package for aarch64 as well as x86_64#43
vietanhdev merged 6 commits into
mainfrom
feat/multiarch-aarch64

Conversation

@vietanhdev

Copy link
Copy Markdown
Owner

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_acpi is an x86 platform driver, so there'd be no hardware to run on. Half of that is still true and half isn't:

  • Fan control genuinely cannot work on ARM — that's a kernel limitation
  • But the ThinkPad X13s is aarch64, and battery thresholds (generic power_supply API), CPU governor, power profiles and system monitoring aren't ThinkPad-specific at all

The app already detects an absent /proc/acpi/ibm/fan and 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-on picks the CPU; container: ubuntu:22.04 pins the glibc floor. Separating those also retires the standing note in release.yml about the deprecated ubuntu-22.04 runner 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:

  1. uname -m assertion in every arch job. If GitHub renames or withdraws the arm label, runs-on falls back silently — CI would build x86_64 twice, publish it under an arm64 name, and stay green.
  2. file(1) check on the built binary in Verify package contents — the last point a mislabelled package is catchable before users get it.
  3. gui-launch runs 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

Channel Declaration
AUR arch=('x86_64' 'aarch64')
COPR ExclusiveArch: x86_64 aarch64
PPA Architecture: any

The PPA asserts the mechanism, not a list — Launchpad builds whatever the series enables, so narrowing to amd64 would 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_builds reads the arches out of release.yml rather than hardcoding them, so it can't pass while disagreeing with the workflow it tracks.

Note for COPR: widening ExclusiveArch alone 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, since navigator.platform is 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:

packaging_architectures_match_what_release_actually_builds ... FAILED
  PKGBUILD arch=() omits aarch64, which release.yml builds
debian_control_delegates_architecture_to_launchpad ... FAILED
  the PPA must delegate architecture to the series

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.

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.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 20, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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.
@vietanhdev
vietanhdev merged commit 597abcc into main Jul 20, 2026
11 of 12 checks passed
@vietanhdev
vietanhdev deleted the feat/multiarch-aarch64 branch July 20, 2026 05:11
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.

1 participant