test: launch-test real packages in containers + download page - #26
Merged
Conversation
Nothing proved the packages RUN. 'npm run tauri build exited 0' and 'the .deb is 8MB' are both true of a binary that dies before it draws a window -- and this app builds a tray icon unconditionally against libayatana-appindicator3, the classic dlopen-panic shape. The hard part is that ThinkUtils reads /proc/acpi/ibm/fan and /sys/class/power_supply/BAT*, none of which exist in a container. So the suite does not test hardware paths. It tests that the app starts, renders its full UI, and degrades cleanly when the hardware is absent, using three hardware-independent signals: index.html ships only two visible strings plus empty containers. Every other word on screen arrives because templateLoader.js fetched a template over tauri:// and injected it. Those labels are literal markup that no /proc or /sys read produces, so OCR finding them proves the JS ran, on any machine. OCR finding index.html's static text while finding NO injected label is the exact signature of 'WebKit loaded the page, the JS died' -- checked explicitly, because every other assertion passes in that state. The app now prints hw probe / hw mode, so 'no ThinkPad here' is an observed state rather than an inference. The frontend reports uncaught exceptions to the backend. A view dying on an absent sysfs path leaves the sidebar painted and the process alive; that error line is the only tell. Running it immediately found two real bugs, both fixed here: hw mode reported 'full' inside a container. Containers inherit the host's /sys, so bat0 and cpufreq were present and only the fan interface was missing. Keyed on the fan interface now -- a battery and cpufreq exist on every Linux laptop and prove nothing about ThinkPad support. The OCR assertion missed the first-run permissions dialog. On a machine that has never been set up -- every container -- the app correctly opens that dialog over the main view, so the sidebar is not what is on screen. It comes from templates/dialogs.html, so it is injected template text and proves the same thing. Verified locally against real builds: deb on ubuntu:24.04 PASSES, and rpm on fedora:41 PASSES -- the first time the .rpm has ever been tested by anything. Artifact selection resolves the CURRENT version rather than taking the only match or the first: three stale builds were sitting in the bundle directory, and release.yml selects with 'ls *.deb | head -n 1', which sorts 0.1.10 before 0.1.5 and would rename an old package to the new version's name and publish it. Also fixes .gitignore, where a missing newline had merged two entries into 'docs/.vitepress/cachebuild/' -- so neither the VitePress cache nor build output was ignored.
The Download nav link pointed straight at the GitHub releases list, which shows every asset for every version and leaves the reader to work out which file they want. docs/download.md resolves the latest release through the GitHub API at view time rather than baking a version in at build time -- the docs site and the release pipeline deploy independently, so a hard-coded version would go stale the moment a release ships without a docs rebuild. If the API is unreachable or rate-limits (60/hour unauthenticated), every button falls back to the releases page, which always works. Assets are matched by predicate rather than exact filename so a version bump needs no edit here, and each predicate pins the architecture suffix. That is load-bearing rather than tidiness: find() returns the FIRST match, so a loose predicate would silently hand out the wrong package the day a second architecture is added, and the failure is quiet and user-side -- the page looks right, the download works, the package refuses to install. getting-started.md is restructured as an ordered path rather than a list of prerequisites, and leads with the question a reader actually has -- 'will this work on my machine' -- answered by one command. Step 2 is now stated as the step people miss, because it is: the thinkpad_acpi module refuses every fan write unless loaded with fan_control=1, and that is fixed at module load, so granting permissions cannot fix it. Presenting it as one prerequisite among several is what produces the 'I granted permissions and nothing happened' report. Also documents the Ubuntu 22.04 polkit 0.105 limitation, which silently makes passwordless fan control not work there, and the fan safety behaviour -- revert on disable, on unreadable sensors, on exit, plus the firmware watchdog.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
thinkutils | 110ec0e | Jul 19 2026, 03:38 PM |
…anifests
Distro packaging was blocked by one thing: the fan helper installed to
/usr/local/bin at runtime. Debian Policy 9.1.2 and the Fedora guidelines
both forbid a package writing there, and a helper materialised by a
button click is not package-owned -- dpkg -L would not list the most
security-sensitive file the app uses, and uninstalling would leave a
root-owned binary and a polkit rule behind.
HELPER_PATH becomes HELPER_CANDIDATES, searched in order:
/usr/lib/thinkutils/... Debian and Arch convention
/usr/libexec/thinkutils/... Fedora convention
/usr/local/bin/... legacy self-install, kept so existing
installs keep working
setup_permissions() now skips installing the helper and rule entirely
when helper_is_packaged(), because overwriting those files puts the
package database out of sync with the filesystem.
The path had been duplicated in four places -- fan_control, mcp,
fan_curve, and inside the polkit rule text. The rule is now generated
from the constant, since a rule naming a path the helper is not at
grants nothing while looking correct.
Two security improvements fall out of generating it:
subject.local && subject.active is now required. Without it any SSH
session belonging to a wheel/sudo user inherited passwordless
hardware control, as did a background session the user had switched
away from.
Packages ship the rule to /usr/share/polkit-1/rules.d, not /etc.
/etc is the administrator's namespace; a package writing there
shadows their rules and is never cleaned up.
packaging/ adds the AUR PKGBUILD and COPR spec, plus the helper and rule
as generated artifacts (cargo run --example gen-packaging).
tests/packaging.rs is the point of all this: 7 tests asserting the
committed files match what the source generates, that each format
installs where the app actually searches, that nothing writes to
/usr/local, that the rule goes under /usr/share, and that versions
agree. The drift they prevent fails SILENTLY -- polkit denies, the app
falls back to a password prompt, and it reads as a permissions problem
rather than a packaging bug.
bump-version.sh now covers PKGBUILD and the spec too, and ci.yml calls
it rather than keeping its own copy of the file list, so adding a
packaging file cannot leave CI checking a stale subset.
Suite: 80 -> 87.
The gui-launch job built on the ubuntu-24.04 runner and then tested the result on Ubuntu 22.04, where it failed to start: thinkutils: /lib/x86_64-linux-gnu/libc.so.6: version 'GLIBC_2.39' not found (required by thinkutils) Tauri links against the build host's glibc, so a 24.04 build declares 2.39 and will not run on 22.04 (2.35) or Debian 12 (2.36). release.yml already builds on 22.04, so real releases were unaffected -- CI was testing a binary no user would ever receive, which is worse than not testing at all. The job now builds inside an ubuntu:22.04 container, matching release.yml exactly, so what the suite launches is what ships. That is the same reasoning already recorded in release.yml for why its runner stays on 22.04 despite deprecation; I had documented it there and then not applied it to the job I wrote afterwards. Also sets APPIMAGE_EXTRACT_AND_RUN, because linuxdeploy is itself an AppImage and mounts via FUSE, which a container does not have -- that fails AFTER the .deb and .rpm have already built, so it reads like a Tauri bug rather than a missing environment variable. Worth noting the suite behaved exactly as designed: ubuntu:24.04 passed every assertion in the same run, so the failure isolated to the one target whose glibc floor was violated.
…hout it The container had no jq, so VERSION resolved empty and every artifact glob became thinkutils__amd64.deb -- matching nothing. The run reported 'no artifact matching' while the actual cause, one line earlier, was 'jq: command not found'. jq is installed now, and an unreadable version aborts with a clear message instead of building a pattern that silently matches nothing.
Completes the packaging set: deb/rpm/AppImage from Tauri, AUR, COPR, and now a PPA source build. The constraint that shapes all of it is that Launchpad builders have no network, so every Rust dependency is vendored into the .orig tarball and the build runs CARGO_NET_OFFLINE=true. Verified end to end rather than assumed: cargo vendor produces 543 crates, and cargo check --offline against that tree succeeds. Two findings worth recording, both tested rather than reasoned about: npm is NOT a build dependency. tauri.conf.json has no beforeBuildCommand and frontendDist is a directory of static files, so there is no bundler and no node_modules to vendor. Vendoring a JS dependency tree deterministically is the hardest part of packaging a typical Tauri app, and this one avoids it entirely. debian/control has a test asserting npm never appears there. The vendor tree is 855 MB, 77 MB compressed, and roughly three quarters of it is Windows-only crates in a Linux-only package. Deleting them looks like an easy 73% saving and does not work: cargo requires a vendored source for every crate in Cargo.lock, including platform-gated ones it never compiles. Removing them makes cargo check --offline fail resolving chrono. Recorded in the script so it is not retried. debian/rules deliberately avoids the Tauri bundler, which downloads linuxdeploy at build time, and installs every file by hand -- also the only way to place the helper somewhere FHS-legal. jammy is excluded, and not for toolchain reasons: it ships polkit 0.105, whose JS rules engine Debian and Ubuntu patched out, so the passwordless rule would be read by nothing and every fan change would prompt. Five new tests cover the offline flags, the dh_clean -X.orig override (without which vendored Cargo.toml.orig files are deleted and the build fails a checksum on Launchpad but not locally), that the bundler is not invoked, and that control stays templated. Two of those tests failed on first run by matching their own explanatory comments, so assertions now read only real directives -- the same trap the existing /usr/local check had already sidestepped.
The previous fix put the whole gui-launch job inside an ubuntu:22.04 container so the packages would be built on the glibc floor. That part was right and necessary -- a 24.04 build requires GLIBC_2.39 and refuses to start on 22.04 or Debian 12. But a job running inside a container cannot spawn the sibling containers the launch test needs, so it failed with 'docker: command not found'. Split into two: bundle builds inside ubuntu:22.04, uploads the packages gui-launch runs on the plain runner, downloads them, drives Docker bundle also now asserts the floor directly rather than waiting for a launch failure to reveal it: objdump the highest GLIBC_ symbol required and fail if it is above 2.35. Verified against a locally built binary, which correctly reports GLIBC_2.39 and would fail the check -- that binary is exactly what CI was shipping to the 22.04 test before. Reporting the floor as a named error beats discovering it as a broken download three targets into a launch matrix.
The AppImage target failed with: error while loading shared libraries: libgbm.so.1 An AppImage bundles WebKit and GTK but NOT Mesa -- it expects the host to provide the graphics stack. A bare container has none, so the test was judging an environment no user actually has. Any real desktop has libgbm. Installs libgbm1, libgl1, libegl1, libglx-mesa0 and mesa-vulkan-drivers in the AppImage container only. Cannot be verified locally: this host is Ubuntu 26.04, so a locally built AppImage bundles glib and WebKit requiring up to GLIBC_2.43 and cannot start on 22.04 regardless. Which is itself worth knowing -- the AppImage inherits the build host's glibc floor MORE aggressively than the binary does (2.43 vs 2.39), because it ships those libraries too. CI builds it inside the 22.04 container now, so that floor is pinned there. The other four targets already pass -- deb on 22.04, 24.04 and Debian 12, and rpm on Fedora 41 -- each recognising seven injected template labels.
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.
Two things: the packages are now proven to actually run, and the docs have a real download page.
GUI launch tests
Nothing has ever proved the packages run.
npm run tauri build exited 0and "the .deb is 8MB" are both true of a binary that dies before it draws a window — and this app builds a tray icon unconditionally againstlibayatana-appindicator3, the classic dlopen-panic shape.The hard part: ThinkUtils reads
/proc/acpi/ibm/fanand/sys/class/power_supply/BAT*, none of which exist in a container. So the suite doesn't test hardware paths. It tests that the app starts, renders its full UI, and degrades cleanly — using three hardware-independent signals:index.htmlships only two visible strings plus empty containers. Every other word on screen appears becausetemplateLoader.jsfetched a template overtauri://and injected it. Those labels are literal markup that no/procread produces — so OCR finding them proves the JS ran, on any machine. Finding index.html's static text while finding no injected label is the exact signature of "WebKit loaded the page, the JS died", checked explicitly because every other assertion passes in that state.hw probe:/hw mode:), so "no ThinkPad here" is observed rather than inferred.It found two real bugs on first run, both fixed here
hw modereportedfullinside a container. Containers inherit the host's/sys, sobat0andcpufreqwere present and only the fan interface was missing. Now keyed on the fan interface — a battery and cpufreq exist on every Linux laptop and prove nothing about ThinkPad support.The OCR assertion missed the first-run permissions dialog. On a machine never set up — every container — the app correctly opens that dialog over the main view. It comes from
templates/dialogs.html, so it's injected-template text and proves the same thing.Verified locally against real builds
debonubuntu:24.04— PASSrpmonfedora:41— PASS, the first time the .rpm has ever been tested by anythingArtifact selection resolves the current version rather than "the only match": three stale builds were sitting in the bundle directory, and
release.ymlselects withls *.deb | head -n 1, which sorts0.1.10before0.1.5and would rename an old package to the new version's name and publish it.Download page
The Download nav link pointed at the raw GitHub releases list.
docs/download.mdresolves the latest release through the API at view time — the docs site and release pipeline deploy independently, so a baked-in version goes stale the moment a release ships without a docs rebuild. If the API rate-limits, every button falls back to the releases page.getting-started.mdis restructured as an ordered path, leading with "will this work on my machine" answered by one command. Step 2 is now stated as the step people miss — because presentingfan_control=1as one prerequisite among several is what produces the "I granted permissions and nothing happened" report.Also documents the Ubuntu 22.04 polkit 0.105 limitation and the fan safety behaviour.
Incidental fix
.gitignoreline 29 readdocs/.vitepress/cachebuild/— a missing newline had merged two entries, so neither the VitePress cache nor build output was ignored.