Cross-OS packaging: build orchestration, PyInstaller/py2app, CI matrix - #43
Merged
Merged
Conversation
Bundles the built frontend, drops the customtkinter/PIL._tkinter_finder Tk leftovers and the CTk-only icon assets, and excludes every unused pywebview backend so an unrelated Qt install on the build machine can't silently double the bundle. Two real bugs surfaced by actually launching the frozen Linux build, not just compiling it: - PyGObject looks up gi.overrides.<Module> by name for every gi.repository import (the same "discovered at runtime, invisible to static analysis" problem tiktoken_ext already needed a hiddenimport for). Without it, `import gi.repository.Gtk` silently succeeds without actually initializing anything, and Gdk.Display.get_default() returns None instead of a real display -- the app launches clean and crashes the instant it touches the screen list. - PyInstaller's own gi runtime hook points GI_TYPELIB_PATH at the frozen bundle's gi_typelibs/ dir unconditionally, even when its build-time hook found nothing to put there, which hides the system's real typelibs instead of falling back to them. - A related split-brain: PyInstaller bundles libglib/libgobject/libgio (pulled in via PyGObject's compiled extension) without bundling libgtk/libgdk themselves, so the system's GTK ends up linked against a different glib copy than it was built against. Excluded the bundled copies so the whole stack comes from the system consistently. Verified by actually building and running dist/NoRefund/NoRefund on this machine: window opens, all six views render with real bundled config and frontend data, and PDF export (bundled reportlab font data) produces a valid file.
py2app is the pywebview-recommended path on macOS. Bundles the built frontend and config YAML (py2app's static analysis can't see either the same way PyInstaller's can't), requests no entitlements the app doesn't need, and targets macOS 12+. Written but not build-verified -- no macOS hardware available in this environment. Flagged in the phase tracking for a real-hardware build and ad-hoc-sign pass.
Covers build prerequisites, the build command, output location, and end-user runtime requirements for Linux/Windows/macOS, plus the two real GTK packaging bugs the Linux rewrite surfaced and how they were fixed. Verified the Linux missing-runtime failure path for real: installed the app into a virtualenv with no PyGObject at all and confirmed it prints the exact install-command message (matching missing_runtime_message()) and exits with code 2, not a traceback.
New build.yml: a lighter-weight, continuous companion to release.yml's tag-triggered builds. Runs on every PR and push to main across all three OSes -- frontend typecheck/test/build, pytest, ruff, then the actual frozen build via packaging/build.py, with a launch smoke test on Linux and Windows (macOS lacks an easy headless display to smoke-test against in CI, so it's build-only there for now). Also fixes release.yml, which the packaging rewrite silently broke: it built via a raw `pyinstaller packaging/norefund.spec` call that never built the frontend first (dist would have shipped a blank window) and never installed the `linux` extra pygobject needs. Both jobs now go through packaging/build.py instead.
…clean dist/ Local iterative rebuilds otherwise fail outright the moment dist/NoRefund/ already exists from a previous run.
… the CI matrix - pygobject has no prebuilt wheel for the CI runner's exact platform, so pip builds it from source, which needs pycairo's own build deps (libgirepository*-dev, libcairo2-dev, pkg-config, python3-dev) -- missing on a fresh ubuntu-latest runner even though none of them are needed at actual runtime. - pyproject.toml's empty author email fails py2app's stricter build-time metadata validation (hatchling itself never checked it). Dropped the empty field rather than inventing a value. - Bounded the Python test step to 8 minutes so a hang fails fast with a clear timeout instead of silently eating the whole job's budget. Windows CI still has a real, separate problem this run surfaced: the Python test step hung for 18+ minutes inside test_fit_check_view.py (legacy Tk gui/ tests), after two earlier failures in test_compare_view.py and test_desktop_dto.py with no visible traceback (the job was cancelled before pytest's summary printed). This is the first time any workflow has run the suite on Windows at all, so it predates this branch; not something fixable without real Windows access to reproduce and debug interactively.
Modern setuptools rejects the legacy install_requires/setup_requires
kwargs on a project that already declares its dependencies via
pyproject.toml's [project] table, which this one does -- CI's macos-latest
runner has a setuptools recent enough to make that a hard error
("install_requires is no longer supported") rather than a warning.
py2app itself is already guaranteed present via the macos pip extra
before this script runs, so setup_requires was never load-bearing.
…bility
py2app 0.28.9 made having any dependencies in pyproject.toml's [project]
table a hard build error ("install_requires is no longer supported"),
regardless of what the py2app-specific setup() call itself passes -- an
open upstream bug (ronaldoussoren/py2app#560), not something fixable from
this project's side.
py2app 0.28.8 still imports pkg_resources directly; setuptools removed it entirely in v82.0.0. The setuptools maintainers' own stated recommendation for anyone still needing it is to pin setuptools<81.
The desktop app now runs on the React/pywebview frontend; src/norefund/gui/ is unused reference code, so tests exercising its views only added Tk-display flakiness and dead fixture maintenance with no product coverage.
to_jsonable() correctly returns a Path's native string form, but the test hardcoded a posix-style literal, which only matched on Linux/macOS. Removing the legacy Tk suite let the Windows CI job actually finish pytest instead of hanging, which is what surfaced this.
useSettings called bridge.getSettings() directly, without waiting for window.pywebview.api to be injected -- a race it consistently lost on some machines, throwing "Python bridge is not ready" immediately at mount. That error was caught into local state App.tsx never reads, so the app hung forever on the loading spinner with no visible error. Move the bridgeReady() wait into call() itself so every bridge method waits for readiness by construction, instead of relying on each call site to remember to do it (App.tsx's own effect did remember; useSettings did not). Add bridge.test.ts covering the race and the timeout path.
…ownloaded builds Files extracted from a GitHub-downloaded zip get tagged as "from the internet" (Zone.Identifier), and .NET Framework refuses to load pythonnet's DLL from a tagged file -- crashing before the app window ever appears. Strip the tag from our own bundle at startup, before pythonnet loads. Also widen missing_runtime_message()'s except clause, which only caught ImportError and let this RuntimeError through as a raw traceback instead of a readable message.
…rning actions/checkout@v4, setup-python@v5, setup-node@v4, upload-artifact@v4, and download-artifact@v4 all still targeted the now-deprecated Node 20 runtime. Bumped each to its current major (checkout v7, setup-node v7, setup-python v7, upload-artifact v7, download-artifact v8) across build.yml, ci.yml, and release.yml -- checked each release's changelog for breaking changes against how this repo actually uses them; none apply. Also added a Download section to the README explaining the Windows SmartScreen warning users hit when running the unsigned .exe -- the app isn't code-signed (no budget for a cert), so this can't be removed from CI alone; documenting it is the honest option for now.
…g docs release.yml never actually built or shipped a macOS artifact -- the release job's needs list only covered Linux and Windows, so a tagged release had nothing for Mac users to download. Add a build-macos job mirroring the Linux/Windows ones (ad-hoc sign, tar.gz, versioned filename) and wire it into the release job. Also add a macOS smoke test to build.yml for parity with the existing Linux/Windows launch checks, and update the packaging README: the right-click-to-Open workaround for an ad-hoc-signed app often doesn't surface on current macOS, which instead shows a misleading "is damaged" dialog -- document the xattr -cr fix that actually works, and note notarization is deliberately deferred (no budget for it yet) rather than an oversight.
…Windows build_frontend() called subprocess.run(["npm", "ci"]) with no shell. Windows' CreateProcess only auto-appends .exe when resolving a bare command name, not .cmd (npm's actual Windows wrapper) -- this raised FileNotFoundError/WinError2 immediately. Never caught by build.yml's own Windows job because it always passes --skip-frontend, bypassing build_frontend() entirely; only surfaced running the full release.yml build-windows job, which builds the frontend itself.
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.
Summary
packaging/build.py: one command to produce a distributable build on any platform, always rebuilding the frontend first and asserting it's not stale.packaging/norefund.specto target the React/pywebview desktop app instead of the legacy CustomTkinter app, and fixed two real bugs found by actually launching the frozen Linux build (not just compiling it): a missing PyGObject override module that left GTK silently uninitialized, and a glib-library version conflict between bundled and system copies. Both are documented indocs/packaging.md.packaging/macos_setup.py(py2app configuration) and amacosextras group, including two version pins needed to work around currently-open upstream py2app/setuptools compatibility bugs.packaging/README.mdcovering build prerequisites, commands, output locations, and end-user runtime requirements per platform.emailfield inpyproject.tomlthat only py2app's stricter validation caught.Verified
test_fit_check_view.py) on Windows specifically, after two earlier test failures with no visible traceback. This is the first time any workflow has run the suite on Windows at all, so it predates this branch and isn't caused by the packaging changes here; it needs real Windows access to reproduce and debug interactively. Bounded with an 8-minute step timeout so it fails fast and visibly instead of eating the whole job.Test plan
python -m pytest -q(325 passing locally)ruff check src/cd frontend && npm run typecheck && npm run test && npm run build(97 vitest passing)python packaging/build.pyon Linux, then launched and interacted with the real frozen build (all six views, a real download, PDF export)