fix(build): pin the freezer, and register what Tcl 9 brought into the bundle - #129
Merged
Merged
Conversation
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>
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.
What broke
A freshly built executable died on every launch, before any project code ran:
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.dllinstead of a directory on disk.TclTkInfo: Tcl library/data directory '//zipfs:/lib/tcl/tcl_library' does not exist!andcollects zero Tcl/Tk data files, so
_internal/_tcl_datais never created - while its ownrun-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_LIBRARYonlyif os.path.isdir(...)and never raises, whilehook-_tkinter.pyraisesSystemExitontcltk_info.tcl_data_missing.libtommath.dlljoined the bundle. The bundle-scan guardcorrectly refused it, which is why the weekly cron on
masteris currently red.Neither symptom came from a commit. Both workflows installed a bare
pyinstaller, so theversion 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.txtpinspyinstaller==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 twoenvironments drifted apart unnoticed.
test_both_workflows_install_the_same_pinned_builder: the pin must use==, bothworkflows must install from the file, and a bare
pyinstalleron anypip installline isrejected. Two entries added to the mutation registry, both confirmed red before being written
down.
libtommathregistered across the whole licensing surface - the component registry,THIRD-PARTY-NOTICES.md, a verbatim licence text inlicenses/, and the bundle-scan pattern.Its licence is the Unlicense, a public-domain dedication, so GPLv3-compatible.
Unlicensewasadded to the curated SPDX allowlist after checking it against the official SPDX list.
_TCL_CRUFTnote 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.ymlstill describedpyinstaller as unpinned.
Verification
--versionand 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).
runs,
libtommathincluded. Paired control both ways - with the registry entry the audit exits0, 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
caught this hours before a hand-built executable did, so
ci.ymlnow says plainly that a redcron is read by hand, and with which command.
number as fact.
report was.