Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f3caaef
build: add cross-platform build orchestration script
Phantom-VK Aug 21, 2026
2b878bd
build: rewrite PyInstaller spec for the webview app
Phantom-VK Aug 21, 2026
ac308c6
build: add macOS py2app bundle configuration
Phantom-VK Aug 21, 2026
13aba41
docs(packaging): document per-platform build and runtime requirements
Phantom-VK Aug 21, 2026
5698a72
ci: build and test on Windows, macOS and Linux
Phantom-VK Aug 21, 2026
fd7dbe5
build: pass --noconfirm to PyInstaller so repeat builds don't need a …
Phantom-VK Aug 21, 2026
0fe78a0
ci: fix Linux build deps and an invalid pyproject.toml field found by…
Phantom-VK Aug 21, 2026
8a65e8e
build: drop setup_requires from the py2app setup script
Phantom-VK Aug 21, 2026
2af6cbf
build: pin py2app below the version that broke pyproject.toml compati…
Phantom-VK Aug 21, 2026
4f8d2cb
build: pin setuptools<81 for the macos extra
Phantom-VK Aug 21, 2026
fcfeb9b
test: remove legacy CustomTkinter view tests
Phantom-VK Aug 22, 2026
4622949
test: fix desktop dto path test to be OS-agnostic
Phantom-VK Aug 22, 2026
f9a3d03
fix: wait for pywebview bridge readiness in every bridge call
Phantom-VK Aug 23, 2026
b3bc60d
fix: strip Mark-of-the-Web from frozen bundle so pythonnet loads on d…
Phantom-VK Aug 23, 2026
329c0a9
ci: bump actions to Node 24 majors; docs: note Windows SmartScreen wa…
Phantom-VK Aug 23, 2026
ba789af
ci: add missing macOS release build and fix Gatekeeper troubleshootin…
Phantom-VK Aug 26, 2026
0412bb5
fix: resolve npm through shutil.which so packaging/build.py works on …
Phantom-VK Aug 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
name: Build

# Cross-OS build verification on every PR and every push to main -- a
# lighter-weight, continuous companion to release.yml's tag-triggered
# release builds. Catches "the frozen build doesn't actually launch" the
# same day it's introduced, not the next time someone cuts a release.
on:
pull_request:
push:
branches:
- main

jobs:
build:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 20
steps:
- uses: actions/checkout@v7

- uses: actions/setup-python@v7
with:
python-version: "3.12"

- uses: actions/setup-node@v7
with:
node-version: "22"
cache: "npm"
cache-dependency-path: frontend/package-lock.json

- name: Install Linux GTK/WebKit dependencies
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 update -y
sudo apt-get install -y \
python3-gi gir1.2-gtk-3.0 gir1.2-webkit2-4.1 xvfb \
libgirepository1.0-dev libgirepository-2.0-dev libcairo2-dev \
pkg-config python3-dev

- name: Install project
shell: bash
run: |
if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then
pip install -e ".[dev,linux]"
elif [ "${{ matrix.os }}" = "macos-latest" ]; then
pip install -e ".[dev,macos]"
else
pip install -e ".[dev]"
fi

- name: Frontend typecheck, test, build
working-directory: frontend
run: |
npm ci
npm run typecheck
npm test
npm run build

- name: Python tests
shell: bash
timeout-minutes: 8
run: |
if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then
xvfb-run --auto-servernum pytest
else
pytest
fi

- name: Lint
run: ruff check src/

- name: Build frozen app
run: python packaging/build.py --skip-frontend

- name: Smoke test the binary actually launches (Linux)
if: matrix.os == 'ubuntu-latest'
run: |
# No --version flag; with no args the app opens the GUI and
# blocks, so give it 5s under a virtual display and kill it.
# A clean launch exits 124 (timeout killed it); any other code
# means it crashed on startup -- this is exactly the class of
# bug (Gdk.Display.get_default() returning None) that compiles
# fine and only shows up when the frozen build actually runs.
set +e
xvfb-run --auto-servernum timeout 5 ./dist/NoRefund/NoRefund
code=$?
set -e
if [ "$code" -ne 0 ] && [ "$code" -ne 124 ]; then
echo "::error::NoRefund exited with code $code within 5s instead of staying up — the frozen build is broken."
exit 1
fi

- name: Smoke test the binary actually launches (Windows)
if: matrix.os == 'windows-latest'
shell: pwsh
run: |
$proc = Start-Process -FilePath "dist\NoRefund\NoRefund.exe" -PassThru
Start-Sleep -Seconds 5
if ($proc.HasExited) {
Write-Error "NoRefund.exe exited within 5s (exit code $($proc.ExitCode)) instead of staying up — the frozen build is broken."
exit 1
}
Stop-Process -Id $proc.Id -Force

- name: Smoke test the binary actually launches (macOS)
if: matrix.os == 'macos-latest'
run: |
# py2app names the bundle executable after the plist, not
# assumed to match "NoRefund" -- read it back rather than
# guessing, same reasoning as the dynamic lookup below.
EXE=$(plutil -extract CFBundleExecutable raw dist/NoRefund.app/Contents/Info.plist)
"dist/NoRefund.app/Contents/MacOS/$EXE" &
PID=$!
sleep 5
if ! kill -0 "$PID" 2>/dev/null; then
echo "::error::NoRefund.app exited within 5s instead of staying up — the frozen build is broken."
exit 1
fi
kill "$PID"

- name: Package artifact (Linux)
if: matrix.os == 'ubuntu-latest'
run: tar -czf NoRefund-linux-x86_64.tar.gz -C dist NoRefund

- name: Package artifact (macOS)
if: matrix.os == 'macos-latest'
run: |
codesign --force --deep --sign - dist/NoRefund.app
tar -czf NoRefund-macos.tar.gz -C dist NoRefund.app

- uses: actions/upload-artifact@v7
if: matrix.os == 'ubuntu-latest'
with:
name: linux-build
path: NoRefund-linux-x86_64.tar.gz
if-no-files-found: error

- uses: actions/upload-artifact@v7
if: matrix.os == 'windows-latest'
with:
name: windows-build
path: dist/NoRefund
if-no-files-found: error

- uses: actions/upload-artifact@v7
if: matrix.os == 'macos-latest'
with:
name: macos-build
path: NoRefund-macos.tar.gz
if-no-files-found: error
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- uses: actions/setup-python@v5
- uses: actions/setup-python@v7
with:
python-version: "3.12"

Expand Down
117 changes: 96 additions & 21 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
outputs:
version: ${{ steps.resolve.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Resolve version
id: resolve
Expand Down Expand Up @@ -48,27 +48,40 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- uses: actions/setup-python@v5
- uses: actions/setup-python@v7
with:
python-version: "3.12"

- uses: actions/setup-node@v7
with:
node-version: "22"
cache: "npm"
cache-dependency-path: frontend/package-lock.json

- name: Install Linux GTK/WebKit dependencies
run: |
sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 update -y
sudo apt-get install -y \
python3-gi gir1.2-gtk-3.0 gir1.2-webkit2-4.1 xvfb \
libgirepository1.0-dev libgirepository-2.0-dev libcairo2-dev \
pkg-config python3-dev

- name: Install project
run: pip install -e .[dev]
run: pip install -e ".[dev,linux]"

- name: Build with PyInstaller
run: pyinstaller packaging/norefund.spec --distpath dist --workpath build --noconfirm
- name: Build
run: python packaging/build.py

- name: Smoke test the binary actually launches
run: |
# There's no --version flag; with no args the app opens the GUI
# and blocks in mainloop(), so give it 5s under a virtual display
# and kill it. A clean launch exits 124 (timeout killed it); any
# other code means it crashed on startup (this is exactly how the
# PIL._tkinter_finder packaging bug was first caught).
sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 update -y
sudo apt-get install -y xvfb
# and blocks, so give it 5s under a virtual display and kill it.
# A clean launch exits 124 (timeout killed it); any other code
# means it crashed on startup -- this is exactly the class of bug
# (Gdk.Display.get_default() returning None; see docs/packaging.md)
# that compiles fine and only shows up when the frozen build runs.
set +e
xvfb-run --auto-servernum timeout 5 ./dist/NoRefund/NoRefund
code=$?
Expand All @@ -83,7 +96,7 @@ jobs:
VERSION="${{ needs.version.outputs.version }}"
tar -czf "NoRefund-${VERSION}-linux-x86_64.tar.gz" -C dist NoRefund

- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v7
with:
name: linux-build
path: NoRefund-*-linux-x86_64.tar.gz
Expand All @@ -93,20 +106,26 @@ jobs:
needs: version
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- uses: actions/setup-python@v5
- uses: actions/setup-python@v7
with:
python-version: "3.12"

- uses: actions/setup-node@v7
with:
node-version: "22"
cache: "npm"
cache-dependency-path: frontend/package-lock.json

- name: Install project
run: pip install -e .[dev]

- name: Install Inno Setup
run: choco install innosetup --no-progress -y

- name: Build with PyInstaller
run: pyinstaller packaging\norefund.spec --distpath dist --workpath build --noconfirm
- name: Build
run: python packaging\build.py

- name: Smoke test the binary actually launches
shell: pwsh
Expand All @@ -125,22 +144,77 @@ jobs:
$version = "${{ needs.version.outputs.version }}"
iscc "/DMyAppVersion=$version" packaging\windows\installer.iss

- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v7
with:
name: windows-build
path: packaging/windows/dist/NoRefund-Setup-*.exe
if-no-files-found: error

build-macos:
needs: version
runs-on: macos-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7

- uses: actions/setup-python@v7
with:
python-version: "3.12"

- uses: actions/setup-node@v7
with:
node-version: "22"
cache: "npm"
cache-dependency-path: frontend/package-lock.json

- name: Install project
run: pip install -e ".[dev,macos]"

- name: Build
run: python packaging/build.py

- name: Smoke test the binary actually launches
run: |
# py2app names the bundle executable after the plist, not
# assumed to match "NoRefund" -- read it back rather than
# guessing (see build.yml's matching macOS smoke test).
EXE=$(plutil -extract CFBundleExecutable raw dist/NoRefund.app/Contents/Info.plist)
"dist/NoRefund.app/Contents/MacOS/$EXE" &
PID=$!
sleep 5
if ! kill -0 "$PID" 2>/dev/null; then
echo "::error::NoRefund.app exited within 5s instead of staying up — the frozen build is broken."
exit 1
fi
kill "$PID"

- name: Ad-hoc sign and package tarball
run: |
# No Apple Developer ID ($99/yr) -- ad-hoc signing is the free
# option. It quiets Gatekeeper's outright refusal to launch but
# not the "is damaged" first-run dialog; see packaging/README.md
# for the xattr -cr workaround end users need until this project
# can afford real notarization.
VERSION="${{ needs.version.outputs.version }}"
codesign --force --deep --sign - dist/NoRefund.app
tar -czf "NoRefund-${VERSION}-macos.tar.gz" -C dist NoRefund.app

- uses: actions/upload-artifact@v7
with:
name: macos-build
path: NoRefund-*-macos.tar.gz
if-no-files-found: error

release:
needs: [version, build-linux, build-windows]
needs: [version, build-linux, build-windows, build-macos]
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v8
with:
path: artifacts

Expand All @@ -151,5 +225,6 @@ jobs:
gh release create "${{ github.ref_name }}" \
artifacts/linux-build/* \
artifacts/windows-build/* \
artifacts/macos-build/* \
--title "NoRefund ${{ github.ref_name }}" \
--generate-notes
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -238,4 +238,5 @@ docs/superpowers/
# The Python packaging "lib/" rule above is unanchored and would otherwise
# swallow the frontend's own src/lib/ directory.
!/frontend/src/lib/
!/frontend/src/lib/**
!/frontend/src/lib/**
/graphify-out/
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ used only when you explicitly download a tokenizer, from the app's Resources vie

---

## Download

Prebuilt Windows, macOS, and Linux builds are on the
[Releases page](https://github.com/Phantom-VK/NoRefund/releases) — no Python install
required.

**Windows SmartScreen:** NoRefund isn't code-signed (a signing certificate costs money
this free project doesn't have), so Windows shows a "Windows protected your PC" warning
the first time you run it. Click **More info → Run anyway** to continue. This warning
means the publisher isn't verified, not that the app is unsafe — the source is right
here in this repo.

---

## Quick Start

```bash
Expand Down
Loading