Build the Linux AppImage on a GitHub Actions Linux runner and attach it to each release.

The release is assembled on Windows, and the AppImage needs Linux (the translator's native build and
appimagetool), so releases carried no Linux download. .github/workflows/linux-appimage.yml runs when a
release is published, or by hand for a tag: it builds the translator, takes the button art from the
release's own app zip, runs tools/make_appimage.py, checks that the AppImage starts and uploads it with
its SHA-256 to that release. tools/make_appimage.py takes --prompt-art as make_kit.py does and bundles
pillow, so an AppImage build can draw the Xbox, PlayStation and keyboard prompts.

Pull request #273 by loteran, taken as it is. Added: the workflow installs glslc and spirv-tools, which
make_appimage.py needs since the runtime's shaders are compiled at build time.

The workflow does nothing until this file is on the repository's default branch on GitHub; from then on
every published release starts it, with write access to that release. It was not run here, and
make_appimage.py only passes a syntax check on Windows.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-10 06:34:28 -05:00
parent d985e0ecb0
commit ff4b6772f9
2 changed files with 82 additions and 4 deletions

68
.github/workflows/linux-appimage.yml vendored Normal file
View File

@@ -0,0 +1,68 @@
# The Linux AppImage of each release (tools/make_appimage.py), built on a Linux runner and attached to the release
# itself: the release is assembled on Windows (tools/make_release.py), and the AppImage needs Linux (the translator's
# NativeAOT build for linux-x64, appimagetool). It holds the build kit only, no game files, as the setup does.
name: Linux AppImage
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build the AppImage for (it replaces one already attached)'
required: true
permissions:
contents: write # gh release upload
concurrency:
group: linux-appimage-${{ github.event.release.tag_name || inputs.tag }}
jobs:
appimage:
runs-on: ubuntu-24.04
timeout-minutes: 60
env:
TAG: ${{ github.event.release.tag_name || inputs.tag }}
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name || inputs.tag }}
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- uses: astral-sh/setup-uv@v6
- name: Shader compilers
# make_appimage.py compiles the runtime's own shaders (tools/build_shaders.py: glslc, spirv-as, spirv-val)
run: sudo apt-get update && sudo apt-get install -y glslc spirv-tools
- name: Shader translator and VMA
run: |
uv venv -q --python 3.12 .venv
uv pip install -q --python .venv/bin/python pillow
.venv/bin/python tools/build_xlate.py
.venv/bin/python -c 'import sys; sys.path.insert(0, "tools"); import build_game; print("VMA:", build_game.vma_include())'
- name: Button art from the release's app zip
run: |
# make_release.py uploads the zip with the release; wait for it if the release was published first
for i in $(seq 1 20); do
gh release download "$TAG" -p 'NX2navi-app-*.zip' -D "$RUNNER_TEMP/app" && break
echo "no app zip on $TAG yet ($i)"; sleep 30
done
unzip -q "$RUNNER_TEMP"/app/NX2navi-app-*.zip 'prompt-art/*' -d "$RUNNER_TEMP/app" || true
ls -d "$RUNNER_TEMP/app/prompt-art" 2>/dev/null || echo "::warning::no prompt-art in the app zip: the AppImage's builds keep the Switch prompts"
- name: AppImage
run: |
uv python install -q 3.14
py=$(find "$(uv python dir)" -maxdepth 1 -type d -name 'cpython-3.14.*-linux-x86_64-gnu' | sort -V | tail -1)
curl -fsSL -o "$RUNNER_TEMP/appimagetool" \
https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x "$RUNNER_TEMP/appimagetool"
art=(); [ -d "$RUNNER_TEMP/app/prompt-art" ] && art=(--prompt-art "$RUNNER_TEMP/app/prompt-art")
APPIMAGE_EXTRACT_AND_RUN=1 .venv/bin/python tools/make_appimage.py --python "$py" \
--appimagetool "$RUNNER_TEMP/appimagetool" --out dist "${art[@]}"
appimage=$(ls dist/NX2navi-Linux-*-x86_64.AppImage)
APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --help | head -3
(cd dist && sha256sum NX2navi-Linux-*-x86_64.AppImage > SHA256SUMS-linux.txt && cat SHA256SUMS-linux.txt)
- name: Attach to the release
run: gh release upload "$TAG" --clobber dist/NX2navi-Linux-*-x86_64.AppImage dist/SHA256SUMS-linux.txt

View File

@@ -5,9 +5,12 @@ player's PC from their own dump with tools/make_game.py, as NX2navi-Setup.exe do
The AppImage holds no game files: the kit's tools/, recomp/, runtime/ and docs, the shader translator built by
tools/build_xlate.py (bin/nx2navi_xlate), the pinned Vulkan Memory Allocator header, and a relocatable CPython 3.14
(python-build-standalone, as `uv python install 3.14` puts in ~/.local/share/uv/python) with numpy, lz4, zstandard and
fonttools. zbic, the GPL package that unpacks the game's program files, is not bundled: the AppImage installs it with
pip into ~/.local/share/nx2navi/pydeps the first time it runs, as the Windows launcher downloads it.
(python-build-standalone, as `uv python install 3.14` puts in ~/.local/share/uv/python) with numpy, lz4, zstandard,
fonttools and pillow, and with --prompt-art the button art make_game.py's prompts step draws from (as make_kit.py
--prompt-art: the pictures tools/prompt_art.py art_files names, into the kit's prompt-art folder; without it that step
is skipped and the game keeps the Switch prompts). zbic, the GPL package that unpacks the game's program files, is not
bundled: the AppImage installs it with pip into ~/.local/share/nx2navi/pydeps the first time it runs, as the Windows
launcher downloads it.
The compiler and the libraries come from the system (README-BuildKit.md, "Linux"): the AppImage checks for clang,
clang++, lld and the headers before it starts, and names the packages for Arch, Fedora and Debian/Ubuntu.
@@ -24,7 +27,7 @@ ROOT = Path(__file__).resolve().parent.parent
KIT_DIRS = ('recomp', 'runtime')
KIT_FILES = ('VERSION', 'LICENSE', 'LICENSE-ADDITIONS', 'THIRD_PARTY_NOTICES.md', 'nx2navi.ini.example')
TOOLS_SKIP = ('vendor', 'xlate', '__pycache__') # the translator ships built (bin/); its sources stay out
PACKAGES = ('numpy==2.5.4', 'lz4==4.4.5', 'zstandard==0.25.0', 'fonttools==4.68.0')
PACKAGES = ('numpy==2.5.4', 'lz4==4.4.5', 'zstandard==0.25.0', 'fonttools==4.68.0', 'pillow==12.3.0')
APPRUN = r'''#!/bin/bash
# NX2navi build kit for Linux: builds Ocarina of Time (Switch 2, game 1.1.1) from the player's own dump.
@@ -132,6 +135,9 @@ def main():
help='a relocatable CPython 3.14 folder (python-build-standalone, ~/.local/share/uv/python/cpython-3.14*)')
ap.add_argument('--appimagetool', type=Path, required=True, help='appimagetool-x86_64.AppImage')
ap.add_argument('--out', type=Path, default=ROOT / 'dist', help='where the AppImage goes (default dist/)')
ap.add_argument('--prompt-art', type=Path, nargs='+', metavar='FOLDER',
help='folders holding XGamepad, P5Gamepad and Keyboard_Mouse (or those folders): the button art, as '
'make_kit.py --prompt-art takes it (a release zip\'s prompt-art folder does too)')
a = ap.parse_args()
version = (ROOT / 'VERSION').read_text(encoding='utf-8').strip()
@@ -160,6 +166,10 @@ def main():
shutil.copy2(xlate / f, kit / 'bin' / f)
(kit / 'toolchain' / 'vma').mkdir(parents=True)
shutil.copy2(ROOT / 'toolchain' / 'vma' / 'vk_mem_alloc.h', kit / 'toolchain' / 'vma' / 'vk_mem_alloc.h')
if a.prompt_art:
import prompt_art
n, size = prompt_art.copy_art(a.prompt_art, kit / prompt_art.FOLDER)
print(f'button art: {n} files, {size / 1e6:.1f} MB')
py = app / 'usr' / 'python'
shutil.copytree(a.python, py, symlinks=True)