Skip to content

Merge pull request #31 from transfix/feature/f3dock-phase5-nfft-plumbing #120

Merge pull request #31 from transfix/feature/f3dock-phase5-nfft-plumbing

Merge pull request #31 from transfix/feature/f3dock-phase5-nfft-plumbing #120

Workflow file for this run

name: CI/CD
on:
pull_request:
branches: [ main, master, develop, 'feature/**' ]
push:
branches: [ main, master ]
tags:
- 'v[0-9]+.[0-9]+.[0-9]+*'
# Cheap jobs cancel on new pushes for fast feedback.
# Packaging jobs keep running so branch builds still produce downloadable
# binaries even when new commits arrive during a long package step.
jobs:
format-check:
name: clang-format (whole codebase)
runs-on: ubuntu-24.04
concurrency:
group: ci-${{ github.ref }}-format-check
cancel-in-progress: true
steps:
- uses: actions/checkout@v5
- name: Install clang-format
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends clang-format
- name: Run clang-format --dry-run --Werror on all tracked sources
run: |
set -euo pipefail
mapfile -t FILES < <(git ls-files -- \
'*.c' '*.cc' '*.cpp' '*.cxx' \
'*.h' '*.hh' '*.hpp' '*.hxx')
echo "Checking ${#FILES[@]} files against .clang-format ..."
if clang-format --dry-run --Werror "${FILES[@]}"; then
echo "Formatting OK."
exit 0
fi
echo "::error::clang-format would reformat files. To apply locally:"
echo " clang-format -i \$(git ls-files -- '*.c' '*.cc' '*.cpp' '*.cxx' '*.h' '*.hh' '*.hpp' '*.hxx')"
exit 1
build:
strategy:
fail-fast: false
matrix:
include:
# Linux
- os: ubuntu-latest
build_type: Debug
coverage: true
integration: true
- os: ubuntu-latest
build_type: Release
coverage: false
integration: false
# macOS
- os: macos-latest
build_type: Debug
coverage: false
integration: true
- os: macos-latest
build_type: Release
coverage: false
integration: false
# Windows
- os: windows-latest
build_type: Debug
coverage: false
integration: true
- os: windows-latest
build_type: Release
coverage: false
integration: false
runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} / ${{ matrix.build_type }}
concurrency:
group: ci-${{ github.ref }}-build-${{ matrix.os }}-${{ matrix.build_type }}
cancel-in-progress: true
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
# ── Linux dependencies ──
- name: Install dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential \
cmake \
ninja-build \
libfftw3-dev \
libboost-all-dev \
libhdf5-dev \
libgsl-dev \
libcgal-dev
# Coverage tools
if [ "${{ matrix.coverage }}" = "true" ]; then
sudo apt-get install -y --no-install-recommends lcov
fi
# ── macOS dependencies ──
- name: Install dependencies (macOS)
if: runner.os == 'macOS'
run: |
brew install \
cmake \
ninja \
fftw \
boost \
hdf5 \
gsl \
cgal
echo "BOOST_ROOT=$(brew --prefix boost)" >> $GITHUB_ENV
echo "CMAKE_PREFIX_PATH=$(brew --prefix boost)" >> $GITHUB_ENV
# ── Windows dependencies (vcpkg) ──
# cgal pulls gmp, which fetches gmp-6.3.0.tar.xz from a GNU mirror
# that times out roughly 1-in-3 cold runs. We mitigate with:
# 1) Route vcpkg working dirs to D: so we have ~80 GB free.
# 2) Cache the installed/ + packages/ + archives/ tree so warm
# runs skip the download entirely.
# 3) Wrap vcpkg install in a 5-attempt retry loop with backoff
# so transient network errors don't fail the job.
- name: 'Free disk space and route vcpkg to D: (Windows)'
if: runner.os == 'Windows'
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path D:\vcpkg-buildtrees | Out-Null
New-Item -ItemType Directory -Force -Path D:\vcpkg-packages | Out-Null
New-Item -ItemType Directory -Force -Path D:\vcpkg-downloads | Out-Null
"VCPKG_DOWNLOADS=D:\vcpkg-downloads" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
"VCPKG_BUILDTREES=D:\vcpkg-buildtrees" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
"VCPKG_PACKAGES=D:\vcpkg-packages" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
- name: Restore vcpkg cache (Windows)
if: runner.os == 'Windows'
id: vcpkg-cache
uses: actions/cache/restore@v4
with:
path: |
${{ env.VCPKG_INSTALLATION_ROOT }}\installed
${{ env.VCPKG_INSTALLATION_ROOT }}\packages
~\AppData\Local\vcpkg\archives
key: vcpkg-f2dock-${{ matrix.build_type }}-v1
restore-keys: |
vcpkg-f2dock-${{ matrix.build_type }}-
- name: Install dependencies (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$packages = @(
'fftw3','pthreads',
'boost-thread','boost-date-time','boost-regex','boost-filesystem',
'boost-system','boost-chrono','boost-program-options','boost-signals2',
'boost-format','boost-property-tree','boost-algorithm','boost-asio',
'boost-any','boost-array','boost-dynamic-bitset','boost-exception',
'boost-foreach','boost-function','boost-lambda','boost-lexical-cast',
'boost-multi-array','boost-optional','boost-smart-ptr','boost-tuple',
'boost-utility','hdf5[cpp]','gsl','cgal'
)
for ($i = 1; $i -le 5; $i++) {
Write-Host "vcpkg install attempt $i / 5"
vcpkg install @packages --triplet x64-windows `
--clean-after-build `
--x-buildtrees-root=D:\vcpkg-buildtrees `
--x-packages-root=D:\vcpkg-packages
if ($LASTEXITCODE -eq 0) { break }
if ($i -eq 5) { exit $LASTEXITCODE }
$sleep = [Math]::Min(60 * $i, 180)
Write-Host "Retrying after transient failure (sleeping $sleep s)..."
Start-Sleep -Seconds $sleep
}
- name: Save vcpkg cache (Windows)
if: runner.os == 'Windows' && steps.vcpkg-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: |
${{ env.VCPKG_INSTALLATION_ROOT }}\installed
${{ env.VCPKG_INSTALLATION_ROOT }}\packages
~\AppData\Local\vcpkg\archives
key: vcpkg-f2dock-${{ matrix.build_type }}-v1
# ── Configure (Unix) ──
- name: Configure (Unix)
if: runner.os != 'Windows'
run: |
integration_tests=OFF
if [ "${{ matrix.integration }}" = "true" ]; then
integration_tests=ON
fi
cmake_args=(
-B build
-G Ninja
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
-DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/install
-DF2DOCK_BUILD_INTEGRATION_TESTS=${integration_tests}
)
if [ "${{ matrix.coverage }}" = "true" ]; then
# F2Dock's CMakeLists.txt overwrites CMAKE_CXX_FLAGS via set(),
# so we inject coverage flags through CPPFLAGS/LDFLAGS env vars
# which the CMakeLists.txt already picks up via $ENV{CPPFLAGS}.
export CPPFLAGS="--coverage -fprofile-arcs -ftest-coverage"
export LDFLAGS="--coverage"
fi
cmake "${cmake_args[@]}"
# ── Configure (Windows) ──
- name: Configure (Windows)
if: runner.os == 'Windows'
shell: bash
run: |
integration_tests=OFF
if [ "${{ matrix.integration }}" = "true" ]; then
integration_tests=ON
fi
cmake -B build \
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \
-DCMAKE_TOOLCHAIN_FILE="$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" \
-DF2DOCK_BUILD_INTEGRATION_TESTS=${integration_tests} \
-DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/install"
# ── Build ──
- name: Build
run: cmake --build build --config ${{ matrix.build_type }} --parallel
# ── Test ──
- name: Run unit tests
working-directory: build
run: ctest --build-config ${{ matrix.build_type }} --output-on-failure --parallel 4 -LE integration
- name: Run integration tests
if: matrix.integration
working-directory: build
run: ctest --build-config ${{ matrix.build_type }} --output-on-failure --parallel 1 -L integration
# ── Coverage (Linux Debug only) ──
- name: Generate coverage report
if: matrix.coverage
run: |
lcov --directory build --capture --output-file build/coverage.info --ignore-errors mismatch
lcov --remove build/coverage.info \
'/usr/*' \
'*/test/*' \
'*/tests/*' \
'*/_deps/*' \
--output-file build/coverage_filtered.info \
--ignore-errors unused
genhtml build/coverage_filtered.info \
--output-directory build/coverage_html \
--title "F2Dock Code Coverage" \
--legend \
--demangle-cpp \
--ignore-errors source
- name: Upload coverage report
if: matrix.coverage
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: build/coverage_html/
retention-days: 30
- name: Upload coverage to Codecov
if: matrix.coverage
uses: codecov/codecov-action@v4
with:
files: build/coverage_filtered.info
fail_ci_if_error: false
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
# ── Verify executables (Unix) ──
- name: Verify executables exist (Unix)
if: runner.os != 'Windows'
run: |
test -x build/bin/F2Dock
test -x build/bin/GB-Rerank
test -x build/bin/F2DockServer
test -x build/bin/FilterOutput
echo "All 4 executables built successfully"
# ── Verify executables (Windows) ──
- name: Verify executables exist (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$exes = @("F2Dock.exe", "GB-Rerank.exe", "FilterOutput.exe")
foreach ($exe in $exes) {
$path = "build/bin/${{ matrix.build_type }}/$exe"
if (-not (Test-Path $path)) {
$path = "build/bin/$exe"
}
if (-not (Test-Path $path)) {
Write-Error "$exe not found"
exit 1
}
Write-Host "Found: $path"
}
Write-Host "All 3 Windows executables built successfully"
# ── Upload build artifacts ──
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.os }}-${{ matrix.build_type }}
path: |
build/bin/
build/lib/
retention-days: 14
package-linux:
runs-on: ubuntu-latest
name: package / linux
concurrency:
group: ci-${{ github.ref }}-package-linux
cancel-in-progress: false
steps:
- uses: actions/checkout@v5
- name: Compute artifact metadata
id: meta
run: |
VERSION=$(awk '/^[[:space:]]*VERSION[[:space:]]+[0-9]/{gsub(/[^0-9.]/,"",$2); print $2; exit}' CMakeLists.txt)
SHA=$(git rev-parse --short=8 HEAD)
ARCH=$(uname -m)
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
echo "arch=$ARCH" >> "$GITHUB_OUTPUT"
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential cmake ninja-build libfftw3-dev dpkg-dev \
libboost-all-dev libhdf5-dev libgsl-dev libcgal-dev
- name: Configure
run: |
cmake -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr \
-DF2DOCK_BUILD_TESTS=OFF
- name: Build
run: cmake --build build --parallel
- name: Create portable tarball
run: |
STEM="F2Dock-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-linux-${{ steps.meta.outputs.arch }}"
cmake --install build --prefix staging/F2Dock
mkdir -p staging/F2Dock/lib
for bin in staging/F2Dock/bin/*; do
ldd "$bin" 2>/dev/null | grep -oP '/\S+libfftw\S+' | while read lib; do
cp -uL "$lib" staging/F2Dock/lib/
done
done
for bin in staging/F2Dock/bin/*; do
name=$(basename "$bin")
mv "$bin" "$bin.real"
printf '%s\n' \
'#!/bin/sh' \
'DIR="$(cd "$(dirname "$0")" && pwd)"' \
'export LD_LIBRARY_PATH="$DIR/../lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"' \
'exec "$DIR/BINNAME.real" "$@"' > "$bin"
sed -i "s/BINNAME/$name/" "$bin"
chmod +x "$bin"
done
cp README.md staging/F2Dock/
tar -czf "$STEM.tar.gz" -C staging F2Dock
- name: Build .deb package
run: |
STEM="F2Dock-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-linux-${{ steps.meta.outputs.arch }}"
cd build
cpack -G DEB
for f in *.deb; do cp "$f" "../$STEM.deb"; done
- name: Upload Linux tarball
uses: actions/upload-artifact@v4
with:
name: F2Dock-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-linux-${{ steps.meta.outputs.arch }}.tar.gz
path: F2Dock-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-linux-${{ steps.meta.outputs.arch }}.tar.gz
if-no-files-found: error
retention-days: 30
- name: Upload Linux .deb
uses: actions/upload-artifact@v4
with:
name: F2Dock-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-linux-${{ steps.meta.outputs.arch }}.deb
path: F2Dock-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-linux-${{ steps.meta.outputs.arch }}.deb
if-no-files-found: error
retention-days: 30
package-macos:
runs-on: macos-latest
name: package / macos
concurrency:
group: ci-${{ github.ref }}-package-macos
cancel-in-progress: false
steps:
- uses: actions/checkout@v5
- name: Compute artifact metadata
id: meta
run: |
VERSION=$(awk '/^[[:space:]]*VERSION[[:space:]]+[0-9]/{gsub(/[^0-9.]/,"",$2); print $2; exit}' CMakeLists.txt)
SHA=$(git rev-parse --short=8 HEAD)
ARCH=$(uname -m)
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
echo "arch=$ARCH" >> "$GITHUB_OUTPUT"
- name: Install dependencies
run: |
brew install cmake ninja fftw boost hdf5 gsl cgal
echo "BOOST_ROOT=$(brew --prefix boost)" >> $GITHUB_ENV
echo "CMAKE_PREFIX_PATH=$(brew --prefix boost):$(brew --prefix hdf5)" >> $GITHUB_ENV
- name: Configure
run: |
cmake -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr/local \
-DF2DOCK_BUILD_TESTS=OFF
- name: Build
run: cmake --build build --parallel
- name: Create .dmg and .zip
run: |
STEM="F2Dock-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-macos-${{ steps.meta.outputs.arch }}"
cmake --install build --prefix staging/F2Dock.app/Contents/Resources
mkdir -p staging/F2Dock.app/Contents/MacOS
cp staging/F2Dock.app/Contents/Resources/bin/* staging/F2Dock.app/Contents/MacOS/
mkdir -p staging/F2Dock.app/Contents/Frameworks
for bin in staging/F2Dock.app/Contents/MacOS/*; do
otool -L "$bin" 2>/dev/null | grep -oE '/[^ ]+libfftw[^ ]+' | while read lib; do
bname=$(basename "$lib")
cp -n "$lib" staging/F2Dock.app/Contents/Frameworks/ 2>/dev/null || true
install_name_tool -change "$lib" "@executable_path/../Frameworks/$bname" "$bin" 2>/dev/null || true
done
done
cp README.md staging/F2Dock.app/Contents/Resources/
ditto -c -k --keepParent staging/F2Dock.app "$STEM.zip"
hdiutil create -volname F2Dock \
-srcfolder staging/F2Dock.app \
-ov -format UDZO \
"$STEM.dmg"
- name: Upload macOS dmg
uses: actions/upload-artifact@v4
with:
name: F2Dock-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-macos-${{ steps.meta.outputs.arch }}.dmg
path: F2Dock-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-macos-${{ steps.meta.outputs.arch }}.dmg
if-no-files-found: error
retention-days: 30
- name: Upload macOS zip
uses: actions/upload-artifact@v4
with:
name: F2Dock-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-macos-${{ steps.meta.outputs.arch }}.zip
path: F2Dock-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-macos-${{ steps.meta.outputs.arch }}.zip
if-no-files-found: error
retention-days: 30
package-windows:
runs-on: windows-latest
name: package / windows
concurrency:
group: ci-${{ github.ref }}-package-windows
cancel-in-progress: false
steps:
- uses: actions/checkout@v5
- name: Compute artifact metadata
id: meta
shell: bash
run: |
VERSION=$(awk '/^[[:space:]]*VERSION[[:space:]]+[0-9]/{gsub(/[^0-9.]/,"",$2); print $2; exit}' CMakeLists.txt)
SHA=$(git rev-parse --short=8 HEAD)
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
echo "arch=x64" >> "$GITHUB_OUTPUT"
- name: 'Free disk space and route vcpkg to D:'
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path D:\vcpkg-buildtrees | Out-Null
New-Item -ItemType Directory -Force -Path D:\vcpkg-packages | Out-Null
New-Item -ItemType Directory -Force -Path D:\vcpkg-downloads | Out-Null
"VCPKG_DOWNLOADS=D:\vcpkg-downloads" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
"VCPKG_BUILDTREES=D:\vcpkg-buildtrees" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
"VCPKG_PACKAGES=D:\vcpkg-packages" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
- name: Restore vcpkg cache
id: vcpkg-cache
uses: actions/cache/restore@v4
with:
path: |
${{ env.VCPKG_INSTALLATION_ROOT }}\installed
${{ env.VCPKG_INSTALLATION_ROOT }}\packages
~\AppData\Local\vcpkg\archives
key: vcpkg-f2dock-package-release-v1
restore-keys: |
vcpkg-f2dock-package-release-
- name: Install dependencies
shell: pwsh
run: |
$packages = @(
'fftw3','pthreads',
'boost-thread','boost-date-time','boost-regex','boost-filesystem',
'boost-system','boost-chrono','boost-program-options','boost-signals2',
'boost-format','boost-property-tree','boost-algorithm','boost-asio',
'boost-any','boost-array','boost-dynamic-bitset','boost-exception',
'boost-foreach','boost-function','boost-lambda','boost-lexical-cast',
'boost-multi-array','boost-optional','boost-smart-ptr','boost-tuple',
'boost-utility','hdf5[cpp]','gsl','cgal'
)
for ($i = 1; $i -le 5; $i++) {
Write-Host "vcpkg install attempt $i / 5"
vcpkg install @packages --triplet x64-windows `
--clean-after-build `
--x-buildtrees-root=D:\vcpkg-buildtrees `
--x-packages-root=D:\vcpkg-packages
if ($LASTEXITCODE -eq 0) { break }
if ($i -eq 5) { exit $LASTEXITCODE }
$sleep = [Math]::Min(60 * $i, 180)
Write-Host "Retrying after transient failure (sleeping $sleep s)..."
Start-Sleep -Seconds $sleep
}
- name: Save vcpkg cache
if: steps.vcpkg-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: |
${{ env.VCPKG_INSTALLATION_ROOT }}\installed
${{ env.VCPKG_INSTALLATION_ROOT }}\packages
~\AppData\Local\vcpkg\archives
key: vcpkg-f2dock-package-release-v1
- name: Configure
shell: bash
run: |
cmake -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE="$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" \
-DCMAKE_INSTALL_PREFIX=install \
-DF2DOCK_BUILD_TESTS=OFF
- name: Build
run: cmake --build build --config Release --parallel
- name: Create zip
shell: bash
run: |
STEM="F2Dock-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-windows-${{ steps.meta.outputs.arch }}"
cmake --install build --config Release --prefix staging/F2Dock
cp "$VCPKG_INSTALLATION_ROOT"/installed/x64-windows/bin/*.dll staging/F2Dock/bin/ 2>/dev/null || true
cp README.md staging/F2Dock/
cd staging
7z a -tzip ../"$STEM.zip" F2Dock/
- name: Upload Windows zip
uses: actions/upload-artifact@v4
with:
name: F2Dock-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-windows-${{ steps.meta.outputs.arch }}.zip
path: F2Dock-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.sha }}-windows-${{ steps.meta.outputs.arch }}.zip
if-no-files-found: error
retention-days: 30