diff --git a/.github/workflows/linux-appimage.yml b/.github/workflows/linux-appimage.yml new file mode 100644 index 00000000..55f9b57e --- /dev/null +++ b/.github/workflows/linux-appimage.yml @@ -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 diff --git a/tools/make_appimage.py b/tools/make_appimage.py index 646224f6..55d4d619 100644 --- a/tools/make_appimage.py +++ b/tools/make_appimage.py @@ -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)