Repository navigation
1.0.12 #68
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build GhostWire Desktop | |
| on: | |
| workflow_dispatch: | |
| release: | |
| types: [published] | |
| env: | |
| BUILD_TYPE: Release | |
| QT_VERSION: 5.15.2 | |
| GHOSTWIRE_VERSION: 1.0.6 | |
| OPENSSL_VERSION_MACOS: 1.1.1w | |
| MACOS_DEPLOYMENT_TARGET: 11.0 | |
| jobs: | |
| build: | |
| name: ${{ matrix.job_name }} | |
| permissions: | |
| contents: write | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| matrix: | |
| include: | |
| - job_name: Windows | |
| os: windows-latest | |
| - job_name: Linux | |
| os: ubuntu-22.04 | |
| - job_name: macOS Intel | |
| os: macos-15-intel | |
| macos_arch: x86_64 | |
| ghostwire_asset_arch: x86_64 | |
| macos_artifact_name: GhostWire-Desktop-macOS-Intel | |
| qt_arch: 'clang_64' | |
| steps: | |
| # Checkout | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| - name: Restore config.json from secret | |
| shell: bash | |
| env: | |
| CONFIG_JSON: ${{ secrets.CONFIG_JSON }} | |
| run: | | |
| if [ -z "$CONFIG_JSON" ]; then | |
| echo "ERROR: CONFIG_JSON secret is not set!" >&2 | |
| exit 1 | |
| fi | |
| mkdir -p resources | |
| printf '%s' "$CONFIG_JSON" > resources/config.json | |
| echo "config.json restored ($(wc -c < resources/config.json) bytes)" | |
| - name: Download ghostwire.dll (Windows) | |
| if: runner.os == 'Windows' | |
| shell: bash | |
| run: | | |
| ASSET="ghostwire-${{ env.GHOSTWIRE_VERSION }}-windows-x86_64.zip" | |
| TMP_DIR="$(mktemp -d)" | |
| mkdir -p libs/ghostwire | |
| curl -fL "https://github.com/momentics/GhostWire.dist/releases/download/${{ env.GHOSTWIRE_VERSION }}/${ASSET}" -o "${TMP_DIR}/${ASSET}" | |
| unzip -q "${TMP_DIR}/${ASSET}" -d "${TMP_DIR}" | |
| DLL_PATH="$(find "${TMP_DIR}" -name "ghostwire.dll" -print -quit)" | |
| if [ -z "$DLL_PATH" ]; then | |
| echo "ERROR: ghostwire.dll not found in ${ASSET}" >&2 | |
| exit 1 | |
| fi | |
| cp "$DLL_PATH" libs/ghostwire/ghostwire.dll | |
| rm -rf "${TMP_DIR}" | |
| # Linux: Download libghostwire.so | |
| - name: Download libghostwire.so (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| ASSET="ghostwire-${{ env.GHOSTWIRE_VERSION }}-linux-x86_64.tar.gz" | |
| curl -fL "https://github.com/momentics/GhostWire.dist/releases/download/${{ env.GHOSTWIRE_VERSION }}/${ASSET}" -o "/tmp/${ASSET}" | |
| tar -xzf "/tmp/${ASSET}" -C libs/ghostwire/ | |
| mv libs/ghostwire/ghostwire-linux-x86_64/libghostwire.so libs/ghostwire/libghostwire.so | |
| rm -rf libs/ghostwire/ghostwire-linux-x86_64 | |
| - name: Download OpenSSL 1.1 for Linux (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| mkdir -p libs/openssl/linux | |
| # OpenSSL 1.1.1w for Ubuntu 20.04 (GLIBC 2.31 compatible) | |
| curl -fL "https://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2_amd64.deb" -o /tmp/libssl.deb | |
| dpkg-deb -x /tmp/libssl.deb /tmp/openssl-extract | |
| cp /tmp/openssl-extract/usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 libs/openssl/linux/ | |
| cp /tmp/openssl-extract/usr/lib/x86_64-linux-gnu/libssl.so.1.1 libs/openssl/linux/ | |
| rm -rf /tmp/openssl-extract /tmp/libssl.deb | |
| # Linux: Install system dependencies | |
| - name: Install system dependencies (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libssl-dev | |
| # Linux | |
| - name: Install Qt (Linux) | |
| if: runner.os == 'Linux' | |
| uses: jurplel/install-qt-action@v4 | |
| with: | |
| version: ${{ env.QT_VERSION }} | |
| host: 'linux' | |
| target: 'desktop' | |
| arch: 'gcc_64' | |
| - name: Configure CMake (Linux) | |
| if: runner.os == 'Linux' | |
| run: cmake -B build -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} | |
| - name: Build (Linux) | |
| if: runner.os == 'Linux' | |
| run: cmake --build build --config ${{ env.BUILD_TYPE }} --parallel | |
| - name: Package DEB (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| cd build | |
| cpack -C ${{ env.BUILD_TYPE }} | |
| - name: Find DEB file (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| DEB_FILE=$(find build -name "GhostWire*.deb" -print -quit) | |
| if [ -z "$DEB_FILE" ]; then | |
| echo "ERROR: DEB file not found!" >&2 | |
| exit 1 | |
| fi | |
| echo "DEB_PATH=$DEB_FILE" >> $GITHUB_ENV | |
| echo "DEB_NAME=$(basename $DEB_FILE)" >> $GITHUB_ENV | |
| - name: Package ZIP (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| ZIP_STAGE="$GITHUB_WORKSPACE/build/package-zip-root" | |
| ZIP_FILE="$GITHUB_WORKSPACE/build/${DEB_NAME%.deb}.zip" | |
| rm -rf "$ZIP_STAGE" "$ZIP_FILE" | |
| mkdir -p "$ZIP_STAGE" | |
| DESTDIR="$ZIP_STAGE" cmake --install build --config ${{ env.BUILD_TYPE }} | |
| ( | |
| cd "$ZIP_STAGE" | |
| zip -r -y "$ZIP_FILE" . | |
| ) | |
| echo "ZIP_PATH=$ZIP_FILE" >> $GITHUB_ENV | |
| echo "ZIP_NAME=$(basename $ZIP_FILE)" >> $GITHUB_ENV | |
| - name: Upload DEB to Release (Linux) | |
| if: runner.os == 'Linux' && github.event_name == 'release' | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| files: ${{ env.DEB_PATH }} | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Upload ZIP to Release (Linux) | |
| if: runner.os == 'Linux' && github.event_name == 'release' | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| files: ${{ env.ZIP_PATH }} | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Upload DEB artifact (Linux) | |
| if: runner.os == 'Linux' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: GhostWire-Desktop-Linux | |
| path: ${{ env.DEB_PATH }} | |
| - name: Upload ZIP artifact (Linux) | |
| if: runner.os == 'Linux' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: GhostWire-Desktop-Linux-ZIP | |
| path: ${{ env.ZIP_PATH }} | |
| # Windows | |
| - name: Install Qt (Windows) | |
| if: runner.os == 'Windows' | |
| uses: jurplel/install-qt-action@v4 | |
| with: | |
| version: ${{ env.QT_VERSION }} | |
| host: 'windows' | |
| target: 'desktop' | |
| arch: 'win64_msvc2019_64' | |
| - name: Install WiX Toolset (Windows) | |
| if: runner.os == 'Windows' | |
| run: | | |
| $wixPath = Get-ChildItem "C:\Program Files (x86)\WiX Toolset v3*" -Directory -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First 1 | |
| if ($wixPath -and (Test-Path "$($wixPath.FullName)\bin\candle.exe")) { | |
| Write-Host "Found WiX 3 at $($wixPath.FullName)" | |
| echo "$($wixPath.FullName)\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append | |
| } else { | |
| Write-Host "WiX 3 not found. Installing v3.11.2..." | |
| choco install wixtoolset --version 3.11.2 --no-progress | |
| $wixPath = Get-ChildItem "C:\Program Files (x86)\WiX Toolset v3*" -Directory -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First 1 | |
| if ($wixPath) { | |
| echo "$($wixPath.FullName)\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append | |
| } else { | |
| Write-Error "WiX installation failed." | |
| exit 1 | |
| } | |
| } | |
| Get-Command candle -ErrorAction Stop | |
| Get-Command light -ErrorAction Stop | |
| - name: Configure CMake (Windows) | |
| if: runner.os == 'Windows' | |
| run: cmake -B build -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} | |
| - name: Build (Windows) | |
| if: runner.os == 'Windows' | |
| run: cmake --build build --config ${{ env.BUILD_TYPE }} --parallel | |
| - name: Package MSI (Windows) | |
| if: runner.os == 'Windows' | |
| run: cpack --config build/CPackConfig.cmake -C ${{ env.BUILD_TYPE }} | |
| - name: Show WiX Log on Failure | |
| if: failure() && runner.os == 'Windows' | |
| run: | | |
| $log = "_CPack_Packages/win64/WIX/wix.log" | |
| if (Test-Path $log) { | |
| Write-Host "=== WiX Error Log ===" | |
| Get-Content $log | |
| } | |
| - name: Find MSI file (Windows) | |
| if: runner.os == 'Windows' | |
| run: | | |
| $msi = Get-ChildItem -Path . -Filter "*.msi" -Recurse | Select-Object -First 1 | |
| if ($msi) { | |
| echo "MSI_PATH=$($msi.FullName)" >> $env:GITHUB_ENV | |
| echo "MSI_NAME=$($msi.Name)" >> $env:GITHUB_ENV | |
| } else { | |
| Write-Error "MSI file not found!" | |
| exit 1 | |
| } | |
| - name: Package ZIP (Windows) | |
| if: runner.os == 'Windows' | |
| run: | | |
| $zipStage = Join-Path $env:GITHUB_WORKSPACE "build\package-zip-root" | |
| $zipFile = Join-Path $env:GITHUB_WORKSPACE "build\$($env:MSI_NAME -replace '\.msi$', '.zip')" | |
| Remove-Item -Recurse -Force $zipStage, $zipFile -ErrorAction SilentlyContinue | |
| New-Item -ItemType Directory -Force -Path $zipStage | Out-Null | |
| cmake --install build --config $env:BUILD_TYPE --prefix $zipStage | |
| $requiredFiles = @( | |
| "GhostWireDesktop.exe", | |
| "ghostwire.dll", | |
| "libcrypto-1_1-x64.dll", | |
| "libssl-1_1-x64.dll" | |
| ) | |
| foreach ($file in $requiredFiles) { | |
| $path = Join-Path $zipStage $file | |
| if (!(Test-Path $path)) { | |
| Write-Error "Required ZIP payload file is missing: $file" | |
| exit 1 | |
| } | |
| } | |
| Compress-Archive -Path (Join-Path $zipStage "*") -DestinationPath $zipFile -Force | |
| echo "WIN_ZIP_PATH=$zipFile" >> $env:GITHUB_ENV | |
| echo "WIN_ZIP_NAME=$(Split-Path $zipFile -Leaf)" >> $env:GITHUB_ENV | |
| - name: Upload MSI to Release (Windows) | |
| if: runner.os == 'Windows' && github.event_name == 'release' | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| files: ${{ env.MSI_PATH }} | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Upload ZIP to Release (Windows) | |
| if: runner.os == 'Windows' && github.event_name == 'release' | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| files: ${{ env.WIN_ZIP_PATH }} | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Upload MSI artifact (Windows) | |
| if: runner.os == 'Windows' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: GhostWire-Desktop-Windows | |
| path: ${{ env.MSI_PATH }} | |
| - name: Upload ZIP artifact (Windows) | |
| if: runner.os == 'Windows' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: GhostWire-Desktop-Windows-ZIP | |
| path: ${{ env.WIN_ZIP_PATH }} | |
| # macOS | |
| - name: Download libghostwire.dylib (macOS) | |
| if: runner.os == 'macOS' | |
| run: | | |
| ASSET="ghostwire-${{ env.GHOSTWIRE_VERSION }}-macos-${{ matrix.ghostwire_asset_arch }}.tar.gz" | |
| TMP_DIR="$(mktemp -d)" | |
| mkdir -p libs/ghostwire | |
| curl -fL "https://github.com/momentics/GhostWire.dist/releases/download/${{ env.GHOSTWIRE_VERSION }}/${ASSET}" -o "${TMP_DIR}/${ASSET}" | |
| tar -xzf "${TMP_DIR}/${ASSET}" -C "${TMP_DIR}" | |
| DYLIB_PATH="$(find "${TMP_DIR}" -name "libghostwire.dylib" -print -quit)" | |
| if [ -z "$DYLIB_PATH" ]; then | |
| echo "ERROR: libghostwire.dylib not found in ${ASSET}" >&2 | |
| exit 1 | |
| fi | |
| cp "$DYLIB_PATH" libs/ghostwire/libghostwire.dylib | |
| rm -rf "${TMP_DIR}" | |
| - name: Install OpenSSL (macOS) | |
| if: runner.os == 'macOS' | |
| run: | | |
| export MACOSX_DEPLOYMENT_TARGET="${{ env.MACOS_DEPLOYMENT_TARGET }}" | |
| mkdir -p libs/openssl/macos | |
| OPENSSL_TARGET="darwin64-${{ matrix.macos_arch }}-cc" | |
| OPENSSL_ARCHIVE="openssl-${{ env.OPENSSL_VERSION_MACOS }}.tar.gz" | |
| OPENSSL_URL="https://github.com/openssl/openssl/releases/download/OpenSSL_1_1_1w/${OPENSSL_ARCHIVE}" | |
| TMP_DIR="$(mktemp -d)" | |
| curl -fL "${OPENSSL_URL}" -o "${TMP_DIR}/${OPENSSL_ARCHIVE}" | |
| tar -xzf "${TMP_DIR}/${OPENSSL_ARCHIVE}" -C "${TMP_DIR}" | |
| pushd "${TMP_DIR}/openssl-${{ env.OPENSSL_VERSION_MACOS }}" | |
| ./Configure "${OPENSSL_TARGET}" shared no-tests --prefix="${TMP_DIR}/openssl-install" | |
| make -j"$(sysctl -n hw.ncpu)" | |
| make install_sw | |
| popd | |
| cp "${TMP_DIR}/openssl-install/lib/libcrypto.1.1.dylib" libs/openssl/macos/ | |
| cp "${TMP_DIR}/openssl-install/lib/libssl.1.1.dylib" libs/openssl/macos/ | |
| OPENSSL_LICENSE="$(find "${TMP_DIR}/openssl-${{ env.OPENSSL_VERSION_MACOS }}" -maxdepth 1 -type f \( -name 'LICENSE' -o -name 'LICENSE.txt' \) | head -n 1)" | |
| if [ -z "$OPENSSL_LICENSE" ]; then | |
| echo "ERROR: OpenSSL license file not found" >&2 | |
| exit 1 | |
| fi | |
| cp "${OPENSSL_LICENSE}" libs/openssl/macos/license.txt | |
| rm -rf "${TMP_DIR}" | |
| - name: Install Qt (macOS) | |
| if: runner.os == 'macOS' | |
| uses: jurplel/install-qt-action@v4 | |
| with: | |
| version: ${{ env.QT_VERSION }} | |
| host: 'mac' | |
| target: 'desktop' | |
| arch: ${{ matrix.qt_arch }} | |
| - name: Configure CMake (macOS) | |
| if: runner.os == 'macOS' | |
| run: cmake -B build -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} -DCMAKE_OSX_ARCHITECTURES=${{ matrix.macos_arch }} -DCMAKE_OSX_DEPLOYMENT_TARGET=${{ env.MACOS_DEPLOYMENT_TARGET }} | |
| - name: Build (macOS) | |
| if: runner.os == 'macOS' | |
| run: cmake --build build --config ${{ env.BUILD_TYPE }} --parallel | |
| - name: Verify macOS deployment target | |
| if: runner.os == 'macOS' | |
| run: | | |
| python3 - <<'PY' | |
| import os | |
| import re | |
| import subprocess | |
| import sys | |
| from pathlib import Path | |
| app = Path("build/GhostWireDesktop.app") | |
| if not app.exists(): | |
| sys.exit(f"App bundle not found: {app}") | |
| def parse_version(value): | |
| parts = [int(part) for part in value.split(".")] | |
| return tuple(parts + [0] * (3 - len(parts))) | |
| target = parse_version(os.environ["MACOS_DEPLOYMENT_TARGET"]) | |
| def version_text(value): | |
| parts = list(value) | |
| while len(parts) > 1 and parts[-1] == 0: | |
| parts.pop() | |
| return ".".join(str(part) for part in parts) | |
| macho_files = [] | |
| for path in app.rglob("*"): | |
| if not path.is_file() or path.is_symlink(): | |
| continue | |
| description = subprocess.run( | |
| ["file", str(path)], | |
| check=True, | |
| text=True, | |
| capture_output=True, | |
| ).stdout | |
| if "Mach-O" in description: | |
| macho_files.append(path) | |
| if not macho_files: | |
| sys.exit(f"No Mach-O files found in {app}") | |
| failures = [] | |
| for path in sorted(macho_files): | |
| output = subprocess.run( | |
| ["otool", "-l", str(path)], | |
| check=True, | |
| text=True, | |
| capture_output=True, | |
| ).stdout | |
| versions = [] | |
| for block in re.split(r"(?=^Load command \d+$)", output, flags=re.MULTILINE): | |
| if re.search(r"^\s*cmd\s+LC_BUILD_VERSION$", block, re.MULTILINE): | |
| match = re.search(r"^\s*minos\s+([0-9]+(?:\.[0-9]+){1,2})", block, re.MULTILINE) | |
| if match: | |
| versions.append(parse_version(match.group(1))) | |
| elif re.search(r"^\s*cmd\s+LC_VERSION_MIN_MACOSX$", block, re.MULTILINE): | |
| match = re.search(r"^\s*version\s+([0-9]+(?:\.[0-9]+){1,2})", block, re.MULTILINE) | |
| if match: | |
| versions.append(parse_version(match.group(1))) | |
| if not versions: | |
| failures.append((path, "missing deployment target load command")) | |
| continue | |
| too_new = [version for version in versions if version > target] | |
| if too_new: | |
| failures.append((path, ", ".join(version_text(version) for version in too_new))) | |
| else: | |
| print(f"OK {path}: min {', '.join(version_text(version) for version in sorted(set(versions)))}") | |
| if failures: | |
| print(f"Mach-O files requiring macOS newer than {os.environ['MACOS_DEPLOYMENT_TARGET']}:", file=sys.stderr) | |
| for path, detail in failures: | |
| print(f" {path}: {detail}", file=sys.stderr) | |
| sys.exit(1) | |
| PY | |
| - name: Package DMG (macOS) | |
| if: runner.os == 'macOS' | |
| run: | | |
| cd build | |
| cpack -C ${{ env.BUILD_TYPE }} | |
| - name: Find DMG file (macOS) | |
| if: runner.os == 'macOS' | |
| run: | | |
| DMG_FILE=$(find build -name "GhostWire*.dmg" -print -quit) | |
| if [ -z "$DMG_FILE" ]; then | |
| echo "ERROR: DMG file not found!" >&2 | |
| exit 1 | |
| fi | |
| echo "DMG_PATH=$DMG_FILE" >> $GITHUB_ENV | |
| echo "DMG_NAME=$(basename $DMG_FILE)" >> $GITHUB_ENV | |
| - name: Upload DMG to Release (macOS) | |
| if: runner.os == 'macOS' && github.event_name == 'release' | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| files: ${{ env.DMG_PATH }} | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Upload DMG artifact (macOS) | |
| if: runner.os == 'macOS' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.macos_artifact_name }} | |
| path: ${{ env.DMG_PATH }} |