Skip to content

update-tools fails with a false sha256 mismatch when the upstream artifact actually changed #14

Description

@manfred-kaiser

Summary

`python -m appimage.ctl update-tools` is supposed to move every toolchain pin (Python interpreter, `appimagetool`, runtime stub, the `appimage` module itself) forward to whatever's currently available. Instead it fails with a spurious `sha256 mismatch` error precisely when there's real work to do — i.e. when the pinned upstream artifact has actually changed since the last pin. That's the one case `update-tools` exists to handle, so it currently can't do its job whenever it would matter.

Root cause

In `appimage/ctl/_appimagetool.py`, both `_resolve_appimagetool()` (~line 283) and `_resolve_runtime_file()` (~line 363):

```python
expected = resolved.appimagetool_sha256 or api_sha256
```

`update_tools()` (in `appimage/ctl/update_tools.py`) builds `resolved` from the existing, unmodified `pyproject.toml` config, then calls `_pinned_download_fields(resolved, ..., existing=set(), force_latest=True)`. `existing=set()` tells that function to attempt to resolve every field fresh — but the underlying `resolved` object it passes into `_resolve_appimagetool`/`_resolve_runtime_file` still carries the old, already-configured pin values. Those functions verify the freshly-downloaded file against `resolved.appimagetool_sha256`/`resolved.runtime_sha256` (the old pin) if set, instead of trusting the digest GitHub itself publishes for the asset (`api_sha256`, already fetched for free at no extra cost) and writing that forward.

Net effect: the freshly downloaded (genuinely newer) artifact never matches the stale pin still sitting in `resolved`, so the mismatch check always fires whenever the upstream artifact has actually moved — which is exactly the scenario `update-tools` is meant to handle.

Reproduction

Minimal repro, isolated from any real project:

```toml

pyproject.toml

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "myapp"
version = "0.1.0"
requires-python = ">=3.11"

[project.scripts]
myapp = "myapp:main"

[tool.hatch.build.targets.wheel]
packages = ["myapp"]

[tool.appimage]
app = "myapp"
entry_point = "myapp"
python = "3.11"

Deliberately stale pin: an appimagetool build that was current a few

days ago, before upstream published a newer build.

appimagetool_version = "appimagetool, continuous build (git version 8c8c91f), build 295 built on 2025-12-04 17:56:36 UTC"
appimagetool_sha256 = "a6d71e2b6cd66f8e8d16c37ad164658985e0cf5fcaa950c90a482890cb9d13e0"
verify_downloads = true
```

```
$ python -m appimage.ctl update-tools
...
Resolving Python 3.11 download URL...
Downloading appimagetool-x86_64.AppImage
Error: appimagetool sha256 mismatch for .../build/appimagetool-x86_64.AppImage: expected a6d71e2b6cd66f8e8d16c37ad164658985e0cf5fcaa950c90a482890cb9d13e0, got ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0. A freshly downloaded file is removed automatically so a retry re-downloads cleanly; a locally configured or already-cached file is left as-is - remove it yourself and retry, or correct the configured hash.
```

Deterministic, not a network fluke — retrying produces the identical error every time, since it always re-downloads the same (genuinely newer) release and compares it against the same stale pin.

Current workaround

Manually delete the stale `_version`/`_sha256` fields (`appimagetool_version`/`appimagetool_sha256`, `runtime_version`/`runtime_sha256`, `appimage_version`/`appimage_sha256`, `appimagectl_version`) from `pyproject.toml` first, then run `enable-reproducible` instead of `update-tools`. With those fields absent, `resolved.appimagetool_sha256` etc. are empty, so the verification falls back to `api_sha256` (GitHub's own published digest) with no stale value to conflict with. This works, but it's manual and undocumented, and defeats the point of having a dedicated "move pins forward" command in the first place.

Suggested fix direction

`_pinned_download_fields` (and the `_resolve_appimagetool`/`_resolve_runtime_file` calls it makes) should not verify a freshly-forced download against the old pin still sitting in `resolved` when called from `update_tools()`'s `force_latest=True` path. Either strip the relevant fields from `resolved` before passing it down (mirroring what `init`'s `existing`-based skip logic effectively achieves for a from-scratch project), or thread an explicit "don't verify against the old pin, only against `api_sha256`" flag through to `_resolve_appimagetool`/`_resolve_runtime_file` for this call path.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions