diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 5212fba703e..9b286c9fcea 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -16,6 +16,11 @@ on: type: string default: "linux64-deploy" description: "CMake preset for Linux" + package_format: + required: false + type: string + default: "appimage" + description: "Artifact packaging format: appimage (default) or gzip" workflow_dispatch: inputs: game: @@ -32,6 +37,13 @@ on: - linux64-deploy - linux64-testing default: linux64-deploy + package_format: + type: choice + description: "Artifact packaging format" + options: + - appimage + - gzip + default: appimage jobs: build: @@ -45,6 +57,23 @@ jobs: with: fetch-depth: 0 + - name: Normalize package format + id: package-format + shell: bash + run: | + # GeneralsX @bugfix GitHubCopilot 10/04/2026 Normalize invalid package_format to appimage to keep workflow resilient. + FORMAT="${{ inputs.package_format }}" + case "${FORMAT}" in + appimage|gzip) + echo "package_format is valid: ${FORMAT}" + ;; + *) + echo "WARNING: Invalid package_format '${FORMAT}'. Falling back to 'appimage'." + FORMAT="appimage" + ;; + esac + echo "value=${FORMAT}" >> "$GITHUB_OUTPUT" + - name: Cache vcpkg id: cache-vcpkg uses: actions/cache@v4 @@ -176,68 +205,103 @@ jobs: exit 1 fi - - name: Deploy Bundle (Binary + Libraries + Wrapper) - if: success() + # GeneralsX @build GitHubCopilot 10/04/2026 Package as AppImage (self-contained, no FUSE needed for appimagetool in CI via APPIMAGE_EXTRACT_AND_RUN). + - name: Package as AppImage + if: success() && steps.package-format.outputs.value == 'appimage' + env: + APPIMAGE_EXTRACT_AND_RUN: "1" + run: | + if [ "${{ inputs.game }}" = "GeneralsMD" ]; then + ./scripts/build/linux/build-linux-appimage-zh.sh ${{ inputs.preset }} + else + ./scripts/build/linux/build-linux-appimage-generals.sh ${{ inputs.preset }} + fi + + # GeneralsX @build GitHubCopilot 10/04/2026 Legacy gzip bundle kept as opt-in fallback for CI/testing. + - name: Package as gzip bundle + if: success() && steps.package-format.outputs.value == 'gzip' run: | if [ "${{ inputs.game }}" = "GeneralsMD" ]; then BINARY="build/${{ inputs.preset }}/GeneralsMD/GeneralsXZH" - GAME_DIR="GeneralsMD" else BINARY="build/${{ inputs.preset }}/Generals/GeneralsX" - GAME_DIR="Generals" fi - + RUNTIME_DIR="/tmp/GeneralsX-${{ inputs.game }}-deploy" mkdir -p "${RUNTIME_DIR}" - + echo "Creating deployment bundle at: ${RUNTIME_DIR}" echo " Copying executable..." cp -v "${BINARY}" "${RUNTIME_DIR}/$(basename ${BINARY})" chmod +x "${RUNTIME_DIR}/$(basename ${BINARY})" - + echo " Copying DXVK libraries..." find "build/${{ inputs.preset }}/_deps/dxvk-src/lib" -name "*.so*" -exec cp -v {} "${RUNTIME_DIR}/" \; 2>/dev/null || true - + echo " Copying SDL3 libraries..." find "build/${{ inputs.preset }}/_deps/sdl3-build" -name "*.so*" -exec cp -v {} "${RUNTIME_DIR}/" \; 2>/dev/null || true find "build/${{ inputs.preset }}/_deps/sdl3_image-build" -name "*.so*" -exec cp -v {} "${RUNTIME_DIR}/" \; 2>/dev/null || true - # GeneralsX @bugfix felipebraz 05/03/2026 Bundle OpenAL (openal_soft-build output, not provided by runner). echo " Copying OpenAL library..." find "build/${{ inputs.preset }}/_deps/openal_soft-build" -name "*.so*" -exec cp -v {} "${RUNTIME_DIR}/" \; 2>/dev/null || true echo " Copying GameSpy library..." find "build/${{ inputs.preset }}" -name "libgamespy.so*" -exec cp -v {} "${RUNTIME_DIR}/" \; 2>/dev/null || true - + echo " Copying run.sh wrapper from scripts..." cp -v "scripts/qa/smoke/run-bundled-game.sh" "${RUNTIME_DIR}/run.sh" chmod +x "${RUNTIME_DIR}/run.sh" - # GeneralsX @bugfix felipebraz 05/03/2026 Validate required runtime libraries in Linux bundle. if ! compgen -G "${RUNTIME_DIR}/libgamespy.so*" > /dev/null; then - echo "❌ Missing required runtime library: libgamespy.so*" + echo "ERROR: Missing required runtime library: libgamespy.so*" exit 1 fi if ! compgen -G "${RUNTIME_DIR}/libdxvk_d3d8.so*" > /dev/null; then - echo "❌ Missing required runtime library: libdxvk_d3d8.so*" + echo "ERROR: Missing required runtime library: libdxvk_d3d8.so*" exit 1 fi if ! compgen -G "${RUNTIME_DIR}/libSDL3.so*" > /dev/null; then - echo "❌ Missing required runtime library: libSDL3.so*" + echo "ERROR: Missing required runtime library: libSDL3.so*" exit 1 fi if ! compgen -G "${RUNTIME_DIR}/libopenal.so*" > /dev/null; then - echo "❌ Missing required runtime library: libopenal.so*" + echo "ERROR: Missing required runtime library: libopenal.so*" exit 1 fi - - echo "✅ Bundle ready at: ${RUNTIME_DIR}" + + echo "Bundle ready at: ${RUNTIME_DIR}" ls -lh "${RUNTIME_DIR}/" - # GeneralsX @build fbraz3 27/03/2026 Archive bundle as tar (no compression) to preserve POSIX permissions. BUNDLE_TAR="/tmp/GeneralsX-${{ inputs.game }}-${{ inputs.preset }}.tar" tar -C "/tmp" -cf "${BUNDLE_TAR}" "GeneralsX-${{ inputs.game }}-deploy/" - echo "✅ Archive ready: ${BUNDLE_TAR}" + echo "Archive ready: ${BUNDLE_TAR}" + + - name: Set artifact metadata + id: artifact + if: success() + run: | + PRESET="${{ inputs.preset }}" + FORMAT="${{ steps.package-format.outputs.value }}" + GAME_SLUG="${{ inputs.game == 'GeneralsMD' && 'generalsxzh' || 'generalsx' }}" + PRESET_SLUG="${{ inputs.preset == 'linux64-deploy' && 'linux64' || inputs.preset }}" + + if [ "${FORMAT}" = "gzip" ]; then + echo "name=linux-${GAME_SLUG}-${PRESET_SLUG}-bundle" >> "$GITHUB_OUTPUT" + echo "path=/tmp/GeneralsX-${{ inputs.game }}-${PRESET}.tar" >> "$GITHUB_OUTPUT" + else + if [ "${{ inputs.game }}" = "GeneralsMD" ]; then + INPUT_PATH="build/GeneralsXZH-${PRESET}-x86_64.AppImage" + OUTPUT_PATH="/tmp/GeneralsXZH-${PRESET}-x86_64.AppImage" + else + INPUT_PATH="build/GeneralsX-${PRESET}-x86_64.AppImage" + OUTPUT_PATH="/tmp/GeneralsX-${PRESET}-x86_64.AppImage" + fi + + # GeneralsX @bugfix GitHubCopilot 10/04/2026 Flatten uploaded AppImage artifact structure (no intermediate directories inside zip). + cp -f "${INPUT_PATH}" "${OUTPUT_PATH}" + echo "name=linux-${GAME_SLUG}-${PRESET_SLUG}-appimage" >> "$GITHUB_OUTPUT" + echo "path=${OUTPUT_PATH}" >> "$GITHUB_OUTPUT" + fi - name: Upload Build Logs if: always() @@ -251,7 +315,7 @@ jobs: if: success() uses: actions/upload-artifact@v4 with: - name: ${{ format('linux-{0}-{1}-bundle', inputs.game == 'GeneralsMD' && 'generalsxzh' || 'generalsx', inputs.preset == 'linux64-deploy' && 'linux64' || inputs.preset) }} - path: /tmp/GeneralsX-${{ inputs.game }}-${{ inputs.preset }}.tar + name: ${{ steps.artifact.outputs.name }} + path: ${{ steps.artifact.outputs.path }} retention-days: 7 if-no-files-found: warn diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 66b50fe71ea..e0aa68c14c2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,12 +41,14 @@ jobs: with: game: GeneralsMD preset: linux64-deploy + package_format: appimage build-linux-generals: uses: ./.github/workflows/build-linux.yml with: game: Generals preset: linux64-deploy + package_format: appimage build-macos-zh: uses: ./.github/workflows/build-macos.yml @@ -104,16 +106,16 @@ jobs: echo "first_release=false" >> "$GITHUB_OUTPUT" fi - - name: Download Linux bundle artifact + - name: Download Linux appimage artifact uses: actions/download-artifact@v4 with: - name: linux-generalsxzh-linux64-bundle + name: linux-generalsxzh-linux64-appimage path: artifacts/linux - - name: Download Linux Generals bundle artifact + - name: Download Linux Generals appimage artifact uses: actions/download-artifact@v4 with: - name: linux-generalsx-linux64-bundle + name: linux-generalsx-linux64-appimage path: artifacts/linux-generals - name: Download macOS bundle artifact @@ -135,34 +137,41 @@ jobs: run: | mkdir -p release-assets - LINUX_TAR=$(find artifacts/linux -name "*.tar" | head -1) - if [ -z "$LINUX_TAR" ] || [ ! -f "$LINUX_TAR" ]; then - echo "ERROR: Linux bundle tar not found in artifacts/linux" - find artifacts/linux -maxdepth 3 -type f || true - exit 1 + # GeneralsX @build GitHubCopilot 10/04/2026 Prefer AppImage artifacts; fall back to legacy tar bundle. + LINUX_APPIMAGE=$(find artifacts/linux -name "*.AppImage" | head -1) + if [ -n "$LINUX_APPIMAGE" ] && [ -f "$LINUX_APPIMAGE" ]; then + cp "$LINUX_APPIMAGE" release-assets/GeneralsXZH-linux-x86_64.AppImage + LINUX_OUT="$GITHUB_WORKSPACE/release-assets/GeneralsXZH-linux-x86_64.AppImage" + else + LINUX_TAR=$(find artifacts/linux -name "*.tar" | head -1) + if [ -z "$LINUX_TAR" ] || [ ! -f "$LINUX_TAR" ]; then + echo "ERROR: Linux ZH bundle not found in artifacts/linux" + find artifacts/linux -maxdepth 3 -type f || true + exit 1 + fi + mkdir -p /tmp/linux-bundle + tar -xf "$LINUX_TAR" -C /tmp/linux-bundle + (cd /tmp/linux-bundle && zip -r "$GITHUB_WORKSPACE/release-assets/linux-generalsxzh-linux64-bundle.zip" .) + LINUX_OUT="$GITHUB_WORKSPACE/release-assets/linux-generalsxzh-linux64-bundle.zip" fi - mkdir -p /tmp/linux-bundle - tar -xf "$LINUX_TAR" -C /tmp/linux-bundle - ( - cd /tmp/linux-bundle - zip -r "$GITHUB_WORKSPACE/release-assets/linux-generalsxzh-linux64-bundle.zip" . - ) - - LINUX_GENERALS_TAR=$(find artifacts/linux-generals -name "*.tar" | head -1) - if [ -z "$LINUX_GENERALS_TAR" ] || [ ! -f "$LINUX_GENERALS_TAR" ]; then - echo "ERROR: Linux Generals bundle tar not found in artifacts/linux-generals" - find artifacts/linux-generals -maxdepth 3 -type f || true - exit 1 + LINUX_GENERALS_APPIMAGE=$(find artifacts/linux-generals -name "*.AppImage" | head -1) + if [ -n "$LINUX_GENERALS_APPIMAGE" ] && [ -f "$LINUX_GENERALS_APPIMAGE" ]; then + cp "$LINUX_GENERALS_APPIMAGE" release-assets/GeneralsX-linux-x86_64.AppImage + LINUX_GENERALS_OUT="$GITHUB_WORKSPACE/release-assets/GeneralsX-linux-x86_64.AppImage" + else + LINUX_GENERALS_TAR=$(find artifacts/linux-generals -name "*.tar" | head -1) + if [ -z "$LINUX_GENERALS_TAR" ] || [ ! -f "$LINUX_GENERALS_TAR" ]; then + echo "ERROR: Linux Generals bundle not found in artifacts/linux-generals" + find artifacts/linux-generals -maxdepth 3 -type f || true + exit 1 + fi + mkdir -p /tmp/linux-generals-bundle + tar -xf "$LINUX_GENERALS_TAR" -C /tmp/linux-generals-bundle + (cd /tmp/linux-generals-bundle && zip -r "$GITHUB_WORKSPACE/release-assets/linux-generalsx-linux64-bundle.zip" .) + LINUX_GENERALS_OUT="$GITHUB_WORKSPACE/release-assets/linux-generalsx-linux64-bundle.zip" fi - mkdir -p /tmp/linux-generals-bundle - tar -xf "$LINUX_GENERALS_TAR" -C /tmp/linux-generals-bundle - ( - cd /tmp/linux-generals-bundle - zip -r "$GITHUB_WORKSPACE/release-assets/linux-generalsx-linux64-bundle.zip" . - ) - MAC_TAR=$(find artifacts/macos -name "*.tar" | head -1) if [ -z "$MAC_TAR" ] || [ ! -f "$MAC_TAR" ]; then echo "ERROR: macOS app tar not found in artifacts/macos" @@ -192,8 +201,8 @@ jobs: cp "$MAC_GENERALS_TAR" release-assets/GeneralsX-macos-arm64.app.tar zip -j release-assets/macos-generalsx-app.tar.zip release-assets/GeneralsX-macos-arm64.app.tar - echo "linux_generals_asset=$GITHUB_WORKSPACE/release-assets/linux-generalsx-linux64-bundle.zip" >> "$GITHUB_OUTPUT" - echo "linux_asset=$GITHUB_WORKSPACE/release-assets/linux-generalsxzh-linux64-bundle.zip" >> "$GITHUB_OUTPUT" + echo "linux_generals_asset=$LINUX_GENERALS_OUT" >> "$GITHUB_OUTPUT" + echo "linux_asset=$LINUX_OUT" >> "$GITHUB_OUTPUT" echo "macos_generals_asset=$GITHUB_WORKSPACE/release-assets/macos-generalsx-app.tar.zip" >> "$GITHUB_OUTPUT" echo "macos_asset=$GITHUB_WORKSPACE/release-assets/macos-generalsxzh-app.tar.zip" >> "$GITHUB_OUTPUT" @@ -371,8 +380,8 @@ jobs: echo "- Create release: ${{ inputs.create_release }}" >> "$GITHUB_STEP_SUMMARY" echo "- Pre-release: ${{ inputs.is_prerelease }}" >> "$GITHUB_STEP_SUMMARY" echo "- Dry run: ${{ inputs.dry_run }}" >> "$GITHUB_STEP_SUMMARY" - echo "- Linux asset (Generals): linux-generalsx-linux64-bundle.zip" >> "$GITHUB_STEP_SUMMARY" - echo "- Linux asset: linux-generalsxzh-linux64-bundle.zip" >> "$GITHUB_STEP_SUMMARY" + echo "- Linux asset (Generals): GeneralsX-linux-x86_64.AppImage" >> "$GITHUB_STEP_SUMMARY" + echo "- Linux asset: GeneralsXZH-linux-x86_64.AppImage" >> "$GITHUB_STEP_SUMMARY" echo "- macOS asset (Generals): macos-generalsx-app.tar.zip" >> "$GITHUB_STEP_SUMMARY" echo "- macOS asset: macos-generalsxzh-app.tar.zip" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 9f5a85b6393..9a978e4007d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -94,6 +94,36 @@ }, "problemMatcher": [] }, + { + "label": "[Linux] Build AppImage GeneralsX", + "type": "shell", + "command": "./scripts/build/linux/build-linux-appimage-generals.sh linux64-deploy", + "isBackground": false, + "group": "build", + "presentation": { + "reveal": "always", + "panel": "dedicated", + "showReuseMessage": false, + "clear": false, + "focus": false + }, + "problemMatcher": [] + }, + { + "label": "[Linux] Build AppImage GeneralsXZH", + "type": "shell", + "command": "./scripts/build/linux/build-linux-appimage-zh.sh linux64-deploy", + "isBackground": false, + "group": "build", + "presentation": { + "reveal": "always", + "panel": "dedicated", + "showReuseMessage": false, + "clear": false, + "focus": false + }, + "problemMatcher": [] + }, { "label": "[Linux] Bundle GeneralsXZH", "type": "shell", diff --git a/docs/BUILD/INSTALL_INSTRUCTIONS.md b/docs/BUILD/INSTALL_INSTRUCTIONS.md index 9e91a07e337..cd1a290a12d 100644 --- a/docs/BUILD/INSTALL_INSTRUCTIONS.md +++ b/docs/BUILD/INSTALL_INSTRUCTIONS.md @@ -13,11 +13,42 @@ Legacy fallback during migration is still supported: ## Linux -1. Download the Linux archive from this release. -2. Extract all files into your Zero Hour directory (for example, `$HOME/GeneralsX/GeneralsZH`). Overwrite existing files if prompted. -3. Some dependencies (such as DXVK) require specific environment variables. The easiest way to launch the game is to run the provided `run.sh` script from a terminal. +1. Download the `.AppImage` file from this release (`GeneralsXZH-linux-x86_64.AppImage` for Zero Hour, `GeneralsX-linux-x86_64.AppImage` for the base game). +2. Open a terminal, make it executable, and run it: -If your existing setup still uses `$HOME/GeneralsX/GeneralsMD`, release scripts keep compatibility with that legacy path. + ```bash + chmod +x GeneralsXZH-linux-x86_64.AppImage + ./GeneralsXZH-linux-x86_64.AppImage -win + ``` + + Troubleshooting: AppImages commonly use FUSE to mount their embedded filesystem at launch. If direct execution fails on a minimal or sandboxed system, try: + + ```bash + APPIMAGE_EXTRACT_AND_RUN=1 ./GeneralsXZH-linux-x86_64.AppImage -win + ``` + + Or extract and run manually: + + ```bash + ./GeneralsXZH-linux-x86_64.AppImage --appimage-extract + ./squashfs-root/AppRun -win + ``` + +3. The AppImage auto-detects game data in the following default locations (checked in order): + - `$HOME/GeneralsX/GeneralsZH` (preferred) + - `$HOME/GeneralsX/GeneralsMD` (legacy fallback) + + If your assets are stored elsewhere, set the environment variable before launching: + + ```bash + CNC_GENERALS_ZH_PATH=/path/to/your/zero-hour-data ./GeneralsXZH-linux-x86_64.AppImage -win + ``` + + For the base game, use `CNC_GENERALS_PATH` instead. + +4. All runtime libraries (DXVK, SDL3, OpenAL, FFmpeg, etc.) are bundled inside the AppImage. No additional packages need to be installed. + +> **GPU Driver note**: Vulkan support must be provided by your host GPU driver. For NVIDIA use the proprietary driver, for AMD/Intel use Mesa 21+. The AppImage does not bundle GPU drivers. ## macOS diff --git a/docs/DEV_BLOG/2026-04-DIARY.md b/docs/DEV_BLOG/2026-04-DIARY.md index 6be6f818c31..311bf0f6931 100644 --- a/docs/DEV_BLOG/2026-04-DIARY.md +++ b/docs/DEV_BLOG/2026-04-DIARY.md @@ -2,6 +2,130 @@ --- +## 2026-04-10 (SESSION CURRENT): Linux build workflow input normalization and AppImage toolchain supply-chain hardening + +Applied two related CI/script hardening updates to reduce avoidable failures and tighten packaging trust boundaries. + +**Workflow update (`build-linux.yml`):** +- Removed the standalone `validate-package-format` job. +- Added in-job normalization step (`package-format`) to sanitize unexpected `package_format` values. +- Invalid values now fall back to `appimage` with warning (fail-safe behavior). +- Packaging conditionals and artifact metadata now use the sanitized output. + +**AppImage script security update:** +- Updated both scripts: + - `scripts/build/linux/build-linux-appimage-zh.sh` + - `scripts/build/linux/build-linux-appimage-generals.sh` +- Replaced legacy floating URL defaults with pinned `AppImage/appimagetool` release (`1.9.1`). +- Added strict URL validation to reject `continuous` and non-release URL patterns. +- Enabled mandatory SHA-256 validation for downloaded `appimagetool` binaries. +- In CI, force use of pinned downloaded binary instead of an arbitrary system `appimagetool` from `PATH`. + +**Validation:** +- `bash -n` syntax checks passed for both updated AppImage scripts. +- YAML diagnostics remain clean for workflow updates. + +**Takeaway:** +For release packaging, deterministic input handling and pinned+verified build tools are mandatory to reduce CI flakiness and supply-chain exposure. + +## 2026-04-10 (SESSION CURRENT): AppImage hardened against FFmpeg SONAME mismatch on newer Ubuntu + +Addressed user-reported runtime failures on Ubuntu 25.10 where host FFmpeg SONAMEs differ from what the game binary requires (`libavcodec.so.60`, `libavformat.so.60`, `libavutil.so.58`, `libswscale.so.7`). + +**Root cause:** +- AppImage builders did not bundle FFmpeg runtime libs, so binaries resolved host libraries. +- Host symlink workarounds do not satisfy ELF symbol versioning (`LIBAVCODEC_60`, `LIBAVFORMAT_60`, etc.). + +**Fixes:** +- Updated both builders: + - `scripts/build/linux/build-linux-appimage-zh.sh` + - `scripts/build/linux/build-linux-appimage-generals.sh` +- Added FFmpeg SONAME bundling + codec dependency closure copy. +- Added fail-fast checks ensuring required FFmpeg libs are present in AppDir runtime. + +**Validation:** +- Both AppImage builds succeeded. +- Verified bundled libs in both AppDirs include: + - `libavcodec.so.60` + - `libavformat.so.60` + - `libavutil.so.58` + - `libswscale.so.7` + - `libswresample.so.4` + +**Takeaway:** +For this project, AppImage compatibility requires shipping version-matched FFmpeg userspace libs; host symlinks are not a reliable solution for symbol-versioned dependencies. + +## 2026-04-10 (SESSION CURRENT): Fixed AppImage task failure caused by removed flatpak icon path + +Resolved a regression where `[Linux] Build AppImage GeneralsXZH` failed after `flatpak/` removal. + +**Root cause:** +- ZH AppImage builder still referenced icon path under `flatpak/`. +- `appimagetool` requires the desktop icon file referenced by the `.desktop` entry. + +**Fixes:** +- Updated ZH builder to use assets-based icon path and fallback order: + - `assets/generalsx-zh_icon.png` + - fallback `assets/generalsx_icon.png` +- Added explicit fail-fast icon checks for both ZH and Generals AppImage builders. + +**Validation:** +- `./scripts/build/linux/build-linux-appimage-zh.sh linux64-deploy` succeeded. +- `./scripts/build/linux/build-linux-appimage-generals.sh linux64-deploy` succeeded. +- Both AppImage artifacts were generated under `build/`. + +## 2026-04-09 (SESSION CURRENT): AppImage launcher path resolution fixed for CNC_GENERALS_* env overrides + +Improved AppImage `AppRun` to resolve assets deterministically and honor user-provided environment variables before fallback auto-detection. + +**Changes:** +- Updated launcher generation in `scripts/build/linux/build-linux-appimage-zh.sh`: + - Prioritizes `CNC_GENERALS_ZH_PATH` when it points to an existing directory + - Prioritizes `CNC_GENERALS_PATH` (and maps to `CNC_GENERALS_INSTALLPATH` if needed) + - Keeps fallback auto-detection (AppImage directory, launch directory, common `~/GeneralsX/*` paths) + - Switches working directory to resolved ZH asset path when available +- Updated AppImage docs and scripts README with explicit override examples + +**Validation:** +- Rebuilt AppImage successfully with updated launcher logic. +- Runtime output confirms resolved asset/base path messages and normal startup progression. + +## 2026-04-09 (SESSION CURRENT): AppImage base-game counterpart added and VS Code tasks aligned + +Expanded the AppImage work on this branch so packaging is no longer Zero Hour-only. + +**Implemented:** +- Added `scripts/build/linux/build-linux-appimage-generals.sh` for base Generals. +- Updated VS Code tasks to expose Linux AppImage packaging for both variants. +- Extended AppImage support docs and usage examples. + +**Result:** +- Branch now carries the AppImage packaging track cleanly for both games. + +## 2026-04-09 (SESSION CURRENT): AppImage PoC implemented and validated (short-term Linux packaging path) + +Given repeated Linux packaging friction around runtime compatibility, implemented a practical AppImage packaging path for Zero Hour as a short-term distribution strategy. + +**Implemented:** +- New builder script: `scripts/build/linux/build-linux-appimage-zh.sh` + - Creates AppDir + AppImage for `GeneralsXZH` + - Bundles DXVK, SDL3, SDL3_image, OpenAL, and GameSpy runtime libs + - Generates AppRun launcher with existing DXVK/OpenAL runtime defaults + - Reuses existing icon asset and desktop metadata + - Auto-downloads `appimagetool` when not installed globally +- Updated scripts inventory docs in `scripts/README.md` +- Added active-work note `docs/WORKDIR/support/APPIMAGE_POC_PLAN_2026-04.md` + +**Validation result:** +- AppImage generated successfully: + - `build/GeneralsXZH-linux64-deploy-x86_64.AppImage` +- Smoke launch succeeded through Vulkan window creation and early engine initialization: + - Vulkan library loaded + - SDL3 Vulkan window created successfully + - Engine initialization and INI loading started normally + +**Takeaway:** +AppImage is currently a viable short-term Linux packaging path for this project state. ## 2026-04-08 (SESSION 113): Remove stale local DXVK patch-flow narrative Aligned macOS DXVK docs and CMake comments with the current pinned-fork source model and deprecated the old local patch helper script. diff --git a/docs/WORKDIR/lessons/2026-04-LESSONS.md b/docs/WORKDIR/lessons/2026-04-LESSONS.md index 84c958af9fb..28405a99d73 100644 --- a/docs/WORKDIR/lessons/2026-04-LESSONS.md +++ b/docs/WORKDIR/lessons/2026-04-LESSONS.md @@ -1,5 +1,12 @@ # 2026-04 Lessons Learned +## Session 2026-04-09 - AppImage can bypass current Flatpak Vulkan/XCB coupling blockers + +- Problem: Flatpak remained blocked by Vulkan ICD/XCB symbol incompatibilities despite multiple runtime workarounds. +- Action: Implemented an AppImage packaging PoC for `GeneralsXZH` bundling userspace runtime libs (DXVK, SDL3, SDL3_image, OpenAL, GameSpy) with a dedicated launcher. +- Result: AppImage launched successfully and progressed beyond Vulkan window creation and early engine initialization, where Flatpak path previously failed. +- Insight: For short-term Linux distribution, AppImage is currently lower-risk and faster to stabilize than Flatpak in this codebase state. +- Prevention: Keep Flatpak as a parallel track for longer-term sandbox goals, but prioritize AppImage for immediate user-facing releases. ## Session 2026-04-01 - User-facing path migrations need runtime fallback, not just docs updates - Problem: Zero Hour user-facing scripts and docs exposed the internal `GeneralsMD` path, which leaks implementation details and conflicts with product naming (`GeneralsZH`). diff --git a/docs/WORKDIR/support/APPIMAGE_POC_PLAN_2026-04.md b/docs/WORKDIR/support/APPIMAGE_POC_PLAN_2026-04.md new file mode 100644 index 00000000000..34ded96dd03 --- /dev/null +++ b/docs/WORKDIR/support/APPIMAGE_POC_PLAN_2026-04.md @@ -0,0 +1,71 @@ +# AppImage PoC Plan (April 2026) + +## Why AppImage now + +Flatpak currently requires heavy runtime workarounds for Vulkan WSI/XCB compatibility. +A short-term AppImage path reduces distro ABI friction while keeping distribution simple for end users. + +## PoC Scope + +- Targets: + - Zero Hour runtime (`GeneralsXZH`) + - Generals base runtime (`GeneralsX`) +- Output: portable files under `build/` +- Tooling: + - `scripts/build/linux/build-linux-appimage-zh.sh` + - `scripts/build/linux/build-linux-appimage-generals.sh` + +## Included runtime artifacts + +- Game binary (`GeneralsXZH` or `GeneralsX`) +- DXVK userspace libs (`libdxvk_d3d8.so*`, optional d3d9) +- SDL3 + SDL3_image +- OpenAL +- GameSpy +- FFmpeg runtime libs with matching SONAMEs (`libavcodec.so.60`, `libavformat.so.60`, `libavutil.so.58`, `libswscale.so.7`, `libswresample.so.4`) plus transitive codec deps +- Optional `dxvk.conf` + +## Launcher behavior + +- Uses bundled libs via `LD_LIBRARY_PATH` +- Exposes DXVK defaults (`DXVK_WSI_DRIVER=SDL3`, logging/HUD knobs) +- Honors user overrides first, then auto-detects paths: + - `CNC_GENERALS_ZH_PATH` (Zero Hour assets) + - `CNC_GENERALS_PATH` and `CNC_GENERALS_INSTALLPATH` (base Generals assets) + - fallback auto-detection from AppImage directory, current launch directory, and common `~/GeneralsX/*` paths +- Keeps OpenAL workaround env for known alignment/backend issues + +### Recommended launch form with explicit paths + +```bash +CNC_GENERALS_ZH_PATH="/path/to/GeneralsZH_or_GeneralsMD" \ +CNC_GENERALS_PATH="/path/to/Generals" \ +./build/GeneralsXZH-linux64-deploy-x86_64.AppImage -win +``` + +For base Generals: + +```bash +CNC_GENERALS_PATH="/path/to/Generals" \ +./build/GeneralsX-linux64-deploy-x86_64.AppImage -win +``` + +## Build command + +Example: + +- `./scripts/build/linux/build-linux-appimage-zh.sh linux64-deploy` +- `./scripts/build/linux/build-linux-appimage-generals.sh linux64-deploy` + +## Validation checklist + +- AppImage file created under `build/` +- Binary starts from AppImage launcher +- Intro/menu reached with expected Vulkan + SDL3 path +- Smoke test on at least two distro families + +## Risks + +- Host driver stack (Vulkan ICD) still remains a runtime dependency +- Some environments may require additional policy tweaks (for example FUSE or sandbox constraints) +- Build baseline still matters for broad compatibility (prefer building AppImage on an older supported distro) diff --git a/scripts/README.md b/scripts/README.md index fa7e1aafa23..8ef6c0142f1 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -8,10 +8,12 @@ This folder is organized by function for easier maintenance and discovery. #### `build/linux/` - Linux & Docker Build Scripts for Linux native and Docker-based builds: +- `build-linux-appimage-generals.sh` - Package GeneralsX as AppImage (portable single-file Linux distribution) - `docker-configure-linux.sh` - Configure CMake for Linux build - `docker-build-linux-zh.sh` - Build GeneralsXZH (Zero Hour) for Linux - `docker-build-linux-generals.sh` - Build GeneralsX (base game) for Linux - `docker-build-mingw-zh.sh` - Cross-compile Windows .exe via MinGW in Docker +- `build-linux-appimage-zh.sh` - Package GeneralsXZH as AppImage (portable single-file Linux distribution) - `bundle-linux-zh.sh` - Bundle compiled binaries - `deploy-linux-zh.sh` - Deploy to runtime directory - `run-linux-zh.sh` - Launch the game windowed @@ -104,6 +106,17 @@ brew install --cask docker # 6. Run ./scripts/build/linux/run-linux-zh.sh -win + +# 7. Optional: build AppImage package +./scripts/build/linux/build-linux-appimage-zh.sh linux64-deploy + +# 7b. Optional: build AppImage package for base Generals +./scripts/build/linux/build-linux-appimage-generals.sh linux64-deploy + +# 8. Optional: run AppImage with explicit asset paths +CNC_GENERALS_ZH_PATH="/path/to/GeneralsZH_or_GeneralsMD" \ +CNC_GENERALS_PATH="/path/to/Generals" \ +./build/GeneralsXZH-linux64-deploy-x86_64.AppImage -win ``` ### macOS Build diff --git a/scripts/build/linux/build-linux-appimage-generals.sh b/scripts/build/linux/build-linux-appimage-generals.sh new file mode 100755 index 00000000000..6c54f5b1e77 --- /dev/null +++ b/scripts/build/linux/build-linux-appimage-generals.sh @@ -0,0 +1,333 @@ +#!/usr/bin/env bash +# GeneralsX @build GitHubCopilot 09/04/2026 Build a portable AppImage package for GeneralsX on Linux. +# Usage: +# ./scripts/build/linux/build-linux-appimage-generals.sh [preset] +set -euo pipefail + +PRESET="${1:-linux64-deploy}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" +BUILD_DIR="${PROJECT_ROOT}/build/${PRESET}" +APPIMAGE_ROOT="${PROJECT_ROOT}/build/appimage" +APPDIR="${APPIMAGE_ROOT}/GeneralsX.AppDir" +OUTPUT_APPIMAGE="${PROJECT_ROOT}/build/GeneralsX-${PRESET}-x86_64.AppImage" +# GeneralsX @build GitHubCopilot 10/04/2026 Pin appimagetool to immutable upstream release and enforce checksum validation. +APPIMAGETOOL_VERSION="${APPIMAGETOOL_VERSION:-1.9.1}" +APPIMAGETOOL_URL="${APPIMAGETOOL_URL:-https://github.com/AppImage/appimagetool/releases/download/${APPIMAGETOOL_VERSION}/appimagetool-x86_64.AppImage}" +APPIMAGETOOL_SHA256="${APPIMAGETOOL_SHA256:-ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0}" + +DXVK_LIB_DIR="${BUILD_DIR}/_deps/dxvk-src/lib" +SDL3_LIB_DIR="${BUILD_DIR}/_deps/sdl3-build" +SDL3_IMAGE_LIB_DIR="${BUILD_DIR}/_deps/sdl3_image-build" +OPENAL_LIB_DIR="${BUILD_DIR}/_deps/openal_soft-build" +FFMPEG_LIB_DIR="/usr/lib/x86_64-linux-gnu" +FFMPEG_DEP_LIB_DIR="/lib/x86_64-linux-gnu" +BINARY_SRC="${BUILD_DIR}/Generals/GeneralsX" +GAMESPY_LIB="${BUILD_DIR}/libgamespy.so" +DXVK_CONF_SRC="${PROJECT_ROOT}/resources/dxvk/dxvk.conf" +ICON_SRC="${PROJECT_ROOT}/assets/generalsx_icon.png" + +copy_optional_libs() { + local source_dir="$1" + local pattern="$2" + if [[ -d "${source_dir}" ]]; then + local matches=() + shopt -s nullglob + matches=("${source_dir}"/${pattern}) + shopt -u nullglob + if (( ${#matches[@]} > 0 )); then + cp -a "${matches[@]}" "${APPDIR}/usr/lib/" + fi + fi +} + +copy_codec_dep() { + local pattern="$1" + copy_optional_libs "${FFMPEG_DEP_LIB_DIR}" "${pattern}" + copy_optional_libs "${FFMPEG_LIB_DIR}" "${pattern}" +} + +copy_ldd_deps() { + local root="$1" + [[ -e "${root}" ]] || return 0 + + while IFS= read -r dep; do + case "${dep}" in + # GeneralsX @bugfix GitHubCopilot 10/04/2026 Exclude glibc loader/runtime files across common Linux layouts to preserve AppImage portability. + linux-vdso.so.1 | \ + /lib64/ld-linux* | /lib/*/ld-linux* | /usr/lib/*/ld-linux* | /usr/lib64/ld-linux* | \ + /lib/*/libc.so.* | /lib64/libc.so.* | /usr/lib/*/libc.so.* | /usr/lib64/libc.so.* | \ + /lib/*/libm.so.* | /lib64/libm.so.* | /usr/lib/*/libm.so.* | /usr/lib64/libm.so.* | \ + /lib/*/libpthread.so.* | /lib64/libpthread.so.* | /usr/lib/*/libpthread.so.* | /usr/lib64/libpthread.so.* | \ + /lib/*/librt.so.* | /lib64/librt.so.* | /usr/lib/*/librt.so.* | /usr/lib64/librt.so.* | \ + /lib/*/libdl.so.* | /lib64/libdl.so.* | /usr/lib/*/libdl.so.* | /usr/lib64/libdl.so.*) + continue + ;; + esac + + cp -a "${dep}" "${APPDIR}/usr/lib/" 2>/dev/null || true + if [[ -L "${dep}" ]]; then + local resolved + resolved="$(readlink -f "${dep}")" + cp -a "${resolved}" "${APPDIR}/usr/lib/" 2>/dev/null || true + fi + done < <(ldd "${root}" | awk '{for (i = 1; i <= NF; ++i) { if ($i ~ /^\//) { print $i; break } }}' | sort -u) +} + +verify_sha256_if_configured() { + local file_path="$1" + + if [[ -z "${APPIMAGETOOL_SHA256}" ]]; then + echo "ERROR: APPIMAGETOOL_SHA256 is required for appimagetool verification." >&2 + exit 1 + fi + + if command -v sha256sum >/dev/null 2>&1; then + echo "${APPIMAGETOOL_SHA256} ${file_path}" | sha256sum -c - + elif command -v shasum >/dev/null 2>&1; then + local actual_sha256 + actual_sha256="$(shasum -a 256 "${file_path}" | awk '{print $1}')" + if [[ "${actual_sha256}" != "${APPIMAGETOOL_SHA256}" ]]; then + echo "ERROR: appimagetool SHA-256 mismatch" >&2 + echo "Expected: ${APPIMAGETOOL_SHA256}" >&2 + echo "Actual: ${actual_sha256}" >&2 + exit 1 + fi + else + echo "ERROR: Neither sha256sum nor shasum is available for checksum verification." >&2 + exit 1 + fi +} + +validate_appimagetool_source() { + case "${APPIMAGETOOL_URL}" in + https://github.com/AppImage/appimagetool/releases/download/*/appimagetool-x86_64.AppImage) + ;; + *) + echo "ERROR: APPIMAGETOOL_URL must target a pinned AppImage/appimagetool release asset." >&2 + exit 1 + ;; + esac + + if [[ "${APPIMAGETOOL_URL}" == *"/releases/download/continuous/"* ]]; then + echo "ERROR: APPIMAGETOOL_URL must not use floating continuous channel." >&2 + exit 1 + fi + + if [[ ! "${APPIMAGETOOL_SHA256}" =~ ^[A-Fa-f0-9]{64}$ ]]; then + echo "ERROR: APPIMAGETOOL_SHA256 must be a 64-character hexadecimal SHA-256 digest." >&2 + exit 1 + fi +} + +validate_appimagetool_source + +if [[ ! -f "${BINARY_SRC}" || ! -s "${BINARY_SRC}" ]]; then + echo "ERROR: Missing or empty binary: ${BINARY_SRC}" >&2 + echo "Build first: ./scripts/build/linux/docker-build-linux-generals.sh ${PRESET}" >&2 + exit 1 +fi +if [[ ! -d "${DXVK_LIB_DIR}" ]]; then + echo "ERROR: Missing DXVK libs dir: ${DXVK_LIB_DIR}" >&2 + exit 1 +fi +if [[ ! -d "${SDL3_LIB_DIR}" || ! -d "${SDL3_IMAGE_LIB_DIR}" ]]; then + echo "ERROR: Missing SDL3/SDL3_image build dirs under ${BUILD_DIR}" >&2 + exit 1 +fi +if [[ ! -f "${GAMESPY_LIB}" ]]; then + echo "ERROR: Missing GameSpy lib: ${GAMESPY_LIB}" >&2 + exit 1 +fi + +rm -rf "${APPDIR}" +mkdir -p "${APPDIR}/usr/bin" "${APPDIR}/usr/lib" "${APPDIR}/usr/share/applications" "${APPDIR}/usr/share/icons/hicolor/512x512/apps" + +cp "${BINARY_SRC}" "${APPDIR}/usr/bin/GeneralsX" +chmod +x "${APPDIR}/usr/bin/GeneralsX" +cp "${GAMESPY_LIB}" "${APPDIR}/usr/lib/" +copy_optional_libs "${DXVK_LIB_DIR}" "libdxvk_d3d8.so*" +copy_optional_libs "${DXVK_LIB_DIR}" "libdxvk_d3d9.so*" +copy_optional_libs "${SDL3_LIB_DIR}" "libSDL3.so*" +copy_optional_libs "${SDL3_IMAGE_LIB_DIR}" "libSDL3_image.so*" +copy_optional_libs "${OPENAL_LIB_DIR}" "libopenal.so*" + +# GeneralsX @bugfix GitHubCopilot 10/04/2026 Bundle FFmpeg SONAME-compatible libs to avoid host version mismatch (e.g. Ubuntu 25.10). +copy_codec_dep "libavcodec.so*" +copy_codec_dep "libavformat.so*" +copy_codec_dep "libavutil.so*" +copy_codec_dep "libswresample.so*" +copy_codec_dep "libswscale.so*" + +# Include transitive codec dependencies required by FFmpeg libs. +copy_codec_dep "libzvbi.so*" +copy_codec_dep "libsnappy.so*" +copy_codec_dep "libaom.so*" +copy_codec_dep "libcodec2.so*" +copy_codec_dep "libgsm.so*" +copy_codec_dep "libjxl.so*" +copy_codec_dep "libjxl_threads.so*" +copy_codec_dep "libmp3lame.so*" +copy_codec_dep "libopenjp2.so*" +copy_codec_dep "libopus.so*" +copy_codec_dep "librav1e.so*" +copy_codec_dep "libshine.so*" +copy_codec_dep "libspeex.so*" +copy_codec_dep "libSvtAv1Enc.so*" +copy_codec_dep "libtheoraenc.so*" +copy_codec_dep "libtheoradec.so*" +copy_codec_dep "libtwolame.so*" +copy_codec_dep "libvorbis.so*" +copy_codec_dep "libvorbisenc.so*" +copy_codec_dep "libwebp.so*" +copy_codec_dep "libwebpmux.so*" +copy_codec_dep "libx264.so*" +copy_codec_dep "libx265.so*" +copy_codec_dep "libxvidcore.so*" +copy_codec_dep "libsoxr.so*" +copy_codec_dep "libvpl.so*" +copy_codec_dep "libva.so*" +copy_codec_dep "libva-drm.so*" +copy_codec_dep "libva-x11.so*" +copy_codec_dep "libvdpau.so*" +copy_codec_dep "libOpenCL.so*" + +shopt -s nullglob +for ffmpeg_root in "${APPDIR}"/usr/lib/libavcodec.so* "${APPDIR}"/usr/lib/libavformat.so* "${APPDIR}"/usr/lib/libavutil.so*; do + copy_ldd_deps "${ffmpeg_root}" +done +shopt -u nullglob + +if ! compgen -G "${APPDIR}/usr/lib/libavcodec.so*" > /dev/null; then + echo "ERROR: Missing required AppImage runtime library libavcodec.so*" >&2 + exit 1 +fi +if ! compgen -G "${APPDIR}/usr/lib/libavformat.so*" > /dev/null; then + echo "ERROR: Missing required AppImage runtime library libavformat.so*" >&2 + exit 1 +fi +if ! compgen -G "${APPDIR}/usr/lib/libavutil.so*" > /dev/null; then + echo "ERROR: Missing required AppImage runtime library libavutil.so*" >&2 + exit 1 +fi + +if [[ -f "${DXVK_CONF_SRC}" ]]; then + mkdir -p "${APPDIR}/usr/share/generalsx" + cp "${DXVK_CONF_SRC}" "${APPDIR}/usr/share/generalsx/dxvk.conf" +fi + +if [[ ! -f "${ICON_SRC}" ]]; then + echo "ERROR: Missing icon asset: ${ICON_SRC}" >&2 + exit 1 +fi + +cat > "${APPDIR}/AppRun" << 'EOF' +#!/usr/bin/env bash +# GeneralsX @build GitHubCopilot 09/04/2026 AppImage runtime launcher for GeneralsX. +# GeneralsX @bugfix GitHubCopilot 09/04/2026 Honor CNC_GENERALS_PATH / CNC_GENERALS_INSTALLPATH with deterministic precedence. +set -euo pipefail + +APPDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +export LD_LIBRARY_PATH="${APPDIR}/usr/lib:${LD_LIBRARY_PATH:-}" +export DXVK_WSI_DRIVER="SDL3" +export DXVK_LOG_LEVEL="${DXVK_LOG_LEVEL:-info}" +export DXVK_HUD="${DXVK_HUD:-0}" + +with_trailing_slash() { + local path="$1" + if [[ "${path}" == */ ]]; then + printf '%s' "${path}" + else + printf '%s/' "${path}" + fi +} + +has_big_files() { + local path="$1" + [[ -d "${path}" ]] || return 1 + find "${path}" -maxdepth 1 -type f -iname '*.big' | grep -q . +} + +APPIMAGE_HOST_DIR="" +if [[ -n "${APPIMAGE:-}" ]]; then + APPIMAGE_HOST_DIR="$(cd "$(dirname "${APPIMAGE}")" && pwd)" +fi +LAUNCH_DIR="$(pwd)" + +if [[ -n "${CNC_GENERALS_PATH:-}" ]]; then + if [[ ! -d "${CNC_GENERALS_PATH}" ]]; then + echo "WARNING: CNC_GENERALS_PATH='${CNC_GENERALS_PATH}' does not exist; falling back to auto-detection" + unset CNC_GENERALS_PATH + else + export CNC_GENERALS_PATH="$(with_trailing_slash "${CNC_GENERALS_PATH}")" + fi +fi + +if [[ -z "${CNC_GENERALS_PATH:-}" && -n "${CNC_GENERALS_INSTALLPATH:-}" && -d "${CNC_GENERALS_INSTALLPATH}" ]]; then + export CNC_GENERALS_PATH="$(with_trailing_slash "${CNC_GENERALS_INSTALLPATH}")" +fi +if [[ -z "${CNC_GENERALS_PATH:-}" && -n "${APPIMAGE_HOST_DIR}" ]] && has_big_files "${APPIMAGE_HOST_DIR}"; then + export CNC_GENERALS_PATH="$(with_trailing_slash "${APPIMAGE_HOST_DIR}")" +fi +if [[ -z "${CNC_GENERALS_PATH:-}" ]] && has_big_files "${LAUNCH_DIR}"; then + export CNC_GENERALS_PATH="$(with_trailing_slash "${LAUNCH_DIR}")" +fi +if [[ -z "${CNC_GENERALS_PATH:-}" ]] && has_big_files "${HOME}/GeneralsX/Generals"; then + export CNC_GENERALS_PATH="$(with_trailing_slash "${HOME}/GeneralsX/Generals")" +fi + +if [[ -n "${CNC_GENERALS_PATH:-}" && -z "${CNC_GENERALS_INSTALLPATH:-}" ]]; then + export CNC_GENERALS_INSTALLPATH="$(with_trailing_slash "${CNC_GENERALS_PATH}")" +fi + +if [[ -n "${CNC_GENERALS_PATH:-}" ]]; then + echo "INFO: AppImage base Generals path: ${CNC_GENERALS_PATH}" + cd "${CNC_GENERALS_PATH}" +fi + +if [[ -z "${ALSOFT_DISABLE_CPU_EXTS:-}" ]]; then + export ALSOFT_DISABLE_CPU_EXTS="all" +fi +if [[ -z "${ALSOFT_DRIVERS:-}" ]]; then + export ALSOFT_DRIVERS="pulse,alsa,oss,jack,null,wave" +fi + +exec "${APPDIR}/usr/bin/GeneralsX" "$@" +EOF +chmod +x "${APPDIR}/AppRun" + +cat > "${APPDIR}/GeneralsX.desktop" << 'EOF' +[Desktop Entry] +Type=Application +Name=Command & Conquer Generals (GeneralsX) +Comment=Cross-platform Generals runtime +Exec=GeneralsX +Icon=GeneralsX +Categories=Game;StrategyGame; +Terminal=false +EOF +cp "${APPDIR}/GeneralsX.desktop" "${APPDIR}/usr/share/applications/GeneralsX.desktop" + +cp "${ICON_SRC}" "${APPDIR}/GeneralsX.png" +cp "${ICON_SRC}" "${APPDIR}/usr/share/icons/hicolor/512x512/apps/GeneralsX.png" + +if command -v appimagetool >/dev/null 2>&1 && [[ -z "${CI:-}" ]]; then + APPIMAGETOOL_BIN="$(command -v appimagetool)" +else + # GeneralsX @build GitHubCopilot 10/04/2026 Use pinned appimagetool artifact with mandatory SHA-256 verification for reproducible packaging. + APPIMAGETOOL_BIN="${APPIMAGE_ROOT}/appimagetool.AppImage" + mkdir -p "${APPIMAGE_ROOT}" + if [[ ! -f "${APPIMAGETOOL_BIN}" ]]; then + echo "Downloading appimagetool..." + curl -fL --retry 3 --output "${APPIMAGETOOL_BIN}" "${APPIMAGETOOL_URL}" + fi + verify_sha256_if_configured "${APPIMAGETOOL_BIN}" + chmod +x "${APPIMAGETOOL_BIN}" +fi + +ARCH=x86_64 "${APPIMAGETOOL_BIN}" "${APPDIR}" "${OUTPUT_APPIMAGE}" + +echo "AppImage generated: ${OUTPUT_APPIMAGE}" +echo "Run example:" +echo " chmod +x ${OUTPUT_APPIMAGE}" +echo " ${OUTPUT_APPIMAGE} -win" \ No newline at end of file diff --git a/scripts/build/linux/build-linux-appimage-zh.sh b/scripts/build/linux/build-linux-appimage-zh.sh new file mode 100755 index 00000000000..35586801a28 --- /dev/null +++ b/scripts/build/linux/build-linux-appimage-zh.sh @@ -0,0 +1,370 @@ +#!/usr/bin/env bash +# GeneralsX @build GitHubCopilot 09/04/2026 Build a portable AppImage package for GeneralsXZH on Linux. +# Usage: +# ./scripts/build/linux/build-linux-appimage-zh.sh [preset] +set -euo pipefail + +PRESET="${1:-linux64-deploy}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" +BUILD_DIR="${PROJECT_ROOT}/build/${PRESET}" +APPIMAGE_ROOT="${PROJECT_ROOT}/build/appimage" +APPDIR="${APPIMAGE_ROOT}/GeneralsXZH.AppDir" +OUTPUT_APPIMAGE="${PROJECT_ROOT}/build/GeneralsXZH-${PRESET}-x86_64.AppImage" +# GeneralsX @build GitHubCopilot 10/04/2026 Pin appimagetool to immutable upstream release and enforce checksum validation. +APPIMAGETOOL_VERSION="${APPIMAGETOOL_VERSION:-1.9.1}" +APPIMAGETOOL_URL="${APPIMAGETOOL_URL:-https://github.com/AppImage/appimagetool/releases/download/${APPIMAGETOOL_VERSION}/appimagetool-x86_64.AppImage}" +APPIMAGETOOL_SHA256="${APPIMAGETOOL_SHA256:-ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0}" + +DXVK_LIB_DIR="${BUILD_DIR}/_deps/dxvk-src/lib" +SDL3_LIB_DIR="${BUILD_DIR}/_deps/sdl3-build" +SDL3_IMAGE_LIB_DIR="${BUILD_DIR}/_deps/sdl3_image-build" +OPENAL_LIB_DIR="${BUILD_DIR}/_deps/openal_soft-build" +FFMPEG_LIB_DIR="/usr/lib/x86_64-linux-gnu" +FFMPEG_DEP_LIB_DIR="/lib/x86_64-linux-gnu" +BINARY_SRC="${BUILD_DIR}/GeneralsMD/GeneralsXZH" +GAMESPY_LIB="${BUILD_DIR}/libgamespy.so" +DXVK_CONF_SRC="${PROJECT_ROOT}/resources/dxvk/dxvk.conf" +ICON_SRC_ZH="${PROJECT_ROOT}/assets/generalsx-zh_icon.png" +ICON_SRC_FALLBACK="${PROJECT_ROOT}/assets/generalsx_icon.png" + +copy_optional_libs() { + local source_dir="$1" + local pattern="$2" + if [[ -d "${source_dir}" ]]; then + local matches=() + shopt -s nullglob + matches=("${source_dir}"/${pattern}) + shopt -u nullglob + if (( ${#matches[@]} > 0 )); then + cp -a "${matches[@]}" "${APPDIR}/usr/lib/" + fi + fi +} + +copy_codec_dep() { + local pattern="$1" + copy_optional_libs "${FFMPEG_DEP_LIB_DIR}" "${pattern}" + copy_optional_libs "${FFMPEG_LIB_DIR}" "${pattern}" +} + +copy_ldd_deps() { + local root="$1" + [[ -e "${root}" ]] || return 0 + + while IFS= read -r dep; do + case "${dep}" in + # GeneralsX @bugfix GitHubCopilot 10/04/2026 Exclude glibc loader/runtime files across common Linux layouts to preserve AppImage portability. + linux-vdso.so.1 | \ + /lib64/ld-linux* | /lib/*/ld-linux* | /usr/lib/*/ld-linux* | /usr/lib64/ld-linux* | \ + /lib/*/libc.so.* | /lib64/libc.so.* | /usr/lib/*/libc.so.* | /usr/lib64/libc.so.* | \ + /lib/*/libm.so.* | /lib64/libm.so.* | /usr/lib/*/libm.so.* | /usr/lib64/libm.so.* | \ + /lib/*/libpthread.so.* | /lib64/libpthread.so.* | /usr/lib/*/libpthread.so.* | /usr/lib64/libpthread.so.* | \ + /lib/*/librt.so.* | /lib64/librt.so.* | /usr/lib/*/librt.so.* | /usr/lib64/librt.so.* | \ + /lib/*/libdl.so.* | /lib64/libdl.so.* | /usr/lib/*/libdl.so.* | /usr/lib64/libdl.so.*) + continue + ;; + esac + + cp -a "${dep}" "${APPDIR}/usr/lib/" 2>/dev/null || true + if [[ -L "${dep}" ]]; then + local resolved + resolved="$(readlink -f "${dep}")" + cp -a "${resolved}" "${APPDIR}/usr/lib/" 2>/dev/null || true + fi + done < <(ldd "${root}" | awk '{for (i = 1; i <= NF; ++i) { if ($i ~ /^\//) { print $i; break } }}' | sort -u) +} + +verify_sha256_if_configured() { + local file_path="$1" + + if [[ -z "${APPIMAGETOOL_SHA256}" ]]; then + echo "ERROR: APPIMAGETOOL_SHA256 is required for appimagetool verification." >&2 + exit 1 + fi + + if command -v sha256sum >/dev/null 2>&1; then + echo "${APPIMAGETOOL_SHA256} ${file_path}" | sha256sum -c - + elif command -v shasum >/dev/null 2>&1; then + local actual_sha256 + actual_sha256="$(shasum -a 256 "${file_path}" | awk '{print $1}')" + if [[ "${actual_sha256}" != "${APPIMAGETOOL_SHA256}" ]]; then + echo "ERROR: appimagetool SHA-256 mismatch" >&2 + echo "Expected: ${APPIMAGETOOL_SHA256}" >&2 + echo "Actual: ${actual_sha256}" >&2 + exit 1 + fi + else + echo "ERROR: Neither sha256sum nor shasum is available for checksum verification." >&2 + exit 1 + fi +} + +validate_appimagetool_source() { + case "${APPIMAGETOOL_URL}" in + https://github.com/AppImage/appimagetool/releases/download/*/appimagetool-x86_64.AppImage) + ;; + *) + echo "ERROR: APPIMAGETOOL_URL must target a pinned AppImage/appimagetool release asset." >&2 + exit 1 + ;; + esac + + if [[ "${APPIMAGETOOL_URL}" == *"/releases/download/continuous/"* ]]; then + echo "ERROR: APPIMAGETOOL_URL must not use floating continuous channel." >&2 + exit 1 + fi + + if [[ ! "${APPIMAGETOOL_SHA256}" =~ ^[A-Fa-f0-9]{64}$ ]]; then + echo "ERROR: APPIMAGETOOL_SHA256 must be a 64-character hexadecimal SHA-256 digest." >&2 + exit 1 + fi +} + +validate_appimagetool_source + +if [[ ! -f "${BINARY_SRC}" || ! -s "${BINARY_SRC}" ]]; then + echo "ERROR: Missing or empty binary: ${BINARY_SRC}" >&2 + echo "Build first: ./scripts/build/linux/docker-build-linux-zh.sh ${PRESET}" >&2 + exit 1 +fi +if [[ ! -d "${DXVK_LIB_DIR}" ]]; then + echo "ERROR: Missing DXVK libs dir: ${DXVK_LIB_DIR}" >&2 + exit 1 +fi +if [[ ! -d "${SDL3_LIB_DIR}" || ! -d "${SDL3_IMAGE_LIB_DIR}" ]]; then + echo "ERROR: Missing SDL3/SDL3_image build dirs under ${BUILD_DIR}" >&2 + exit 1 +fi +if [[ ! -f "${GAMESPY_LIB}" ]]; then + echo "ERROR: Missing GameSpy lib: ${GAMESPY_LIB}" >&2 + exit 1 +fi + +rm -rf "${APPDIR}" +mkdir -p "${APPDIR}/usr/bin" "${APPDIR}/usr/lib" "${APPDIR}/usr/share/applications" "${APPDIR}/usr/share/icons/hicolor/512x512/apps" + +cp "${BINARY_SRC}" "${APPDIR}/usr/bin/GeneralsXZH" +chmod +x "${APPDIR}/usr/bin/GeneralsXZH" +cp "${GAMESPY_LIB}" "${APPDIR}/usr/lib/" +copy_optional_libs "${DXVK_LIB_DIR}" "libdxvk_d3d8.so*" +copy_optional_libs "${DXVK_LIB_DIR}" "libdxvk_d3d9.so*" +copy_optional_libs "${SDL3_LIB_DIR}" "libSDL3.so*" +copy_optional_libs "${SDL3_IMAGE_LIB_DIR}" "libSDL3_image.so*" +copy_optional_libs "${OPENAL_LIB_DIR}" "libopenal.so*" + +# GeneralsX @bugfix GitHubCopilot 10/04/2026 Bundle FFmpeg SONAME-compatible libs to avoid host version mismatch (e.g. Ubuntu 25.10). +copy_codec_dep "libavcodec.so*" +copy_codec_dep "libavformat.so*" +copy_codec_dep "libavutil.so*" +copy_codec_dep "libswresample.so*" +copy_codec_dep "libswscale.so*" + +# Include transitive codec dependencies required by FFmpeg libs. +copy_codec_dep "libzvbi.so*" +copy_codec_dep "libsnappy.so*" +copy_codec_dep "libaom.so*" +copy_codec_dep "libcodec2.so*" +copy_codec_dep "libgsm.so*" +copy_codec_dep "libjxl.so*" +copy_codec_dep "libjxl_threads.so*" +copy_codec_dep "libmp3lame.so*" +copy_codec_dep "libopenjp2.so*" +copy_codec_dep "libopus.so*" +copy_codec_dep "librav1e.so*" +copy_codec_dep "libshine.so*" +copy_codec_dep "libspeex.so*" +copy_codec_dep "libSvtAv1Enc.so*" +copy_codec_dep "libtheoraenc.so*" +copy_codec_dep "libtheoradec.so*" +copy_codec_dep "libtwolame.so*" +copy_codec_dep "libvorbis.so*" +copy_codec_dep "libvorbisenc.so*" +copy_codec_dep "libwebp.so*" +copy_codec_dep "libwebpmux.so*" +copy_codec_dep "libx264.so*" +copy_codec_dep "libx265.so*" +copy_codec_dep "libxvidcore.so*" +copy_codec_dep "libsoxr.so*" +copy_codec_dep "libvpl.so*" +copy_codec_dep "libva.so*" +copy_codec_dep "libva-drm.so*" +copy_codec_dep "libva-x11.so*" +copy_codec_dep "libvdpau.so*" +copy_codec_dep "libOpenCL.so*" + +shopt -s nullglob +for ffmpeg_root in "${APPDIR}"/usr/lib/libavcodec.so* "${APPDIR}"/usr/lib/libavformat.so* "${APPDIR}"/usr/lib/libavutil.so*; do + copy_ldd_deps "${ffmpeg_root}" +done +shopt -u nullglob + +if ! compgen -G "${APPDIR}/usr/lib/libavcodec.so*" > /dev/null; then + echo "ERROR: Missing required AppImage runtime library libavcodec.so*" >&2 + exit 1 +fi +if ! compgen -G "${APPDIR}/usr/lib/libavformat.so*" > /dev/null; then + echo "ERROR: Missing required AppImage runtime library libavformat.so*" >&2 + exit 1 +fi +if ! compgen -G "${APPDIR}/usr/lib/libavutil.so*" > /dev/null; then + echo "ERROR: Missing required AppImage runtime library libavutil.so*" >&2 + exit 1 +fi + +if [[ -f "${DXVK_CONF_SRC}" ]]; then + mkdir -p "${APPDIR}/usr/share/generalsxzh" + cp "${DXVK_CONF_SRC}" "${APPDIR}/usr/share/generalsxzh/dxvk.conf" +fi + +cat > "${APPDIR}/AppRun" << 'EOF' +#!/usr/bin/env bash +# GeneralsX @build GitHubCopilot 09/04/2026 AppImage runtime launcher for GeneralsXZH. +# GeneralsX @bugfix GitHubCopilot 09/04/2026 Honor CNC_GENERALS_PATH and CNC_GENERALS_ZH_PATH with deterministic precedence. +set -euo pipefail + +APPDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +export LD_LIBRARY_PATH="${APPDIR}/usr/lib:${LD_LIBRARY_PATH:-}" +export DXVK_WSI_DRIVER="SDL3" +export DXVK_LOG_LEVEL="${DXVK_LOG_LEVEL:-info}" +export DXVK_HUD="${DXVK_HUD:-0}" + +has_big_files() { + local dir="$1" + [[ -d "${dir}" ]] || return 1 + compgen -G "${dir}/*.big" > /dev/null +} + +with_trailing_slash() { + local path="$1" + if [[ "${path}" == */ ]]; then + printf '%s' "${path}" + else + printf '%s/' "${path}" + fi +} + +APPIMAGE_HOST_DIR="" +if [[ -n "${APPIMAGE:-}" ]]; then + APPIMAGE_HOST_DIR="$(cd "$(dirname "${APPIMAGE}")" && pwd)" +fi +LAUNCH_DIR="$(pwd)" + +# Resolve Zero Hour assets path (CNC_GENERALS_ZH_PATH) +if [[ -n "${CNC_GENERALS_ZH_PATH:-}" ]]; then + if [[ ! -d "${CNC_GENERALS_ZH_PATH}" ]]; then + echo "WARNING: CNC_GENERALS_ZH_PATH='${CNC_GENERALS_ZH_PATH}' does not exist; falling back to auto-detection" + unset CNC_GENERALS_ZH_PATH + else + export CNC_GENERALS_ZH_PATH="$(with_trailing_slash "${CNC_GENERALS_ZH_PATH}")" + fi +fi + +if [[ -z "${CNC_GENERALS_ZH_PATH:-}" && -n "${APPIMAGE_HOST_DIR}" ]] && has_big_files "${APPIMAGE_HOST_DIR}"; then + export CNC_GENERALS_ZH_PATH="$(with_trailing_slash "${APPIMAGE_HOST_DIR}")" +fi +if [[ -z "${CNC_GENERALS_ZH_PATH:-}" ]] && has_big_files "${LAUNCH_DIR}"; then + export CNC_GENERALS_ZH_PATH="$(with_trailing_slash "${LAUNCH_DIR}")" +fi +if [[ -z "${CNC_GENERALS_ZH_PATH:-}" ]] && has_big_files "${HOME}/GeneralsX/GeneralsZH"; then + export CNC_GENERALS_ZH_PATH="$(with_trailing_slash "${HOME}/GeneralsX/GeneralsZH")" +fi +if [[ -z "${CNC_GENERALS_ZH_PATH:-}" ]] && has_big_files "${HOME}/GeneralsX/GeneralsMD"; then + export CNC_GENERALS_ZH_PATH="$(with_trailing_slash "${HOME}/GeneralsX/GeneralsMD")" +fi + +# Resolve base Generals path (CNC_GENERALS_PATH / CNC_GENERALS_INSTALLPATH) +if [[ -n "${CNC_GENERALS_PATH:-}" ]]; then + if [[ ! -d "${CNC_GENERALS_PATH}" ]]; then + echo "WARNING: CNC_GENERALS_PATH='${CNC_GENERALS_PATH}' does not exist; falling back to auto-detection" + unset CNC_GENERALS_PATH + else + export CNC_GENERALS_PATH="$(with_trailing_slash "${CNC_GENERALS_PATH}")" + fi +fi + +if [[ -z "${CNC_GENERALS_PATH:-}" && -n "${CNC_GENERALS_INSTALLPATH:-}" && -d "${CNC_GENERALS_INSTALLPATH}" ]]; then + export CNC_GENERALS_PATH="$(with_trailing_slash "${CNC_GENERALS_INSTALLPATH}")" +fi +if [[ -z "${CNC_GENERALS_PATH:-}" && -n "${CNC_GENERALS_ZH_PATH:-}" ]]; then + _zh_parent="$(cd "${CNC_GENERALS_ZH_PATH}/.." && pwd)" + if [[ -d "${_zh_parent}/Generals" ]]; then + export CNC_GENERALS_PATH="$(with_trailing_slash "${_zh_parent}/Generals")" + fi +fi +if [[ -z "${CNC_GENERALS_PATH:-}" && -d "${HOME}/GeneralsX/Generals" ]]; then + export CNC_GENERALS_PATH="$(with_trailing_slash "${HOME}/GeneralsX/Generals")" +fi + +if [[ -n "${CNC_GENERALS_PATH:-}" && -z "${CNC_GENERALS_INSTALLPATH:-}" ]]; then + export CNC_GENERALS_INSTALLPATH="$(with_trailing_slash "${CNC_GENERALS_PATH}")" +fi + +if [[ -n "${CNC_GENERALS_ZH_PATH:-}" ]]; then + echo "INFO: AppImage assets path (ZH): ${CNC_GENERALS_ZH_PATH}" + cd "${CNC_GENERALS_ZH_PATH}" +fi +if [[ -n "${CNC_GENERALS_PATH:-}" ]]; then + echo "INFO: AppImage base Generals path: ${CNC_GENERALS_PATH}" +fi + +if [[ -z "${ALSOFT_DISABLE_CPU_EXTS:-}" ]]; then + export ALSOFT_DISABLE_CPU_EXTS="all" +fi +if [[ -z "${ALSOFT_DRIVERS:-}" ]]; then + export ALSOFT_DRIVERS="pulse,alsa,oss,jack,null,wave" +fi + +exec "${APPDIR}/usr/bin/GeneralsXZH" "$@" +EOF +chmod +x "${APPDIR}/AppRun" + +cat > "${APPDIR}/GeneralsXZH.desktop" << 'EOF' +[Desktop Entry] +Type=Application +Name=Command & Conquer Generals Zero Hour (GeneralsXZH) +Comment=Cross-platform Generals Zero Hour runtime +Exec=GeneralsXZH +Icon=GeneralsXZH +Categories=Game;StrategyGame; +Terminal=false +EOF +cp "${APPDIR}/GeneralsXZH.desktop" "${APPDIR}/usr/share/applications/GeneralsXZH.desktop" + +ICON_SRC="" +if [[ -f "${ICON_SRC_ZH}" ]]; then + ICON_SRC="${ICON_SRC_ZH}" +elif [[ -f "${ICON_SRC_FALLBACK}" ]]; then + ICON_SRC="${ICON_SRC_FALLBACK}" +fi + +if [[ -z "${ICON_SRC}" ]]; then + echo "ERROR: Missing icon assets. Expected one of:" >&2 + echo " ${ICON_SRC_ZH}" >&2 + echo " ${ICON_SRC_FALLBACK}" >&2 + exit 1 +fi + +cp "${ICON_SRC}" "${APPDIR}/GeneralsXZH.png" +cp "${ICON_SRC}" "${APPDIR}/usr/share/icons/hicolor/512x512/apps/GeneralsXZH.png" + +if command -v appimagetool >/dev/null 2>&1 && [[ -z "${CI:-}" ]]; then + APPIMAGETOOL_BIN="$(command -v appimagetool)" +else + # GeneralsX @build GitHubCopilot 10/04/2026 Use pinned appimagetool artifact with mandatory SHA-256 verification for reproducible packaging. + APPIMAGETOOL_BIN="${APPIMAGE_ROOT}/appimagetool.AppImage" + mkdir -p "${APPIMAGE_ROOT}" + if [[ ! -f "${APPIMAGETOOL_BIN}" ]]; then + echo "Downloading appimagetool..." + curl -fL --retry 3 --output "${APPIMAGETOOL_BIN}" "${APPIMAGETOOL_URL}" + fi + verify_sha256_if_configured "${APPIMAGETOOL_BIN}" + chmod +x "${APPIMAGETOOL_BIN}" +fi + +ARCH=x86_64 "${APPIMAGETOOL_BIN}" "${APPDIR}" "${OUTPUT_APPIMAGE}" + +echo "AppImage generated: ${OUTPUT_APPIMAGE}" +echo "Run example:" +echo " chmod +x ${OUTPUT_APPIMAGE}" +echo " ${OUTPUT_APPIMAGE} -win"