Skip to content

Cross-OS packaging: build orchestration, PyInstaller/py2app, CI matrix - #43

Open
Phantom-VK wants to merge 12 commits into
mainfrom
feat/rebuild-12-packaging
Open

Cross-OS packaging: build orchestration, PyInstaller/py2app, CI matrix#43
Phantom-VK wants to merge 12 commits into
mainfrom
feat/rebuild-12-packaging

Conversation

@Phantom-VK

@Phantom-VK Phantom-VK commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Summary

  • Added packaging/build.py: one command to produce a distributable build on any platform, always rebuilding the frontend first and asserting it's not stale.
  • Rewrote packaging/norefund.spec to 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 in docs/packaging.md.
  • Added packaging/macos_setup.py (py2app configuration) and a macos extras group, including two version pins needed to work around currently-open upstream py2app/setuptools compatibility bugs.
  • Added packaging/README.md covering build prerequisites, commands, output locations, and end-user runtime requirements per platform.
  • Added a new CI workflow building and smoke-testing on Windows, macOS and Linux on every PR/push, and fixed the existing release workflow, which the packaging rewrite had silently broken (it built via a raw PyInstaller call that never built the frontend or installed the Linux GTK extra first).
  • Fixed an invalid empty email field in pyproject.toml that only py2app's stricter validation caught.

Verified

  • Linux: built and launched the real frozen binary on this machine — window opens, all six views render with real bundled config/frontend data, and PDF export (bundled reportlab font data) produces a valid file. Also verified the missing-runtime failure path for real — installed into an environment with no PyGObject at all and confirmed a clean, actionable message and exit code 2, not a traceback.
  • CI matrix: ubuntu-latest and macos-latest both build and test fully green.
  • Windows: the frozen build itself is untested (no Windows hardware available), and CI surfaced a separate, real, pre-existing issue — the Python test suite hangs inside a legacy Tk GUI test (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.py on Linux, then launched and interacted with the real frozen build (all six views, a real download, PDF export)
  • CI: ubuntu-latest and macos-latest green
  • CI: windows-latest — build itself untested; a separate, pre-existing Tk test-suite hang on Windows needs follow-up with real hardware

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.
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