From 5179b089bd116bdb3707504715ed12f3ba372fa5 Mon Sep 17 00:00:00 2001 From: Felipe Braz Date: Thu, 9 Apr 2026 23:08:48 -0300 Subject: [PATCH 1/9] build(packaging): add appimage poc for zh --- docs/DEV_BLOG/2026-04-DIARY.md | 24 +++ docs/WORKDIR/lessons/2026-04-LESSONS.md | 7 + .../support/APPIMAGE_POC_PLAN_2026-04.md | 47 ++++++ scripts/README.md | 4 + .../build/linux/build-linux-appimage-zh.sh | 139 ++++++++++++++++++ 5 files changed, 221 insertions(+) create mode 100644 docs/WORKDIR/support/APPIMAGE_POC_PLAN_2026-04.md create mode 100755 scripts/build/linux/build-linux-appimage-zh.sh diff --git a/docs/DEV_BLOG/2026-04-DIARY.md b/docs/DEV_BLOG/2026-04-DIARY.md index 6be6f818c31..c3b3f9a4f84 100644 --- a/docs/DEV_BLOG/2026-04-DIARY.md +++ b/docs/DEV_BLOG/2026-04-DIARY.md @@ -2,6 +2,30 @@ --- +## 2026-04-09 (SESSION CURRENT): AppImage PoC implemented and validated (short-term Linux packaging path) + +Given repeated Flatpak runtime friction around Vulkan/XCB ABI combinations, 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 past previous Flatpak blocker: + - 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 with less runtime coupling friction than Flatpak 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..3de4fd99641 --- /dev/null +++ b/docs/WORKDIR/support/APPIMAGE_POC_PLAN_2026-04.md @@ -0,0 +1,47 @@ +# 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 + +- Target: Zero Hour runtime (`GeneralsXZH`) +- Output: single portable file under `build/` +- Tooling: `scripts/build/linux/build-linux-appimage-zh.sh` + +## Included runtime artifacts + +- Game binary (`GeneralsXZH`) +- DXVK userspace libs (`libdxvk_d3d8.so*`, optional d3d9) +- SDL3 + SDL3_image +- OpenAL +- GameSpy +- Optional `dxvk.conf` + +## Launcher behavior + +- Uses bundled libs via `LD_LIBRARY_PATH` +- Exposes DXVK defaults (`DXVK_WSI_DRIVER=SDL3`, logging/HUD knobs) +- Auto-detects base Generals path from common directories +- Keeps OpenAL workaround env for known alignment/backend issues + +## Build command + +Example: + +- `./scripts/build/linux/build-linux-appimage-zh.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) +- FFmpeg/video libraries are not yet bundled in this initial PoC diff --git a/scripts/README.md b/scripts/README.md index fa7e1aafa23..133406f5273 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -12,6 +12,7 @@ Scripts for Linux native and Docker-based builds: - `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 +105,9 @@ 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 ``` ### macOS Build 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..e4ec4fefaa6 --- /dev/null +++ b/scripts/build/linux/build-linux-appimage-zh.sh @@ -0,0 +1,139 @@ +#!/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" + +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" +BINARY_SRC="${BUILD_DIR}/GeneralsMD/GeneralsXZH" +GAMESPY_LIB="${BUILD_DIR}/libgamespy.so" +DXVK_CONF_SRC="${PROJECT_ROOT}/resources/dxvk/dxvk.conf" +ICON_SRC="${PROJECT_ROOT}/flatpak/generalsx-zh_icon_512.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 +} + +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*" + +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. +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}" + +if [[ -z "${CNC_GENERALS_INSTALLPATH:-}" && -d "${HOME}/GeneralsX/Generals" ]]; then + export CNC_GENERALS_INSTALLPATH="${HOME}/GeneralsX/Generals/" +fi +if [[ -z "${CNC_GENERALS_INSTALLPATH:-}" && -d "${HOME}/GeneralsX/GeneralsZH" ]]; then + export CNC_GENERALS_INSTALLPATH="${HOME}/GeneralsX/GeneralsZH/" +fi +if [[ -z "${CNC_GENERALS_INSTALLPATH:-}" && -d "${HOME}/GeneralsX/GeneralsMD" ]]; then + export CNC_GENERALS_INSTALLPATH="${HOME}/GeneralsX/GeneralsMD/" +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" + +if [[ -f "${ICON_SRC}" ]]; then + cp "${ICON_SRC}" "${APPDIR}/GeneralsXZH.png" + cp "${ICON_SRC}" "${APPDIR}/usr/share/icons/hicolor/512x512/apps/GeneralsXZH.png" +fi + +if command -v appimagetool >/dev/null 2>&1; then + APPIMAGETOOL_BIN="$(command -v appimagetool)" +else + APPIMAGETOOL_BIN="${APPIMAGE_ROOT}/appimagetool.AppImage" + mkdir -p "${APPIMAGE_ROOT}" + if [[ ! -x "${APPIMAGETOOL_BIN}" ]]; then + echo "Downloading appimagetool..." + curl -L -o "${APPIMAGETOOL_BIN}" "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" + chmod +x "${APPIMAGETOOL_BIN}" + fi +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" From 193af71e4285976443e7c8c52ed0720fd17e02f2 Mon Sep 17 00:00:00 2001 From: Felipe Braz Date: Thu, 9 Apr 2026 23:38:28 -0300 Subject: [PATCH 2/9] fix(appimage): honor cnc asset path env overrides --- docs/DEV_BLOG/2026-04-DIARY.md | 16 ++++ .../support/APPIMAGE_POC_PLAN_2026-04.md | 13 ++- scripts/README.md | 5 ++ .../build/linux/build-linux-appimage-zh.sh | 82 +++++++++++++++++-- 4 files changed, 109 insertions(+), 7 deletions(-) diff --git a/docs/DEV_BLOG/2026-04-DIARY.md b/docs/DEV_BLOG/2026-04-DIARY.md index c3b3f9a4f84..24c55aa48c5 100644 --- a/docs/DEV_BLOG/2026-04-DIARY.md +++ b/docs/DEV_BLOG/2026-04-DIARY.md @@ -2,6 +2,22 @@ --- +## 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 PoC implemented and validated (short-term Linux packaging path) Given repeated Flatpak runtime friction around Vulkan/XCB ABI combinations, implemented a practical AppImage packaging path for Zero Hour as a short-term distribution strategy. diff --git a/docs/WORKDIR/support/APPIMAGE_POC_PLAN_2026-04.md b/docs/WORKDIR/support/APPIMAGE_POC_PLAN_2026-04.md index 3de4fd99641..fb0721e1171 100644 --- a/docs/WORKDIR/support/APPIMAGE_POC_PLAN_2026-04.md +++ b/docs/WORKDIR/support/APPIMAGE_POC_PLAN_2026-04.md @@ -24,9 +24,20 @@ A short-term AppImage path reduces distro ABI friction while keeping distributio - Uses bundled libs via `LD_LIBRARY_PATH` - Exposes DXVK defaults (`DXVK_WSI_DRIVER=SDL3`, logging/HUD knobs) -- Auto-detects base Generals path from common directories +- 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 +``` + ## Build command Example: diff --git a/scripts/README.md b/scripts/README.md index 133406f5273..507c70a98e4 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -108,6 +108,11 @@ brew install --cask docker # 7. Optional: build AppImage package ./scripts/build/linux/build-linux-appimage-zh.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-zh.sh b/scripts/build/linux/build-linux-appimage-zh.sh index e4ec4fefaa6..2aa698f7c3c 100755 --- a/scripts/build/linux/build-linux-appimage-zh.sh +++ b/scripts/build/linux/build-linux-appimage-zh.sh @@ -73,6 +73,7 @@ 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)" @@ -81,14 +82,83 @@ export DXVK_WSI_DRIVER="SDL3" export DXVK_LOG_LEVEL="${DXVK_LOG_LEVEL:-info}" export DXVK_HUD="${DXVK_HUD:-0}" -if [[ -z "${CNC_GENERALS_INSTALLPATH:-}" && -d "${HOME}/GeneralsX/Generals" ]]; then - export CNC_GENERALS_INSTALLPATH="${HOME}/GeneralsX/Generals/" +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 -if [[ -z "${CNC_GENERALS_INSTALLPATH:-}" && -d "${HOME}/GeneralsX/GeneralsZH" ]]; then - export CNC_GENERALS_INSTALLPATH="${HOME}/GeneralsX/GeneralsZH/" + +# 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 [[ -z "${CNC_GENERALS_INSTALLPATH:-}" && -d "${HOME}/GeneralsX/GeneralsMD" ]]; then - export CNC_GENERALS_INSTALLPATH="${HOME}/GeneralsX/GeneralsMD/" +if [[ -n "${CNC_GENERALS_PATH:-}" ]]; then + echo "INFO: AppImage base Generals path: ${CNC_GENERALS_PATH}" fi if [[ -z "${ALSOFT_DISABLE_CPU_EXTS:-}" ]]; then From ac3ef05765237b007b45af20dd67fa8458cd603d Mon Sep 17 00:00:00 2001 From: Felipe Braz Date: Thu, 9 Apr 2026 23:49:33 -0300 Subject: [PATCH 3/9] feat(appimage): add generals package and vscode tasks --- .vscode/tasks.json | 30 +++ docs/DEV_BLOG/2026-04-DIARY.md | 18 +- .../support/APPIMAGE_POC_PLAN_2026-04.md | 20 +- scripts/README.md | 4 + .../linux/build-linux-appimage-generals.sh | 176 ++++++++++++++++++ 5 files changed, 241 insertions(+), 7 deletions(-) create mode 100755 scripts/build/linux/build-linux-appimage-generals.sh 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/DEV_BLOG/2026-04-DIARY.md b/docs/DEV_BLOG/2026-04-DIARY.md index 24c55aa48c5..6c7faf6d491 100644 --- a/docs/DEV_BLOG/2026-04-DIARY.md +++ b/docs/DEV_BLOG/2026-04-DIARY.md @@ -18,9 +18,21 @@ Improved AppImage `AppRun` to resolve assets deterministically and honor user-pr - 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 Flatpak runtime friction around Vulkan/XCB ABI combinations, implemented a practical AppImage packaging path for Zero Hour as a short-term distribution strategy. +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` @@ -35,13 +47,13 @@ Given repeated Flatpak runtime friction around Vulkan/XCB ABI combinations, impl **Validation result:** - AppImage generated successfully: - `build/GeneralsXZH-linux64-deploy-x86_64.AppImage` -- Smoke launch succeeded past previous Flatpak blocker: +- 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 with less runtime coupling friction than Flatpak for this project state. +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/support/APPIMAGE_POC_PLAN_2026-04.md b/docs/WORKDIR/support/APPIMAGE_POC_PLAN_2026-04.md index fb0721e1171..8f054c21884 100644 --- a/docs/WORKDIR/support/APPIMAGE_POC_PLAN_2026-04.md +++ b/docs/WORKDIR/support/APPIMAGE_POC_PLAN_2026-04.md @@ -7,13 +7,17 @@ A short-term AppImage path reduces distro ABI friction while keeping distributio ## PoC Scope -- Target: Zero Hour runtime (`GeneralsXZH`) -- Output: single portable file under `build/` -- Tooling: `scripts/build/linux/build-linux-appimage-zh.sh` +- 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`) +- Game binary (`GeneralsXZH` or `GeneralsX`) - DXVK userspace libs (`libdxvk_d3d8.so*`, optional d3d9) - SDL3 + SDL3_image - OpenAL @@ -38,11 +42,19 @@ 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 diff --git a/scripts/README.md b/scripts/README.md index 507c70a98e4..8ef6c0142f1 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -8,6 +8,7 @@ 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 @@ -109,6 +110,9 @@ brew install --cask docker # 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" \ 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..7ee7322ef4b --- /dev/null +++ b/scripts/build/linux/build-linux-appimage-generals.sh @@ -0,0 +1,176 @@ +#!/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" + +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" +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 +} + +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*" + +if [[ -f "${DXVK_CONF_SRC}" ]]; then + mkdir -p "${APPDIR}/usr/share/generalsx" + cp "${DXVK_CONF_SRC}" "${APPDIR}/usr/share/generalsx/dxvk.conf" +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 +} + +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}" && -d "${APPIMAGE_HOST_DIR}" ]]; then + export CNC_GENERALS_PATH="$(with_trailing_slash "${APPIMAGE_HOST_DIR}")" +fi +if [[ -z "${CNC_GENERALS_PATH:-}" && -d "${LAUNCH_DIR}" ]]; then + export CNC_GENERALS_PATH="$(with_trailing_slash "${LAUNCH_DIR}")" +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_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" + +if [[ -f "${ICON_SRC}" ]]; then + cp "${ICON_SRC}" "${APPDIR}/GeneralsX.png" + cp "${ICON_SRC}" "${APPDIR}/usr/share/icons/hicolor/512x512/apps/GeneralsX.png" +fi + +if command -v appimagetool >/dev/null 2>&1; then + APPIMAGETOOL_BIN="$(command -v appimagetool)" +else + APPIMAGETOOL_BIN="${APPIMAGE_ROOT}/appimagetool.AppImage" + mkdir -p "${APPIMAGE_ROOT}" + if [[ ! -x "${APPIMAGETOOL_BIN}" ]]; then + echo "Downloading appimagetool..." + curl -L -o "${APPIMAGETOOL_BIN}" "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" + chmod +x "${APPIMAGETOOL_BIN}" + fi +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 From d4916ed3d3a6890fe1f0e5c27c4ed5c97caef150 Mon Sep 17 00:00:00 2001 From: Felipe Braz Date: Fri, 10 Apr 2026 00:22:54 -0300 Subject: [PATCH 4/9] fix(appimage): resolve icon source in build tasks --- docs/DEV_BLOG/2026-04-DIARY.md | 19 +++++++++++++++++ .../linux/build-linux-appimage-generals.sh | 11 ++++++---- .../build/linux/build-linux-appimage-zh.sh | 21 +++++++++++++++---- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/docs/DEV_BLOG/2026-04-DIARY.md b/docs/DEV_BLOG/2026-04-DIARY.md index 6c7faf6d491..c5c26b883a3 100644 --- a/docs/DEV_BLOG/2026-04-DIARY.md +++ b/docs/DEV_BLOG/2026-04-DIARY.md @@ -2,6 +2,25 @@ --- +## 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. diff --git a/scripts/build/linux/build-linux-appimage-generals.sh b/scripts/build/linux/build-linux-appimage-generals.sh index 7ee7322ef4b..661199acf7a 100755 --- a/scripts/build/linux/build-linux-appimage-generals.sh +++ b/scripts/build/linux/build-linux-appimage-generals.sh @@ -70,6 +70,11 @@ if [[ -f "${DXVK_CONF_SRC}" ]]; then 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. @@ -151,10 +156,8 @@ Terminal=false EOF cp "${APPDIR}/GeneralsX.desktop" "${APPDIR}/usr/share/applications/GeneralsX.desktop" -if [[ -f "${ICON_SRC}" ]]; then - cp "${ICON_SRC}" "${APPDIR}/GeneralsX.png" - cp "${ICON_SRC}" "${APPDIR}/usr/share/icons/hicolor/512x512/apps/GeneralsX.png" -fi +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; then APPIMAGETOOL_BIN="$(command -v appimagetool)" diff --git a/scripts/build/linux/build-linux-appimage-zh.sh b/scripts/build/linux/build-linux-appimage-zh.sh index 2aa698f7c3c..2f3373f2b5c 100755 --- a/scripts/build/linux/build-linux-appimage-zh.sh +++ b/scripts/build/linux/build-linux-appimage-zh.sh @@ -19,7 +19,8 @@ OPENAL_LIB_DIR="${BUILD_DIR}/_deps/openal_soft-build" BINARY_SRC="${BUILD_DIR}/GeneralsMD/GeneralsXZH" GAMESPY_LIB="${BUILD_DIR}/libgamespy.so" DXVK_CONF_SRC="${PROJECT_ROOT}/resources/dxvk/dxvk.conf" -ICON_SRC="${PROJECT_ROOT}/flatpak/generalsx-zh_icon_512.png" +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" @@ -184,11 +185,23 @@ Terminal=false EOF cp "${APPDIR}/GeneralsXZH.desktop" "${APPDIR}/usr/share/applications/GeneralsXZH.desktop" -if [[ -f "${ICON_SRC}" ]]; then - cp "${ICON_SRC}" "${APPDIR}/GeneralsXZH.png" - cp "${ICON_SRC}" "${APPDIR}/usr/share/icons/hicolor/512x512/apps/GeneralsXZH.png" +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; then APPIMAGETOOL_BIN="$(command -v appimagetool)" else From b388d5e08aeb280db4aeef830447588645a19952 Mon Sep 17 00:00:00 2001 From: Felipe Braz Date: Fri, 10 Apr 2026 00:31:34 -0300 Subject: [PATCH 5/9] fix(appimage): bundle ffmpeg runtime sonames --- docs/DEV_BLOG/2026-04-DIARY.md | 27 ++++++ .../support/APPIMAGE_POC_PLAN_2026-04.md | 3 +- .../linux/build-linux-appimage-generals.sh | 87 +++++++++++++++++++ .../build/linux/build-linux-appimage-zh.sh | 87 +++++++++++++++++++ 4 files changed, 203 insertions(+), 1 deletion(-) diff --git a/docs/DEV_BLOG/2026-04-DIARY.md b/docs/DEV_BLOG/2026-04-DIARY.md index c5c26b883a3..aa63af78ae6 100644 --- a/docs/DEV_BLOG/2026-04-DIARY.md +++ b/docs/DEV_BLOG/2026-04-DIARY.md @@ -2,6 +2,33 @@ --- +## 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. diff --git a/docs/WORKDIR/support/APPIMAGE_POC_PLAN_2026-04.md b/docs/WORKDIR/support/APPIMAGE_POC_PLAN_2026-04.md index 8f054c21884..34ded96dd03 100644 --- a/docs/WORKDIR/support/APPIMAGE_POC_PLAN_2026-04.md +++ b/docs/WORKDIR/support/APPIMAGE_POC_PLAN_2026-04.md @@ -22,6 +22,7 @@ A short-term AppImage path reduces distro ABI friction while keeping distributio - 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 @@ -67,4 +68,4 @@ Example: - Host driver stack (Vulkan ICD) still remains a runtime dependency - Some environments may require additional policy tweaks (for example FUSE or sandbox constraints) -- FFmpeg/video libraries are not yet bundled in this initial PoC +- Build baseline still matters for broad compatibility (prefer building AppImage on an older supported distro) diff --git a/scripts/build/linux/build-linux-appimage-generals.sh b/scripts/build/linux/build-linux-appimage-generals.sh index 661199acf7a..d5b1198f8b5 100755 --- a/scripts/build/linux/build-linux-appimage-generals.sh +++ b/scripts/build/linux/build-linux-appimage-generals.sh @@ -16,6 +16,8 @@ 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" @@ -35,6 +37,32 @@ copy_optional_libs() { 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 + /lib64/ld-linux* | /lib/*/ld-linux* | /lib/*/libc.so.* | /lib/*/libm.so.* | /lib/*/libpthread.so.* | /lib/*/librt.so.* | /lib/*/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) +} + 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 @@ -65,6 +93,65 @@ 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" diff --git a/scripts/build/linux/build-linux-appimage-zh.sh b/scripts/build/linux/build-linux-appimage-zh.sh index 2f3373f2b5c..608bbf89e6a 100755 --- a/scripts/build/linux/build-linux-appimage-zh.sh +++ b/scripts/build/linux/build-linux-appimage-zh.sh @@ -16,6 +16,8 @@ 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" @@ -36,6 +38,32 @@ copy_optional_libs() { 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 + /lib64/ld-linux* | /lib/*/ld-linux* | /lib/*/libc.so.* | /lib/*/libm.so.* | /lib/*/libpthread.so.* | /lib/*/librt.so.* | /lib/*/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) +} + 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 @@ -66,6 +94,65 @@ 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" From 844772aea79a1b0337974347ebae9c04004889a1 Mon Sep 17 00:00:00 2001 From: Felipe Braz Date: Fri, 10 Apr 2026 00:46:37 -0300 Subject: [PATCH 6/9] build(ci): use appimage as default linux artifact format - build-linux.yml: add package_format input (appimage|gzip, default appimage) Replace monolithic deploy bundle step with two conditional steps: Package as AppImage (APPIMAGE_EXTRACT_AND_RUN=1 for CI) and Package as gzip bundle (legacy opt-in). Compute artifact path via Set artifact path step so Upload Deployment Bundle stays format-agnostic. - release.yml: pass package_format: appimage to both Linux build jobs. Normalize step now detects .AppImage first (direct copy) and falls back to legacy tar+zip path for older artifacts. Summary updated to new asset names. - INSTALL_INSTRUCTIONS.md: rewrite Linux install section for AppImage workflow, including chmod +x one-liner, default path auto-detection, CNC_GENERALS_ZH_PATH override, and a note that all runtime libs are bundled. --- .github/workflows/build-linux.yml | 76 ++++++++++++++++++++++-------- .github/workflows/release.yml | 65 ++++++++++++++----------- docs/BUILD/INSTALL_INSTRUCTIONS.md | 26 ++++++++-- 3 files changed, 115 insertions(+), 52 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 5212fba703e..ab40c0bbd83 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: @@ -176,68 +188,92 @@ 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() && inputs.package_format != 'gzip' + 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() && inputs.package_format == '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 path + id: artifact + if: success() + run: | + PRESET="${{ inputs.preset }}" + FORMAT="${{ inputs.package_format }}" + if [ "${FORMAT}" = "gzip" ]; then + echo "path=/tmp/GeneralsX-${{ inputs.game }}-${PRESET}.tar" >> "$GITHUB_OUTPUT" + else + if [ "${{ inputs.game }}" = "GeneralsMD" ]; then + echo "path=build/GeneralsXZH-${PRESET}-x86_64.AppImage" >> "$GITHUB_OUTPUT" + else + echo "path=build/GeneralsX-${PRESET}-x86_64.AppImage" >> "$GITHUB_OUTPUT" + fi + fi - name: Upload Build Logs if: always() @@ -252,6 +288,6 @@ jobs: 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 + 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..aa569deddd7 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 @@ -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/docs/BUILD/INSTALL_INSTRUCTIONS.md b/docs/BUILD/INSTALL_INSTRUCTIONS.md index 9e91a07e337..aeeb8ad72eb 100644 --- a/docs/BUILD/INSTALL_INSTRUCTIONS.md +++ b/docs/BUILD/INSTALL_INSTRUCTIONS.md @@ -13,11 +13,29 @@ 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 + ``` + +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 From 4bea0012f618383f591d99d655c2d6e5b7a9b510 Mon Sep 17 00:00:00 2001 From: Felipe Braz Date: Fri, 10 Apr 2026 16:13:30 -0300 Subject: [PATCH 7/9] fix(appimage): address review comments on packaging flow --- .github/workflows/build-linux.yml | 20 ++++++- docs/BUILD/INSTALL_INSTRUCTIONS.md | 13 +++++ .../linux/build-linux-appimage-generals.sh | 56 ++++++++++++++++--- .../build/linux/build-linux-appimage-zh.sh | 44 +++++++++++++-- 4 files changed, 121 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index ab40c0bbd83..967ce84c611 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -46,7 +46,25 @@ on: default: appimage jobs: + validate-package-format: + runs-on: ubuntu-latest + steps: + - name: Validate package format input + shell: bash + run: | + # GeneralsX @bugfix GitHubCopilot 10/04/2026 Reject unexpected package_format values to avoid broken artifact paths. + case "${{ inputs.package_format }}" in + appimage|gzip) + echo "package_format is valid: ${{ inputs.package_format }}" + ;; + *) + echo "ERROR: Invalid package_format '${{ inputs.package_format }}'. Expected 'appimage' or 'gzip'." >&2 + exit 1 + ;; + esac + build: + needs: [validate-package-format] name: ${{ inputs.game }}@${{ inputs.preset }} runs-on: ubuntu-latest timeout-minutes: 120 @@ -190,7 +208,7 @@ jobs: # 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() && inputs.package_format != 'gzip' + if: success() && inputs.package_format == 'appimage' env: APPIMAGE_EXTRACT_AND_RUN: "1" run: | diff --git a/docs/BUILD/INSTALL_INSTRUCTIONS.md b/docs/BUILD/INSTALL_INSTRUCTIONS.md index aeeb8ad72eb..cd1a290a12d 100644 --- a/docs/BUILD/INSTALL_INSTRUCTIONS.md +++ b/docs/BUILD/INSTALL_INSTRUCTIONS.md @@ -21,6 +21,19 @@ Legacy fallback during migration is still supported: ./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) diff --git a/scripts/build/linux/build-linux-appimage-generals.sh b/scripts/build/linux/build-linux-appimage-generals.sh index d5b1198f8b5..5c5a55fa166 100755 --- a/scripts/build/linux/build-linux-appimage-generals.sh +++ b/scripts/build/linux/build-linux-appimage-generals.sh @@ -11,6 +11,8 @@ 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" +APPIMAGETOOL_URL="${APPIMAGETOOL_URL:-https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage}" +APPIMAGETOOL_SHA256="${APPIMAGETOOL_SHA256:-}" DXVK_LIB_DIR="${BUILD_DIR}/_deps/dxvk-src/lib" SDL3_LIB_DIR="${BUILD_DIR}/_deps/sdl3-build" @@ -49,7 +51,14 @@ copy_ldd_deps() { while IFS= read -r dep; do case "${dep}" in - /lib64/ld-linux* | /lib/*/ld-linux* | /lib/*/libc.so.* | /lib/*/libm.so.* | /lib/*/libpthread.so.* | /lib/*/librt.so.* | /lib/*/libdl.so.*) + # 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 @@ -63,6 +72,31 @@ copy_ldd_deps() { 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 "WARNING: APPIMAGETOOL_SHA256 is not set; skipping appimagetool checksum verification." + return 0 + 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 +} + 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 @@ -183,6 +217,12 @@ with_trailing_slash() { 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)" @@ -201,13 +241,13 @@ 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}" && -d "${APPIMAGE_HOST_DIR}" ]]; then +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:-}" && -d "${LAUNCH_DIR}" ]]; then +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:-}" && -d "${HOME}/GeneralsX/Generals" ]]; then +if [[ -z "${CNC_GENERALS_PATH:-}" ]] && has_big_files "${HOME}/GeneralsX/Generals"; then export CNC_GENERALS_PATH="$(with_trailing_slash "${HOME}/GeneralsX/Generals")" fi @@ -249,13 +289,15 @@ cp "${ICON_SRC}" "${APPDIR}/usr/share/icons/hicolor/512x512/apps/GeneralsX.png" if command -v appimagetool >/dev/null 2>&1; then APPIMAGETOOL_BIN="$(command -v appimagetool)" else + # GeneralsX @build GitHubCopilot 10/04/2026 Allow pinned appimagetool URL + optional SHA256 verification for reproducible CI packaging. APPIMAGETOOL_BIN="${APPIMAGE_ROOT}/appimagetool.AppImage" mkdir -p "${APPIMAGE_ROOT}" - if [[ ! -x "${APPIMAGETOOL_BIN}" ]]; then + if [[ ! -f "${APPIMAGETOOL_BIN}" ]]; then echo "Downloading appimagetool..." - curl -L -o "${APPIMAGETOOL_BIN}" "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" - chmod +x "${APPIMAGETOOL_BIN}" + 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}" diff --git a/scripts/build/linux/build-linux-appimage-zh.sh b/scripts/build/linux/build-linux-appimage-zh.sh index 608bbf89e6a..0d6901a27b4 100755 --- a/scripts/build/linux/build-linux-appimage-zh.sh +++ b/scripts/build/linux/build-linux-appimage-zh.sh @@ -11,6 +11,8 @@ 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" +APPIMAGETOOL_URL="${APPIMAGETOOL_URL:-https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage}" +APPIMAGETOOL_SHA256="${APPIMAGETOOL_SHA256:-}" DXVK_LIB_DIR="${BUILD_DIR}/_deps/dxvk-src/lib" SDL3_LIB_DIR="${BUILD_DIR}/_deps/sdl3-build" @@ -50,7 +52,14 @@ copy_ldd_deps() { while IFS= read -r dep; do case "${dep}" in - /lib64/ld-linux* | /lib/*/ld-linux* | /lib/*/libc.so.* | /lib/*/libm.so.* | /lib/*/libpthread.so.* | /lib/*/librt.so.* | /lib/*/libdl.so.*) + # 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 @@ -64,6 +73,31 @@ copy_ldd_deps() { 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 "WARNING: APPIMAGETOOL_SHA256 is not set; skipping appimagetool checksum verification." + return 0 + 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 +} + 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 @@ -292,13 +326,15 @@ cp "${ICON_SRC}" "${APPDIR}/usr/share/icons/hicolor/512x512/apps/GeneralsXZH.png if command -v appimagetool >/dev/null 2>&1; then APPIMAGETOOL_BIN="$(command -v appimagetool)" else + # GeneralsX @build GitHubCopilot 10/04/2026 Allow pinned appimagetool URL + optional SHA256 verification for reproducible CI packaging. APPIMAGETOOL_BIN="${APPIMAGE_ROOT}/appimagetool.AppImage" mkdir -p "${APPIMAGE_ROOT}" - if [[ ! -x "${APPIMAGETOOL_BIN}" ]]; then + if [[ ! -f "${APPIMAGETOOL_BIN}" ]]; then echo "Downloading appimagetool..." - curl -L -o "${APPIMAGETOOL_BIN}" "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" - chmod +x "${APPIMAGETOOL_BIN}" + 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}" From d704678665272a16742fd772129c450fd397d5dc Mon Sep 17 00:00:00 2001 From: Felipe Braz Date: Fri, 10 Apr 2026 19:49:19 -0300 Subject: [PATCH 8/9] fix(ci): rename linux appimage artifacts and flatten zip --- .github/workflows/build-linux.yml | 19 +++++++++++++++---- .github/workflows/release.yml | 8 ++++---- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 967ce84c611..3ffce470600 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -277,20 +277,31 @@ jobs: tar -C "/tmp" -cf "${BUNDLE_TAR}" "GeneralsX-${{ inputs.game }}-deploy/" echo "Archive ready: ${BUNDLE_TAR}" - - name: Set artifact path + - name: Set artifact metadata id: artifact if: success() run: | PRESET="${{ inputs.preset }}" FORMAT="${{ inputs.package_format }}" + 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 - echo "path=build/GeneralsXZH-${PRESET}-x86_64.AppImage" >> "$GITHUB_OUTPUT" + INPUT_PATH="build/GeneralsXZH-${PRESET}-x86_64.AppImage" + OUTPUT_PATH="/tmp/GeneralsXZH-${PRESET}-x86_64.AppImage" else - echo "path=build/GeneralsX-${PRESET}-x86_64.AppImage" >> "$GITHUB_OUTPUT" + 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 @@ -305,7 +316,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) }} + 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 aa569deddd7..e0aa68c14c2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -106,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 From 627b7d2e6fda112938c50ebffba58362bd2e18c1 Mon Sep 17 00:00:00 2001 From: Felipe Braz Date: Fri, 10 Apr 2026 21:45:59 -0300 Subject: [PATCH 9/9] build(appimage): normalize format and pin appimagetool --- .github/workflows/build-linux.yml | 41 +++++++++---------- docs/DEV_BLOG/2026-04-DIARY.md | 26 ++++++++++++ .../linux/build-linux-appimage-generals.sh | 37 ++++++++++++++--- .../build/linux/build-linux-appimage-zh.sh | 37 ++++++++++++++--- 4 files changed, 108 insertions(+), 33 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 3ffce470600..9b286c9fcea 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -46,25 +46,7 @@ on: default: appimage jobs: - validate-package-format: - runs-on: ubuntu-latest - steps: - - name: Validate package format input - shell: bash - run: | - # GeneralsX @bugfix GitHubCopilot 10/04/2026 Reject unexpected package_format values to avoid broken artifact paths. - case "${{ inputs.package_format }}" in - appimage|gzip) - echo "package_format is valid: ${{ inputs.package_format }}" - ;; - *) - echo "ERROR: Invalid package_format '${{ inputs.package_format }}'. Expected 'appimage' or 'gzip'." >&2 - exit 1 - ;; - esac - build: - needs: [validate-package-format] name: ${{ inputs.game }}@${{ inputs.preset }} runs-on: ubuntu-latest timeout-minutes: 120 @@ -75,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 @@ -208,7 +207,7 @@ jobs: # 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() && inputs.package_format == 'appimage' + if: success() && steps.package-format.outputs.value == 'appimage' env: APPIMAGE_EXTRACT_AND_RUN: "1" run: | @@ -220,7 +219,7 @@ jobs: # GeneralsX @build GitHubCopilot 10/04/2026 Legacy gzip bundle kept as opt-in fallback for CI/testing. - name: Package as gzip bundle - if: success() && inputs.package_format == 'gzip' + if: success() && steps.package-format.outputs.value == 'gzip' run: | if [ "${{ inputs.game }}" = "GeneralsMD" ]; then BINARY="build/${{ inputs.preset }}/GeneralsMD/GeneralsXZH" @@ -282,7 +281,7 @@ jobs: if: success() run: | PRESET="${{ inputs.preset }}" - FORMAT="${{ inputs.package_format }}" + FORMAT="${{ steps.package-format.outputs.value }}" GAME_SLUG="${{ inputs.game == 'GeneralsMD' && 'generalsxzh' || 'generalsx' }}" PRESET_SLUG="${{ inputs.preset == 'linux64-deploy' && 'linux64' || inputs.preset }}" diff --git a/docs/DEV_BLOG/2026-04-DIARY.md b/docs/DEV_BLOG/2026-04-DIARY.md index aa63af78ae6..311bf0f6931 100644 --- a/docs/DEV_BLOG/2026-04-DIARY.md +++ b/docs/DEV_BLOG/2026-04-DIARY.md @@ -2,6 +2,32 @@ --- +## 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`). diff --git a/scripts/build/linux/build-linux-appimage-generals.sh b/scripts/build/linux/build-linux-appimage-generals.sh index 5c5a55fa166..6c54f5b1e77 100755 --- a/scripts/build/linux/build-linux-appimage-generals.sh +++ b/scripts/build/linux/build-linux-appimage-generals.sh @@ -11,8 +11,10 @@ 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" -APPIMAGETOOL_URL="${APPIMAGETOOL_URL:-https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage}" -APPIMAGETOOL_SHA256="${APPIMAGETOOL_SHA256:-}" +# 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" @@ -76,8 +78,8 @@ verify_sha256_if_configured() { local file_path="$1" if [[ -z "${APPIMAGETOOL_SHA256}" ]]; then - echo "WARNING: APPIMAGETOOL_SHA256 is not set; skipping appimagetool checksum verification." - return 0 + echo "ERROR: APPIMAGETOOL_SHA256 is required for appimagetool verification." >&2 + exit 1 fi if command -v sha256sum >/dev/null 2>&1; then @@ -97,6 +99,29 @@ verify_sha256_if_configured() { 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 @@ -286,10 +311,10 @@ cp "${APPDIR}/GeneralsX.desktop" "${APPDIR}/usr/share/applications/GeneralsX.des 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; then +if command -v appimagetool >/dev/null 2>&1 && [[ -z "${CI:-}" ]]; then APPIMAGETOOL_BIN="$(command -v appimagetool)" else - # GeneralsX @build GitHubCopilot 10/04/2026 Allow pinned appimagetool URL + optional SHA256 verification for reproducible CI packaging. + # 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 diff --git a/scripts/build/linux/build-linux-appimage-zh.sh b/scripts/build/linux/build-linux-appimage-zh.sh index 0d6901a27b4..35586801a28 100755 --- a/scripts/build/linux/build-linux-appimage-zh.sh +++ b/scripts/build/linux/build-linux-appimage-zh.sh @@ -11,8 +11,10 @@ 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" -APPIMAGETOOL_URL="${APPIMAGETOOL_URL:-https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage}" -APPIMAGETOOL_SHA256="${APPIMAGETOOL_SHA256:-}" +# 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" @@ -77,8 +79,8 @@ verify_sha256_if_configured() { local file_path="$1" if [[ -z "${APPIMAGETOOL_SHA256}" ]]; then - echo "WARNING: APPIMAGETOOL_SHA256 is not set; skipping appimagetool checksum verification." - return 0 + echo "ERROR: APPIMAGETOOL_SHA256 is required for appimagetool verification." >&2 + exit 1 fi if command -v sha256sum >/dev/null 2>&1; then @@ -98,6 +100,29 @@ verify_sha256_if_configured() { 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 @@ -323,10 +348,10 @@ 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; then +if command -v appimagetool >/dev/null 2>&1 && [[ -z "${CI:-}" ]]; then APPIMAGETOOL_BIN="$(command -v appimagetool)" else - # GeneralsX @build GitHubCopilot 10/04/2026 Allow pinned appimagetool URL + optional SHA256 verification for reproducible CI packaging. + # 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