Add PyInstaller spec and Gitea Actions CI for Linux/Windows binaries
- run.py: top-level entry point for PyInstaller (relative imports need it) - psp-preserve.spec: onedir build bundling DATs, loader, cover bundles, icons - requirements-build.txt: pinned build deps (PySide6 6.8.3 for Windows ICU fix) - .gitea/workflows/build.yml: builds linux + windows artifacts on tags/manual - gitignore build/ dist/
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["v*"]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Linux x86_64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements-build.txt
|
||||
|
||||
- name: Build
|
||||
run: pyinstaller --clean --noconfirm psp-preserve.spec
|
||||
|
||||
- name: Package tarball
|
||||
run: tar -czf psp-preserve-linux-x86_64.tar.gz -C dist psp-preserve
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: psp-preserve-linux-x86_64
|
||||
path: psp-preserve-linux-x86_64.tar.gz
|
||||
|
||||
windows:
|
||||
name: Windows x64
|
||||
runs-on: windows
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements-build.txt
|
||||
|
||||
- name: Build
|
||||
run: pyinstaller --clean --noconfirm psp-preserve.spec
|
||||
|
||||
- name: Package zip
|
||||
shell: pwsh
|
||||
run: Compress-Archive -Path dist\psp-preserve -DestinationPath psp-preserve-windows-x64.zip
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: psp-preserve-windows-x64
|
||||
path: psp-preserve-windows-x64.zip
|
||||
@@ -20,3 +20,7 @@ data/nps/*.tsv
|
||||
|
||||
# Local environment overrides (LAN paths, secrets). See .env.example
|
||||
.env
|
||||
|
||||
# PyInstaller output
|
||||
build/
|
||||
dist/
|
||||
@@ -59,6 +59,57 @@ pip install -r requirements.txt
|
||||
python3 -m app.main
|
||||
```
|
||||
|
||||
## Building binaries
|
||||
|
||||
The GUI is packaged with PyInstaller into a self-contained folder. Build
|
||||
requirements are pinned in `requirements-build.txt` (PySide6 6.8.3 on purpose —
|
||||
see the note inside).
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv-build
|
||||
. .venv-build/bin/activate
|
||||
pip install -r requirements-build.txt
|
||||
pyinstaller --clean --noconfirm psp-preserve.spec
|
||||
# output: dist/psp-preserve/ → run dist/psp-preserve/psp-preserve
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- The build bundles the DATs, PS1 loader, and the offline cover bundles. The
|
||||
cover zips are large (~850 MB), so builds are slow and the output is big.
|
||||
- `7z` (p7zip-full on Linux, 7-Zip on Windows) is a **runtime** dependency for
|
||||
reading store archives — it is not bundled.
|
||||
- User data (credentials, manual covers, PS1 templates, NPS cache, library DB,
|
||||
cover cache) lives in the OS state dir, never next to the binary.
|
||||
|
||||
### CI (Gitea Actions)
|
||||
|
||||
`.gitea/workflows/build.yml` builds Linux and Windows artifacts on version-tag
|
||||
pushes (`v*`) or on manual trigger (**Actions → build → Run workflow**).
|
||||
Artifacts appear on the Actions run page and can be attached to a release.
|
||||
|
||||
Each job needs a [Gitea Actions runner](https://docs.gitea.com/usage/actions/act-runner)
|
||||
registered with the matching label:
|
||||
|
||||
```bash
|
||||
# Linux runner (Docker mode, run on your server)
|
||||
./act_runner register \
|
||||
--instance https://git.ceaf.dev \
|
||||
--token <registration-token> \
|
||||
--name linux-builder \
|
||||
--labels ubuntu-latest:docker://node:20-bookworm
|
||||
|
||||
# Windows runner (host mode, run on a Windows machine)
|
||||
act_runner.exe register \
|
||||
--instance https://git.ceaf.dev \
|
||||
--token <registration-token> \
|
||||
--name win-builder \
|
||||
--labels windows:host
|
||||
```
|
||||
|
||||
The registration token is generated in Gitea under
|
||||
**Site Administration → Actions → Runners → Create new runner**.
|
||||
|
||||
## First-time setup
|
||||
|
||||
1. Put your DAT files in `data/dats/`:
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
"""PyInstaller spec: build the PSP Preservation Tool (onedir).
|
||||
|
||||
Bundles the read-only resources (DATs, PS1 loader, offline cover bundles,
|
||||
icons) under ``_internal/`` next to the executable. User-writable data
|
||||
(credentials, manual covers, PS1 templates, NPS cache, library DB, cover
|
||||
cache) lives in the OS state dir at runtime — see ``app/config.py``.
|
||||
|
||||
Usage: pyinstaller --clean --noconfirm psp-preserve.spec
|
||||
Output: dist/psp-preserve/
|
||||
"""
|
||||
from PyInstaller.utils.hooks import collect_dynamic_libs, collect_submodules
|
||||
|
||||
datas = [
|
||||
("data/dats", "data/dats"),
|
||||
("data/loader", "data/loader"),
|
||||
("data/supplement.json", "data"),
|
||||
("data/covers/libretro-covers.zip", "data/covers"),
|
||||
("data/covers/openvgdb-covers.zip", "data/covers"),
|
||||
("data/covers/libretro-index.json", "data/covers"),
|
||||
("data/covers/openvgdb-index.json", "data/covers"),
|
||||
("data/covers/UMD_COVER_TEMPLATE.png", "data/covers"),
|
||||
("data/covers/UMD_COVER_TEMPLATE_FEATURE.png", "data/covers"),
|
||||
("data/covers/UMD_GAME_COVER_TEMPLATE.png", "data/covers"),
|
||||
("img/app-icon.png", "img"),
|
||||
("img/chevron.png", "img"),
|
||||
]
|
||||
|
||||
a = Analysis(
|
||||
["run.py"],
|
||||
pathex=[],
|
||||
binaries=collect_dynamic_libs("curl_cffi"),
|
||||
datas=datas,
|
||||
hiddenimports=collect_submodules("curl_cffi"),
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=["tkinter", "unittest", "pydoc"],
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name="psp-preserve",
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=False,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
)
|
||||
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=False,
|
||||
name="psp-preserve",
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
# Build dependencies (pinned for reproducible CI builds).
|
||||
# PySide6 is pinned to 6.8.3: newer 6.11.x bundles a Qt6Core.dll that imports
|
||||
# icuuc.dll on Windows, which is not shipped and crashes at startup.
|
||||
pyinstaller>=6.11
|
||||
PySide6==6.8.3
|
||||
curl_cffi>=0.7
|
||||
pycryptodome>=3.20
|
||||
@@ -0,0 +1,10 @@
|
||||
"""PyInstaller entry point.
|
||||
|
||||
Runs the GUI the same way ``python -m app.main`` does, but as a plain script
|
||||
so PyInstaller can build it (relative imports inside ``app`` won't resolve when
|
||||
a module file is executed directly, hence this top-level launcher).
|
||||
"""
|
||||
from app.main import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user