Skip to content

chore(flatpak): pin every build source to an immutable reference (#443) - #655

Merged
TheZupZup merged 4 commits into
mainfrom
thezupzup/adoring-ptolemy-o1soox
Sep 20, 2026
Merged

TheZupZup merged 4 commits into
mainfrom
thezupzup/adoring-ptolemy-o1soox

Conversation

@TheZupZup

@TheZupZup TheZupZup commented Sep 17, 2026

Copy link
Copy Markdown
Owner

Closes #443

The audit first

Of the 251 remote sources the committed manifest fetches, 250 were already pinned by URL + sha256 and one was not.

Input State before Now
ffmpeg, libplacebo (+ jinja, markupsafe, glad), libass, mpv archive URL + sha256, release tags unchanged
Flutter SDK, in the generated module tag + commit unchanged
Flutter SDK, in the hand-authored template tag: '3.44.7' only, and a tag can be repointed upstream tag + commit, held to the generated module and to .flutter-version
222 pub.dev archives + their hosted-hash entries URL + sha256 from pubspec.lock unchanged, and now checked against the lockfile offline too
16 Flutter engine/Dart SDK artifacts storage.googleapis.com URL + sha256 unchanged, plus "all artifacts belong to one engine hash"
SQLite amalgamation, mimalloc URL + sha256, pre-fetched where the plugin's CMake looks, URL_HASH in the committed patch unchanged, plus the patch's version has to be the locked plugin's
flatpak-flutter (the generator) TOOL_COMMIT, a full commit unchanged, now asserted to be a full commit
runtime, SDK, SDK extension, the app's own dir source not content-addressed, and cannot be documented, with the reason, and held to the manifest in both directions

So the pin itself is one line. What was missing is everything that keeps the pins from drifting apart afterwards, because nothing held:

  • the hand-authored template to any rule at all. Every existing check reads the generated manifest, so a floating source added where a human actually edits would be caught one regeneration later, if ever;
  • the template and the generated manifest to each other. Bumping mpv in the template (or hand-editing the generated manifest) builds something other than what the source of truth says;
  • .flutter-version to the Flatpak's Flutter. An SDK bump could leave the package building the old toolchain, silently, since the generated module's tag, commit and filename were tied to nothing;
  • the derived native pins to the lockfile. flatpak-flutter picks its per-plugin fixes by "highest entry not newer than the locked version", so a sqlite3_flutter_libs bump quietly reuses the previous version's patch and pre-fetched amalgamation instead of failing.

What this adds

  • flatpak/flatpak-flutter.yml: the Flutter source carries tag and commit. The tag is what the generator reads; the commit is what makes it a pin. Verified with git ls-remote --tags, and it is the same commit the generated SDK module already recorded, so the generated manifest is byte-identical. Nothing was regenerated in this PR.
  • scripts/check_flatpak_sources.py: reads both manifests, every generated module and source list they include, pubspec.lock and .flutter-version, and fails on a source with no sha256 (or only MD5/SHA-1), a git source without a full commit or tracking a branch, a URL that resolves at download time (HEAD, refs/heads/, latest, ?branch=, a default-branch snapshot), plain HTTP, a type with no content pin (svn, bzr, extra-data), a local path that is absolute or escapes the checkout, the two manifests disagreeing, a Flutter pin that only moved in one place, engine artifacts from two engines, a Dart package that drifted from the lockfile, a plugin patch applied to a version it was not written for, a FetchContent URL with no URL_HASH, and a TOOL_COMMIT that is not a full commit. A source type it does not recognise is a failure, not a skip.
  • test/tooling/check_flatpak_sources_test.py: 48 tests. A miniature checkout that passes, one broken copy per rule, and the real repository as the last case.
  • docs/flatpak-source-pinning.md: which four files are the sources of truth, what pins what, how to refresh a pin and what to read in the resulting diff, and the four inputs that genuinely cannot be immutable with the reason for each (the runtime and SDK OSTree refs, where freezing a commit would keep Linthra on an unpatched platform and is not how Flathub builds; the SDK extension, which has no per-commit form; and the app's own checkout, which git already names). That table is compared against the manifest both ways, so an unpinnable input needs a written reason and a reason cannot outlive its input.
  • scripts/regenerate_flatpak_sources.sh now checks its own output before exiting, refuses a flatpak-flutter whose requirements.txt stopped pinning every dependency, and rebuilds its venv when TOOL_COMMIT moves.
  • CI runs the check and its tests on every PR (offline, no Flutter or Flatpak), and scripts/verify_linux.sh is the local twin.

Deliberately not in scope

No dependency version changed, no auto-merge, no sandbox permission touched, and Android/F-Droid tooling is untouched. Refreshing sources stays a normal reviewable diff.

test/tooling/flatpak_offline_build_test.dart (#442) still owns the other half, that a declared source lands where the offline build reads it. The two overlap only on "a remote source carries its own digest", which is what lets this one run with no toolchain at regeneration time.

One open question for review

An automated deps/dart-packages PR bumps pubspec.lock only, and check_dependency_update_files.sh keeps the derived Flatpak pins off that branch, so the regeneration has to be a follow-up PR. That tension predates this PR (flatpak_offline_build_test.dart already compares the lockfile to the generated sources), so this documents the flow in docs/dependency-updates.md rather than widening the guard. Happy to add flatpak/generated/sources/pubspec.json to the dart-packages allowlist instead if riding along on the bot branch is preferred.

Verified locally

ruff check scripts tool tools                        # clean
ruff format --check scripts tool tools               # clean
python3 scripts/check_flatpak_sources.py             # 251 sources, all pinned
python3 test/tooling/check_flatpak_sources_test.py   # 48 passed
python3 scripts/check_linux_runner.py                # OK
python3 scripts/check_flatpak_permissions.py         # OK

Plus the neighbouring tooling tests (check_flatpak_permissions_test, check_linux_runner_test, flathub_builder_lint_test, flatpak_bundle_test, make_flatpak_smoke_manifest_test, flatpak_smoke_cleanup_test). The Dart tests need a Flutter SDK, which this environment has no toolchain for, so CI runs those.

The audit first: of the 251 remote sources the committed manifest fetches,
250 were already pinned by URL + sha256 (the native chain, every pub.dev
archive, every Flutter engine artifact, both plugin downloads) and one was
not. The template's Flutter source carried only `tag: '3.44.7'`, and a tag
can be repointed upstream.

So the pin itself is one line. What was actually missing is everything that
keeps the pins from drifting apart later, because nothing held:

* the hand-authored template to any rule at all. Every existing check reads
  the *generated* manifest, so a floating source added where a human edits
  would be caught one regeneration later, if ever;
* the template and the generated manifest to each other. Bumping mpv in the
  template (or hand-editing the generated manifest) builds something other
  than what the source of truth says;
* `.flutter-version` to the Flatpak's Flutter. An SDK bump could leave the
  package building the old toolchain, silently, since the generated module's
  tag, commit and filename were tied to nothing;
* the generated per-plugin native pins to the lockfile. flatpak-flutter picks
  its fixes by "highest entry not newer than the locked version", so a
  `sqlite3_flutter_libs` bump reuses the previous version's patch and
  pre-fetched SQLite amalgamation instead of failing.

What this adds

* `flatpak/flatpak-flutter.yml`: the Flutter source now carries `tag` and
  `commit`. The tag is what the generator reads, the commit is what makes it
  a pin. Verified against `git ls-remote --tags`, and it is the same commit
  the generated SDK module already recorded, so the generated manifest is
  byte-identical (nothing was regenerated in this PR).
* `scripts/check_flatpak_sources.py`: reads both manifests, every generated
  module and source list they include, `pubspec.lock` and `.flutter-version`,
  and fails on a source with no sha256, a git source without a full commit or
  tracking a branch, a URL that resolves at download time (`HEAD`,
  `refs/heads/`, `latest`, `?branch=`, a default-branch snapshot), plain
  HTTP, a type that carries no content pin (`svn`, `bzr`, `extra-data`), a
  local path that is absolute or escapes the checkout, the two manifests
  disagreeing, a Flutter pin that only moved in one place, engine artifacts
  from two engines, a Dart package that drifted from the lockfile, a plugin
  patch applied to a version it was not written for, a `FetchContent` URL
  with no `URL_HASH`, and a `TOOL_COMMIT` that is not a full commit. It fails
  on a source type it does not recognise rather than skipping it.
* `test/tooling/check_flatpak_sources_test.py`: 48 tests. A miniature
  checkout that passes, then one broken copy per rule, plus the real
  repository as the last case.
* `docs/flatpak-source-pinning.md`: which four files are the sources of
  truth, what pins what, how to refresh a pin (and what to read in the diff),
  and the four inputs that genuinely cannot be content-addressed, with the
  reason for each: the runtime and SDK OSTree refs (freezing them would keep
  us on an unpatched platform, and Flathub updates them in place), the SDK
  extension, and the app's own checkout, which git already names. That table
  is compared against the manifest in both directions, so an unpinnable input
  needs a written reason and a reason cannot outlive its input.
* `scripts/regenerate_flatpak_sources.sh` now checks its own output before it
  exits, refuses a flatpak-flutter whose `requirements.txt` stopped pinning
  every dependency, and rebuilds its venv when `TOOL_COMMIT` moves.
* CI runs the check and its tests on every PR (offline, no Flutter or
  Flatpak), and `scripts/verify_linux.sh` is the local twin.

Not in scope, deliberately: no dependency version changed, no auto-merge,
no sandbox permission touched, and Android/F-Droid tooling is untouched. The
existing `flatpak_offline_build_test.dart` still owns the other half (that a
declared source lands where the offline build reads it); the two overlap only
on "a remote source carries its own digest", so this one can run without a
toolchain at regeneration time.

Closes #443
The row explaining why the runtime cannot be content-addressed quoted
`runtime-version: '50'`, which turns into a stale number the moment the
runtime is bumped. The rule is what matters (an exact released version,
never a channel), and scripts/check_flatpak_sources.py is what enforces
it, so the literal adds nothing but a thing to forget.

Copy link
Copy Markdown
Owner Author

"Build and launch Flatpak" is failing here, and it is not this PR. Flagging it rather than fixing it, because the fix changes what ships.

The job dies in the pre-build Flathub lint step:

--- manifest flatpak/io.github.thezupzup.linthra.yml ---
  runtime-update-available-to-org.gnome.Platform-51 (warning)
FAIL: 1 finding(s) with no written reason.

Why it is not this PR's:

  • The identical failure is on main at this PR's base commit (c72965e), same step, same single finding: run 35183838746. It appeared because GNOME 51 was released upstream, not because anything in the repository changed.
  • The finding is about runtime-version: '50' in flatpak/io.github.thezupzup.linthra.yml, and this PR does not modify that file at all (nothing under flatpak/generated/ or the generated manifest is touched; the only Flatpak change is flatpak-flutter.yml gaining a commit: on the Flutter git source).

No fix exists to port, and neither candidate belongs in this PR:

  1. Bump the runtime to 51. That is a new GNOME/freedesktop base for the shipped package: it needs org.freedesktop.Sdk.Extension.llvm20 to still be right for the 51 SDK, a regeneration, and the audio, launch, local-library and filesystem smokes to pass on the new runtime. Porting it here would widen a source-pinning PR into a platform bump and change the artifact.
  2. Record a lint exception. flatpak/flathub-lint-exceptions.json rules that out by its own policy: "A finding that is simply not fixed yet belongs in its issue, not here." A pending runtime bump is exactly that.

I have not re-run the job. The failure is a linter reading a committed file, so it is deterministic rather than flaky, and the base-branch run above is stronger evidence than a re-run would be.

One small thing this PR does make easier: scripts/check_flatpak_sources.py requires both manifests to name the same runtime-version, and rejects a channel name (master, stable, beta) in that field. So whenever the bump happens it is a two-file edit that CI holds together, instead of a single edit that can leave the template behind.

Everything else on the head commit is green or still running; the rest of this PR's own checks, including the new source-pin check and its 48 tests, pass.


Generated by Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions

Copy link
Copy Markdown
Contributor

Repository integrity review

CLEAN

Previously reported repository-integrity findings are resolved.

The GNOME 51 migration (#660) moved the compiler extension from llvm20
to llvm22 in both manifests. The unpinnable-inputs table still named
llvm20, which scripts/check_flatpak_sources.py reports from both sides:
an input with no written reason, and a reason for an input that is gone.

That is the check doing its job on its first real bump, so the fix is
the row, not the rule.

Copy link
Copy Markdown
Owner Author

The GNOME 51 blocker is gone: #660 landed the runtime migration on main, and the merge into this branch brings it here, so the Flathub lint finding that was failing "Build and launch Flatpak" (and main before it) no longer applies.

The merge did surface one thing, which is the new check earning its keep on its first real bump. #660 moved the compiler extension from llvm20 to llvm22, and the unpinnable-inputs table in docs/flatpak-source-pinning.md still named llvm20, so check_flatpak_sources.py reported it from both sides:

FAIL: sdk-extension:org.freedesktop.Sdk.Extension.llvm22 is not content-addressed and has no row in flatpak-source-pinning.md.
FAIL: sdk-extension:org.freedesktop.Sdk.Extension.llvm20 has a row in flatpak-source-pinning.md but is no longer a build input.

Fixed in 3601a17 by following the row to llvm22. The rule is unchanged: the LLVM major is part of the extension name, so a compiler bump stays a visible manifest change with a written reason next to it.

Re-validated on the merged head: the pin check passes over all 251 remote sources, its 48 tests pass, and check_flatpak_permissions.py, check_linux_runner.py, ruff and the neighbouring tooling tests are clean. Nothing else in this PR needed touching for the runtime bump, which is the point: the sources are pinned by digest and commit, and the runtime is the documented exception.


Generated by Claude Code

@TheZupZup
TheZupZup merged commit 61fe2d4 into main Sep 20, 2026
20 checks passed
@TheZupZup
TheZupZup deleted the thezupzup/adoring-ptolemy-o1soox branch September 20, 2026 01:07
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.

[Flatpak] Pin all Flatpak build sources for reproducibility

2 participants