Skip to content

fix(build): pin the freezer, and register what Tcl 9 brought into the bundle - #129

Merged
donislawdev merged 3 commits into
masterfrom
fix/pin-the-builder-and-tcl9-bundle
Aug 17, 2026
Merged

fix(build): pin the freezer, and register what Tcl 9 brought into the bundle#129
donislawdev merged 3 commits into
masterfrom
fix/pin-the-builder-and-tcl9-bundle

Conversation

@donislawdev

Copy link
Copy Markdown
Owner

What broke

A freshly built executable died on every launch, before any project code ran:

File "pyi_rth__tkinter.py", line 28, in _pyi_rthook
FileNotFoundError: Tcl data directory "...\_internal\_tcl_data" not found.

One cause, two symptoms. Python 3.14 on Windows ships Tcl/Tk 9.0, and Tcl 9 keeps its library
scripts in a zip archive embedded in tcl90.dll instead of a directory on disk.

  1. PyInstaller 6.21.0 reads that path as a filesystem directory, warns
    TclTkInfo: Tcl library/data directory '//zipfs:/lib/tcl/tcl_library' does not exist! and
    collects zero Tcl/Tk data files, so _internal/_tcl_data is never created - while its own
    run-time hook requires it. PyInstaller 6.22.0 fixed both halves upstream: support for
    Tcl/Tk 9 builds with embedded data archives, and the missing-data-directory error moved from
    run time to build time. Verified in the installed sources, not just the changelog: the new
    run-time hook sets TCL_LIBRARY only if os.path.isdir(...) and never raises, while
    hook-_tkinter.py raises SystemExit on tcltk_info.tcl_data_missing.
  2. Tcl 9 links libtommath, so libtommath.dll joined the bundle. The bundle-scan guard
    correctly refused it, which is why the weekly cron on master is currently red.

Neither symptom came from a commit. Both workflows installed a bare pyinstaller, so the
version that built an artefact was whatever the index served that day: CI resolved 6.22.1 and a
developer machine had 6.21.0. The same commit produced a working executable in one place and a
crashing one in the other, and nothing recorded which was which. Backlog item B-12 predicted this
in writing.

What this changes

  • requirements-build.txt pins pyinstaller==6.22.1, and both workflows install it with
    -r. The interpreter already had a parity guard; the freezer had none, which is how the two
    environments drifted apart unnoticed.
  • New guard test_both_workflows_install_the_same_pinned_builder: the pin must use ==, both
    workflows must install from the file, and a bare pyinstaller on any pip install line is
    rejected. Two entries added to the mutation registry, both confirmed red before being written
    down.
  • libtommath registered across the whole licensing surface - the component registry,
    THIRD-PARTY-NOTICES.md, a verbatim licence text in licenses/, and the bundle-scan pattern.
    Its licence is the Unlicense, a public-domain dedication, so GPLv3-compatible. Unlicense was
    added to the curated SPDX allowlist after checking it against the official SPDX list.
  • Two false comments corrected. The _TCL_CRUFT note in the spec claimed the filter drops
    "~750 files"; on Tcl 9 it drops nothing, and the comment now says which build is which and marks
    the Tcl 8.6 saving as not re-measured. The weekly-cron rationale in ci.yml still described
    pyinstaller as unpinned.

Verification

  • Full suite: 1122 passed, plus the GUI smoke script.
  • The rebuilt executable was launched, which is what nothing here had been doing: --version
    and a simulated session both exit 0, a bad configuration exits 3, and the graphical interface
    opens a real window (checked by window title, because a live process with no window looks
    identical from outside).
  • Real bundle scan, not a synthetic one: syft names 56 artefacts and agrees with the version CI
    runs, libtommath included. Paired control both ways - with the registry entry the audit exits
    0, with the entry deleted it exits 1 and names the component. The first attempt at that control
    silently tested nothing, because the restore command had nothing to restore; it was redone.

Incidental measurement: the Tcl 9 bundle is 105 files against roughly 266 on Tcl 8.6.

Not done, on purpose

  • Automatically opening an issue when the weekly cron fails was considered and declined. The cron
    caught this hours before a hand-built executable did, so ci.yml now says plainly that a red
    cron is read by hand, and with which command.
  • The Tcl 8.6 size saving was not re-measured - the comment says so rather than restating an old
    number as fact.
  • The graphical About window was not checked with a screenshot; only the command-line licence
    report was.

donislawdev and others added 3 commits August 17, 2026 18:49
Python 3.14 on Windows ships Tcl/Tk 9.0, which keeps its library scripts in a
zip archive embedded in tcl90.dll instead of a directory on disk. PyInstaller
6.21.0 reads that path as a filesystem directory, collects ZERO Tcl/Tk data
files, and so never creates _internal/_tcl_data - while its own
pyi_rth__tkinter run-time hook requires that directory. The frozen exe raised
FileNotFoundError before any project code ran, on every launch, CLI and GUI
alike.

PyInstaller 6.22.0 fixed both halves upstream: support for Tcl/Tk 9 builds with
embedded data archives, and the missing-data-directory error moved from run
time to build time. Both workflows installed a bare `pyinstaller`, so the
version that built an artefact was whatever PyPI served that day - CI resolved
6.22.1 while a developer machine had 6.21.0, and the same commit produced a
working exe there and a crashing one here.

- requirements-build.txt pins pyinstaller==6.22.1, with the reasoning kept next
  to the number
- the ci.yml build job and release.yml install it with -r, not by name
- new test_both_workflows_install_the_same_pinned_builder: the pin must use ==,
  both workflows must install from the file, and a bare pyinstaller on any pip
  install line is rejected
- two MUTATIONS entries, both confirmed red before being written down
- corrected the weekly-cron comment in ci.yml, which still described
  pyinstaller as unpinned

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment above _TCL_CRUFT claimed the filter drops "~750 files" of tzdata
and msgcat catalogs. On a Tcl 9 build it drops nothing at all.

Measured 2026-08-17 (Python 3.14.7, Tcl 9.0.4, PyInstaller 6.22.1): Tcl 9 keeps
its library scripts in a zip archive embedded in tcl90.dll, so
tcltk_info.data_files is 0, the bundle has no _tcl_data/ or _tk_data/ at all,
and _internal/ carries exactly tcl90.dll and tcl9tk90.dll.

The filter itself stays, because requires-python = ">=3.10" still permits an
interpreter shipping Tcl 8.6 where it does bite. That saving is now marked as
not re-measured rather than stated as fact, and the comment names the number to
re-read if a future Python moves the Tcl data again.

No behaviour change - the comprehension is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The weekly cron went red on master: the build and the exe smoke both passed, and
the job failed at `sbom.py --audit-bundle` naming `\_internal\libtommath`.

Python 3.14 ships Tcl/Tk 9.0, and Tcl 9 links libtommath for its
arbitrary-precision integer arithmetic, so libtommath.dll arrives from the
CPython DLLs directory with everything else Tk needs. No requirements file names
it - the third component found by scanning the built bundle rather than by
reading a manifest, after zlib and libffi. The guard worked exactly as intended.

Licence read at the source: the Unlicense, a public-domain dedication, so
GPLv3-compatible. The full text is bundled verbatim.

- legal.COMPONENTS gains the entry, so --license and the About window name it
- THIRD-PARTY-NOTICES.md gains its section, licenses/ its text
- sbom.py maps it with a substring pattern, because the scan reports the name
  with a path prefix
- "Unlicense" added to the curated SPDX allowlist in test_sbom.py, after
  checking it against the official SPDX list

Verified locally in both directions with a synthetic syft-json, since syft is
not installed here: a scan naming the component now exits 0, and a control scan
naming an unknown one still exits 1 and names it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@donislawdev
donislawdev merged commit 966a440 into master Aug 17, 2026
5 of 7 checks passed
@donislawdev
donislawdev deleted the fix/pin-the-builder-and-tcl9-bundle branch August 17, 2026 17:39
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