Skip to content

Build and Package

Build and Package #17

Workflow file for this run

name: Build and Package
on:
push:
branches: [ main, develop, develop-weekly ]
pull_request:
branches: [ main, develop, develop-weekly ]
workflow_dispatch:
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- name: Linux x64
os: ubuntu-latest
triplet: x64-linux
artifact-name: ginan-linux-x64
build-dir: linux-Release
cache-key: vcpkg-linux-v4
install-deps: |
sudo apt-get update
sudo apt-get install -y build-essential cmake ninja-build curl zip unzip tar pkg-config git
preset: release
- name: macOS ARM64
os: macos-14
triplet: arm64-osx
artifact-name: ginan-macos-arm64
build-dir: mac-arm64-Release
cache-key: vcpkg-macos-arm64-v4
install-deps: |
brew install cmake ninja pkg-config libomp gcc
echo "/opt/homebrew/bin" >> $GITHUB_PATH
preset: macos-arm64-release
- name: macOS x64
os: macos-15-intel
triplet: x64-osx
artifact-name: ginan-macos-x64
build-dir: mac-x64-Release
cache-key: vcpkg-macos-x64-v3
install-deps: |
brew install cmake ninja pkg-config libomp gcc
echo "/usr/local/bin" >> $GITHUB_PATH
preset: macos-x64-release
- name: Windows x64 (Cross)
os: ubuntu-latest
triplet: x64-mingw-static
artifact-name: ginan-windows-x64
build-dir: windows-cross-Release
cache-key: vcpkg-windows-cross-v4
install-deps: |
sudo apt-get update
sudo apt-get install -y build-essential cmake ninja-build curl zip unzip tar pkg-config git mingw-w64 g++-mingw-w64-x86-64 gcc-mingw-w64-x86-64
preset: windows-cross-release
vcpkg-extra-flags: --allow-unsupported
name: Build ${{ matrix.name }}
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install system dependencies
run: ${{ matrix.install-deps }}
- name: Setup vcpkg binary cache
run: |
mkdir -p ${{ github.workspace }}/.vcpkg-cache
echo "VCPKG_BINARY_SOURCES=clear;files,${{ github.workspace }}/.vcpkg-cache,readwrite" >> $GITHUB_ENV
echo "VCPKG_BUILD_TYPE=release" >> $GITHUB_ENV
- name: Cache vcpkg
uses: actions/cache@v4
with:
path: |
.vcpkg-cache
vcpkg
key: ${{ matrix.cache-key }}-${{ hashFiles('vcpkg.json') }}
restore-keys: |
${{ matrix.cache-key }}-
- name: Setup vcpkg
run: |
if [ ! -d "vcpkg" ]; then
git clone https://github.com/Microsoft/vcpkg.git
./vcpkg/bootstrap-vcpkg.sh -disableMetrics
fi
echo "VCPKG_ROOT=${{ github.workspace }}/vcpkg" >> $GITHUB_ENV
- name: Install vcpkg dependencies
run: |
export VCPKG_BUILD_TYPE=release
cd ${{ github.workspace }}
# Retry logic for transient network failures
for i in 1 2 3; do
./vcpkg/vcpkg install --triplet ${{ matrix.triplet }} --x-install-root=./vcpkg_installed ${{ matrix.vcpkg-extra-flags }} && break || {
echo "vcpkg install attempt $i failed, retrying...";
sleep 10;
}
done
- name: Clean build directory
working-directory: src
run: rm -rf build/${{ matrix.build-dir }}
- name: Configure CMake
working-directory: src
run: cmake --preset ${{ matrix.preset }}
- name: Build
working-directory: src
run: |
if [ "$RUNNER_OS" = "Linux" ]; then
cmake --build build/${{ matrix.build-dir }} --parallel $(nproc)
else
cmake --build build/${{ matrix.build-dir }} --parallel $(sysctl -n hw.ncpu)
fi
- name: Collect artifacts
run: |
mkdir -p artifacts
cp -r bin/* artifacts/ || true
find . -name "*.exe" -exec cp {} artifacts/ \; 2>/dev/null || true
# Include required runtime libraries for macOS
if [ "$RUNNER_OS" = "macOS" ]; then
if [ -f "/opt/homebrew/lib/libomp.dylib" ]; then
cp /opt/homebrew/lib/libomp.dylib artifacts/
elif [ -f "/usr/local/lib/libomp.dylib" ]; then
cp /usr/local/lib/libomp.dylib artifacts/
fi
if [ -f "/opt/homebrew/lib/libgfortran.5.dylib" ]; then
cp /opt/homebrew/lib/libgfortran.5.dylib artifacts/ || true
elif [ -f "/usr/local/lib/libgfortran.5.dylib" ]; then
cp /usr/local/lib/libgfortran.5.dylib artifacts/ || true
fi
fi
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact-name }}
path: artifacts/
retention-days: 30
build-gui:
needs: build
strategy:
fail-fast: false
matrix:
include:
- name: Linux x64
os: ubuntu-latest
binary-artifact: ginan-linux-x64
gui-artifact: ginan-gui-linux-x64
pyinstaller-args: --windowed
extra-steps: ""
- name: macOS ARM64
os: macos-15
binary-artifact: ginan-macos-arm64
gui-artifact: ginan-gui-macos-arm64
pyinstaller-args: --onedir --clean --target-arch arm64 --noconfirm
extra-steps: macos
- name: macOS x64
os: macos-15-intel
binary-artifact: ginan-macos-x64
gui-artifact: ginan-gui-macos-x64
pyinstaller-args: --onedir --clean --noconfirm
extra-steps: macos
- name: Windows x64
os: windows-latest
binary-artifact: ginan-windows-x64
gui-artifact: ginan-gui-windows-x64
pyinstaller-args: --windowed
pyinstaller-separator: ";"
extra-steps: ""
name: Build GUI ${{ matrix.name }}
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Download binaries
uses: actions/download-artifact@v4
with:
name: ${{ matrix.binary-artifact }}
path: bin/
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Python dependencies
run: |
pip install -r scripts/GinanUI/requirements.txt
pip install pyinstaller
- name: Convert UI files to Python
working-directory: scripts/GinanUI
run: python app/utils/ui_compilation.py
- name: Make binaries executable (Unix)
if: runner.os != 'Windows'
run: chmod +x bin/*
- name: Install OpenMP (macOS)
if: matrix.extra-steps == 'macos'
run: |
echo "Installing OpenMP via Homebrew..."
brew install libomp
echo "OpenMP installed, checking location..."
ls -la /opt/homebrew/lib/libomp* 2>/dev/null || ls -la /usr/local/lib/libomp* 2>/dev/null || true
- name: Copy required libraries (macOS)
if: matrix.extra-steps == 'macos'
run: |
echo "Looking for OpenMP library..."
# libomp is keg-only, so check Cellar and opt paths
LIBOMP_PATH=""
if [ -f "/opt/homebrew/opt/libomp/lib/libomp.dylib" ]; then
LIBOMP_PATH="/opt/homebrew/opt/libomp/lib/libomp.dylib"
elif [ -f "/usr/local/opt/libomp/lib/libomp.dylib" ]; then
LIBOMP_PATH="/usr/local/opt/libomp/lib/libomp.dylib"
elif [ -f "/opt/homebrew/lib/libomp.dylib" ]; then
LIBOMP_PATH="/opt/homebrew/lib/libomp.dylib"
elif [ -f "/usr/local/lib/libomp.dylib" ]; then
LIBOMP_PATH="/usr/local/lib/libomp.dylib"
fi
if [ -n "$LIBOMP_PATH" ]; then
echo "Found libomp at: $LIBOMP_PATH"
cp -v "$LIBOMP_PATH" bin/
chmod 644 bin/libomp.dylib
else
echo "ERROR: libomp.dylib not found!"
echo "Searching in Cellar..."
find /usr/local/Cellar/libomp -name "libomp.dylib" 2>/dev/null || true
find /opt/homebrew/Cellar/libomp -name "libomp.dylib" 2>/dev/null || true
exit 1
fi
# Copy other potential dependencies
if [ -f "/opt/homebrew/lib/libgfortran.5.dylib" ]; then
cp /opt/homebrew/lib/libgfortran.5.dylib bin/ || true
elif [ -f "/usr/local/lib/libgfortran.5.dylib" ]; then
cp /usr/local/lib/libgfortran.5.dylib bin/ || true
fi
echo "Contents of bin/ after library copy:"
ls -lh bin/*.dylib 2>/dev/null || echo "No .dylib files in bin/"
- name: Build GUI with PyInstaller (Unix)
if: runner.os != 'Windows'
run: |
python -m PyInstaller --name GinanUI \
${{ matrix.pyinstaller-args }} \
--add-data "scripts/GinanUI/app:scripts/GinanUI/app" \
--add-data "scripts/GinanUI/docs:scripts/GinanUI/docs" \
--add-data "scripts/plot_pos.py:scripts" \
--add-data "scripts/plot_trace_res.py:scripts" \
--add-binary "bin/*:bin" \
--hidden-import PySide6 \
--hidden-import PySide6.QtWebEngineWidgets \
--hidden-import PySide6.QtWebEngineCore \
--hidden-import plotly \
--hidden-import scripts.plot_pos \
--hidden-import scripts.plot_trace_res \
--hidden-import scripts.GinanUI.app \
--hidden-import scripts.GinanUI.app.models \
--hidden-import scripts.GinanUI.app.models.execution \
--hidden-import scripts.GinanUI.app.controllers \
--hidden-import scripts.GinanUI.app.controllers.input_controller \
--hidden-import scripts.GinanUI.app.controllers.visualisation_controller \
--hidden-import scripts.GinanUI.app.utils \
--hidden-import scripts.GinanUI.app.utils.workers \
--hidden-import scripts.GinanUI.app.utils.cddis_credentials \
--hidden-import scripts.GinanUI.app.utils.cddis_email \
--hidden-import scripts.GinanUI.app.utils.common_dirs \
--hidden-import scripts.GinanUI.app.utils.gn_functions \
--hidden-import scripts.GinanUI.app.utils.yaml \
--hidden-import scripts.GinanUI.app.views.main_window_ui \
scripts/GinanUI/main.py
- name: Build GUI with PyInstaller (Windows)
if: runner.os == 'Windows'
run: |
pyinstaller --name GinanUI `
${{ matrix.pyinstaller-args }} `
--add-data "scripts/GinanUI/app;scripts/GinanUI/app" `
--add-data "scripts/GinanUI/docs;scripts/GinanUI/docs" `
--add-data "scripts/plot_pos.py;scripts" `
--add-data "scripts/plot_trace_res.py;scripts" `
--add-binary "bin/*;bin" `
--hidden-import PySide6 `
--hidden-import PySide6.QtWebEngineWidgets `
--hidden-import PySide6.QtWebEngineCore `
--hidden-import plotly `
--hidden-import scripts.plot_pos `
--hidden-import scripts.plot_trace_res `
--hidden-import scripts.GinanUI.app `
--hidden-import scripts.GinanUI.app.models `
--hidden-import scripts.GinanUI.app.models.execution `
--hidden-import scripts.GinanUI.app.controllers `
--hidden-import scripts.GinanUI.app.controllers.input_controller `
--hidden-import scripts.GinanUI.app.controllers.visualisation_controller `
--hidden-import scripts.GinanUI.app.utils `
--hidden-import scripts.GinanUI.app.utils.workers `
--hidden-import scripts.GinanUI.app.utils.cddis_credentials `
--hidden-import scripts.GinanUI.app.utils.cddis_email `
--hidden-import scripts.GinanUI.app.utils.common_dirs `
--hidden-import scripts.GinanUI.app.utils.gn_functions `
--hidden-import scripts.GinanUI.app.utils.yaml `
--hidden-import scripts.GinanUI.app.views.main_window_ui `
scripts/GinanUI/main.py
# Post-build cleanup
- name: Remove unnecessary files (Unix)
if: runner.os != 'Windows'
run: |
cd dist/GinanUI
# Only remove clearly unnecessary files
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
find . -type f -name "*.pyc" -delete 2>/dev/null || true
if [ "$RUNNER_OS" = "macOS" ]; then
find . -name ".DS_Store" -delete 2>/dev/null || true
fi
- name: Remove unnecessary files (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
cd dist/GinanUI
# Only remove clearly unnecessary files
Get-ChildItem -Recurse -Directory | Where-Object { $_.Name -eq "__pycache__" } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
Get-ChildItem -Recurse -File -Filter "*.pyc" | Remove-Item -Force -ErrorAction SilentlyContinue
# macOS-specific post-build steps
- name: Copy Qt WebEngine resources (macOS)
if: matrix.extra-steps == 'macos'
run: |
set -e
PYSIDE_DIR=$(python -c 'import PySide6, os; print(os.path.dirname(PySide6.__file__))')
QT_DIR="$PYSIDE_DIR/Qt"
RES_SRC1="$QT_DIR/resources"
RES_SRC2="$QT_DIR/lib/QtWebEngineCore.framework/Resources"
RES_DEST="dist/GinanUI/_internal/PySide6/Qt/resources"
mkdir -p "$RES_DEST"
copied=false
if [ -d "$RES_SRC1" ]; then
cp -R "$RES_SRC1"/* "$RES_DEST/" 2>/dev/null || true
copied=true
fi
if [ -d "$RES_SRC2" ]; then
cp -R "$RES_SRC2"/* "$RES_DEST/" 2>/dev/null || true
copied=true
fi
if [ "$copied" != true ]; then
echo "Qt WebEngine resources not found; build may fail";
fi
- name: Create launcher script (macOS)
if: matrix.extra-steps == 'macos'
run: |
cat > dist/GinanUI/run.sh << 'EOF'
#!/usr/bin/env zsh
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
export QTWEBENGINEPROCESS_PATH="$SCRIPT_DIR/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess"
export QTWEBENGINE_RESOURCES_PATH="$SCRIPT_DIR/_internal/PySide6/Qt/resources"
export QTWEBENGINE_LOCALES_PATH="$SCRIPT_DIR/_internal/PySide6/Qt/resources/qtwebengine_locales"
export QTWEBENGINE_DISABLE_SANDBOX=1
export QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu-sandbox --disable-seccomp-filter-sandbox --disable-logging --log-level=3"
export DYLD_FRAMEWORK_PATH="$SCRIPT_DIR/_internal/PySide6/Qt/lib"
export DYLD_LIBRARY_PATH="$SCRIPT_DIR/_internal/PySide6/Qt/lib"
if command -v xattr >/dev/null 2>&1; then
xattr -dr com.apple.quarantine "$SCRIPT_DIR" 2>/dev/null || true
fi
exec "$SCRIPT_DIR/GinanUI"
EOF
chmod +x dist/GinanUI/run.sh
- name: Copy QtWebEngineProcess (macOS)
if: matrix.extra-steps == 'macos'
run: |
PYSIDE_PATH=$(python -c "import PySide6; import os; print(os.path.dirname(PySide6.__file__))")
mkdir -p dist/GinanUI/Helpers
if [ -d "$PYSIDE_PATH/Qt/libexec/QtWebEngineProcess.app" ]; then
cp -R "$PYSIDE_PATH/Qt/libexec/QtWebEngineProcess.app" dist/GinanUI/Helpers/
elif [ -d "$PYSIDE_PATH/Qt/lib/QtWebEngineCore.framework/Helpers/QtWebEngineProcess.app" ]; then
cp -R "$PYSIDE_PATH/Qt/lib/QtWebEngineCore.framework/Helpers/QtWebEngineProcess.app" dist/GinanUI/Helpers/
fi
- name: Copy runtime libraries to package (macOS)
if: matrix.extra-steps == 'macos'
run: |
echo "Checking for libraries to copy..."
ls -lh bin/*.dylib 2>/dev/null || echo "No .dylib files in bin/"
# Copy OpenMP and other libraries into the package
mkdir -p dist/GinanUI/_internal/lib
if [ -f "bin/libomp.dylib" ]; then
echo "Copying libomp.dylib to package..."
cp -v bin/libomp.dylib dist/GinanUI/_internal/lib/
chmod 644 dist/GinanUI/_internal/lib/libomp.dylib
else
echo "ERROR: bin/libomp.dylib not found!"
exit 1
fi
if [ -f "bin/libgfortran.5.dylib" ]; then
echo "Copying libgfortran.5.dylib to package..."
cp -v bin/libgfortran.5.dylib dist/GinanUI/_internal/lib/
fi
echo "Libraries copied to package:"
ls -lh dist/GinanUI/_internal/lib/
# Update binaries to look in @executable_path/../lib for libraries
echo "Updating binary rpaths..."
for binary in dist/GinanUI/_internal/bin/*; do
if [ -f "$binary" ] && file "$binary" | grep -q "Mach-O"; then
echo "Processing $binary"
install_name_tool -add_rpath "@executable_path/../lib" "$binary" 2>/dev/null || true
# Update libomp reference
install_name_tool -change "/opt/homebrew/lib/libomp.dylib" "@rpath/libomp.dylib" "$binary" 2>/dev/null || true
install_name_tool -change "/usr/local/lib/libomp.dylib" "@rpath/libomp.dylib" "$binary" 2>/dev/null || true
fi
done
echo "Verifying binary rpaths after update:"
otool -L dist/GinanUI/_internal/bin/pea | grep -i omp || echo "No OpenMP reference found"
- name: Code sign binaries (macOS)
if: matrix.extra-steps == 'macos'
working-directory: dist/GinanUI
run: |
find . -type f -perm +111 -exec codesign --force --deep -s - {} \;
find . -name "*.dylib" -exec codesign --force -s - {} \;
find . -name "*.so" -exec codesign --force -s - {} \;
codesign --force -s - _internal/Python || true
codesign --force --deep -s - Helpers/QtWebEngineProcess.app || true
find _internal/PySide6/Qt/resources -type f -name "*.pak" -exec codesign --force --sign - {} \; || true
- name: Upload GUI artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.gui-artifact }}
path: dist/GinanUI/
retention-days: 30