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
|
||||||
+5
-1
@@ -19,4 +19,8 @@ data/ps1_templates/
|
|||||||
data/nps/*.tsv
|
data/nps/*.tsv
|
||||||
|
|
||||||
# Local environment overrides (LAN paths, secrets). See .env.example
|
# Local environment overrides (LAN paths, secrets). See .env.example
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# PyInstaller output
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
@@ -59,6 +59,57 @@ pip install -r requirements.txt
|
|||||||
python3 -m app.main
|
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
|
## First-time setup
|
||||||
|
|
||||||
1. Put your DAT files in `data/dats/`:
|
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