Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 85 additions & 21 deletions .github/workflows/build-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Comment thread
fbraz3 marked this conversation as resolved.
workflow_dispatch:
inputs:
game:
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Comment on lines +208 to +218

# 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()
Expand All @@ -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
73 changes: 41 additions & 32 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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"
Expand Down
30 changes: 30 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
39 changes: 35 additions & 4 deletions docs/BUILD/INSTALL_INSTRUCTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Comment thread
fbraz3 marked this conversation as resolved.
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

Expand Down
Loading
Loading