diff --git a/Launcher/WiiCompiled.Setup.Linux/Program.cs b/Launcher/WiiCompiled.Setup.Linux/Program.cs index 4c0dcad..7ab7391 100644 --- a/Launcher/WiiCompiled.Setup.Linux/Program.cs +++ b/Launcher/WiiCompiled.Setup.Linux/Program.cs @@ -6,8 +6,10 @@ internal static class Program { private static async Task Main(string[] args) { - if (args.Length == 0 || args[0] is "-h" or "--help") { PrintUsage(); return 0; } - if (args[0] == "--version") { Console.WriteLine(ProductInfo.Version); return 0; } + // Checked anywhere in argv, not just args[0]: AppRun (Launcher/build-appimage.sh) prepends + // --workspace ahead of whatever the caller passed, so these can't assume position 0. + if (args.Length == 0 || args.Contains("-h") || args.Contains("--help")) { PrintUsage(); return 0; } + if (args.Contains("--version")) { Console.WriteLine(ProductInfo.Version); return 0; } using var cts = new CancellationTokenSource(); // Replaces CancellationSignal.cs's named-EventWaitHandle IPC (Windows-only): SIGINT/SIGTERM @@ -22,9 +24,11 @@ internal static class Program private static async Task RunAsync(string[] args, CancellationTokenSource cts) { - var command = args[0]; - var rest = args[1..]; - var flags = ParseFlags(rest); + // AppRun (Launcher/build-appimage.sh) invokes this as `wiicompiled-setup --workspace + // [options]` - a global flag ahead of the subcommand - so the command + // word is whichever token isn't part of a --flag/value pair, not strictly args[0]. + var (command, flags) = ParseArgs(args); + if (command is null) { PrintUsage(); return 1; } var progressJson = flags.ContainsKey("progress-json"); IInstallReporter reporter = progressJson ? new NdjsonInstallReporter() : new ConsoleInstallReporter(); @@ -216,24 +220,37 @@ internal static class Program Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WiiCompiled", "Install", profile == "base" ? "Base" : "RetroRewind"); - private static Dictionary ParseFlags(string[] args) + /// + /// A single pass that finds both the command word and every --flag[=value] pair, regardless + /// of order - a --flag may appear before or after the command (see the AppRun caller note in + /// RunAsync). The first token that is neither a --flag nor a value already consumed by the + /// preceding --flag is taken as the command. + /// + private static (string? Command, Dictionary Flags) ParseArgs(string[] args) { + string? command = null; var flags = new Dictionary(); for (var i = 0; i < args.Length; i++) { var arg = args[i]; - if (!arg.StartsWith("--", StringComparison.Ordinal)) continue; - var name = arg[2..]; - if (i + 1 < args.Length && !args[i + 1].StartsWith("--", StringComparison.Ordinal)) + if (arg.StartsWith("--", StringComparison.Ordinal)) { - flags[name] = args[++i]; + var name = arg[2..]; + if (i + 1 < args.Length && !args[i + 1].StartsWith("--", StringComparison.Ordinal)) + { + flags[name] = args[++i]; + } + else + { + flags[name] = null; // boolean flag + } } - else + else if (command is null) { - flags[name] = null; // boolean flag + command = arg; } } - return flags; + return (command, flags); } private static void PrintUsage() diff --git a/Launcher/build-appimage.sh b/Launcher/build-appimage.sh new file mode 100644 index 0000000..9f6ac4c --- /dev/null +++ b/Launcher/build-appimage.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# Packages Launcher/WiiCompiled.Setup.Linux as a self-contained AppImage: a single file Wheel +# Wizard (or anyone else) can fetch and execute with no git clone and no `dotnet` install required +# just to run the installer itself. It still shells out to system clang/cmake/ninja/dolphin-tool - +# no toolchain is bundled, matching Launcher/local-build.sh's own prerequisites. +# +# An AppImage mounts read-only, but local-build.sh writes generated/, native-build/, Assets/, etc. +# into the workspace it's given. So AppRun (written below) copies the bundled workspace snapshot +# out to a writable cache directory on first run, and only ever re-syncs the bundled directories +# (runtime/, aurora-main/, translator/, projects/, local-build.sh) on a later run whose bundled +# version changed - generated/native-build/Assets/PulsarPacks live only in that writable cache and +# are never touched by the sync, so local-build.sh's own incremental caching survives across runs +# and across AppImage updates. +set -euo pipefail + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +workspace=$(cd "$script_dir/.." && pwd) + +output_dir="$workspace/Launcher/dist" +appimagetool_override="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --output-dir) output_dir=$2; shift 2 ;; + --appimagetool) appimagetool_override=$2; shift 2 ;; + -h|--help) + echo "Usage: build-appimage.sh [--output-dir DIR] [--appimagetool PATH]" + exit 0 + ;; + *) echo "build-appimage.sh: unknown argument: $1" >&2; exit 1 ;; + esac +done + +appdir="$workspace/Launcher/artifacts/appimage-build/AppDir" +rm -rf "$appdir" +mkdir -p "$appdir/usr/bin" "$appdir/workspace/Launcher" + +echo "Publishing the installer (self-contained linux-x64)..." +publish_tmp="$workspace/Launcher/artifacts/appimage-build/publish" +rm -rf "$publish_tmp" +dotnet publish "$workspace/Launcher/WiiCompiled.Setup.Linux" -c Release -r linux-x64 \ + --self-contained -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true \ + -o "$publish_tmp" +cp "$publish_tmp/WiiCompiled.Setup.Linux" "$appdir/usr/bin/wiicompiled-setup" +chmod +x "$appdir/usr/bin/wiicompiled-setup" + +echo "Staging the bundled workspace snapshot..." +for dir in runtime aurora-main translator projects; do + cp -r "$workspace/$dir" "$appdir/workspace/$dir" +done +# Mirrors Build-Installer.ps1's own staging exclusions exactly: aurora-main/extern/CMakeLists.txt +# is the real FetchContent driver and must ship, but any already-fetched dependency *subdirectory* +# a developer's local checkout accumulated under extern/ is stale/large build output, not a +# release input - only directories inside extern/ are stripped, never the file itself. runtime/build +# is a plain developer build directory. +find "$appdir/workspace/aurora-main/extern" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} + +rm -rf "$appdir/workspace/runtime/build" +cp "$workspace/Launcher/local-build.sh" "$appdir/workspace/Launcher/local-build.sh" + +if git -C "$workspace" rev-parse HEAD >/dev/null 2>&1; then + git -C "$workspace" rev-parse HEAD > "$appdir/workspace/.bundle-version" +else + date -u +%s > "$appdir/workspace/.bundle-version" +fi + +echo "Writing AppRun..." +cat > "$appdir/AppRun" <<'APPRUN' +#!/bin/bash +set -euo pipefail +HERE="$(dirname "$(readlink -f "$0")")" +CACHE="${XDG_DATA_HOME:-$HOME/.local/share}/WiiCompiled/workspace" +if [ ! -f "$CACHE/.bundle-version" ] || \ + [ "$(cat "$HERE/workspace/.bundle-version")" != "$(cat "$CACHE/.bundle-version")" ]; then + mkdir -p "$CACHE/Launcher" + for dir in runtime aurora-main translator projects; do + rm -rf "$CACHE/$dir" + cp -r "$HERE/workspace/$dir" "$CACHE/$dir" + done + cp "$HERE/workspace/Launcher/local-build.sh" "$CACHE/Launcher/local-build.sh" + cp "$HERE/workspace/.bundle-version" "$CACHE/.bundle-version" +fi +exec "$HERE/usr/bin/wiicompiled-setup" --workspace "$CACHE" "$@" +APPRUN +chmod +x "$appdir/AppRun" + +echo "Writing desktop entry and icon..." +cat > "$appdir/wiicompiled-setup.desktop" <<'DESKTOP' +[Desktop Entry] +Type=Application +Name=WiiCompiled Setup +Comment=Translate, compile, and launch Mario Kart Wii natively on Linux +Exec=AppRun +Icon=wiicompiled-setup +Categories=Game; +Terminal=true +DESKTOP + +# No WiiCompiled logo/icon asset exists anywhere in this repo yet. appimagetool refuses to package +# without one, so this is a minimal solid-color placeholder - a one-line swap for real branding +# later (just replace this generated file with a real wiicompiled-setup.png before packaging). +python3 - "$appdir/wiicompiled-setup.png" <<'PY' +import struct +import sys +import zlib + +path = sys.argv[1] + + +def chunk(tag: bytes, data: bytes) -> bytes: + return struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data)) + + +width = height = 256 +row = b"\x00" + bytes([0x3A, 0x5F, 0x8F, 0xFF]) * width # filter byte + opaque blue-grey pixels +raw = row * height +ihdr = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0) +idat = zlib.compress(raw, 9) + +with open(path, "wb") as handle: + handle.write(b"\x89PNG\r\n\x1a\n") + handle.write(chunk(b"IHDR", ihdr)) + handle.write(chunk(b"IDAT", idat)) + handle.write(chunk(b"IEND", b"")) +PY + +echo "Resolving appimagetool..." +appimagetool="$appimagetool_override" +if [[ -z "$appimagetool" ]]; then + appimagetool="$workspace/Launcher/artifacts/appimagetool" + if [[ ! -x "$appimagetool" ]]; then + echo "Downloading appimagetool..." + mkdir -p "$(dirname "$appimagetool")" + curl -fsSL "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" \ + -o "$appimagetool" + chmod +x "$appimagetool" + fi +fi + +mkdir -p "$output_dir" +echo "Packaging..." +# appimagetool detects the target architecture from the first ELF executable it finds in the +# AppDir; AppRun here is a shell script, not ELF, so ARCH must be set explicitly. +ARCH=x86_64 "$appimagetool" "$appdir" "$output_dir/WiiCompiled-Setup-x86_64.AppImage" +echo "Built: $output_dir/WiiCompiled-Setup-x86_64.AppImage"