Skip to content

Static Dependencies Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add a STATIC_DEPS CMake option and build-deps.sh script so distribution builds statically link all third-party dependencies into libForgeCommon, producing self-contained binaries for forge-studio bundling.

Architecture: A shell script builds Armadillo and ISMRMRD as static libraries into a local prefix. A new STATIC_DEPS=ON CMake option switches Boost, FFTW, HDF5, Armadillo, and ISMRMRD to static linking, disables MPI, and handles transitive deps (zlib, BLAS/LAPACK). The default STATIC_DEPS=OFF path is completely unchanged.

Tech Stack: CMake 3.17+, Bash, C++17

Spec: docs/superpowers/specs/2026-03-25-static-deps-design.md


Task 1: Create scripts/build-deps.sh

The script builds Armadillo and ISMRMRD as static PIC libraries into a local prefix.

Files: - Create: scripts/build-deps.sh

  • [ ] Step 1: Write the script

Create scripts/build-deps.sh:

#!/usr/bin/env bash
# Build Armadillo and ISMRMRD as static PIC libraries for distribution builds.
# Usage: ./scripts/build-deps.sh [--prefix deps/install] [--jobs 4]

set -euo pipefail

PREFIX="$(pwd)/deps/install"
JOBS=4
ARMA_VERSION="15.2.3"
ISMRMRD_VERSION="v1.15.0"

while [[ $# -gt 0 ]]; do
    case $1 in
        --prefix)  PREFIX="$(cd "$(dirname "$2")" 2>/dev/null && pwd)/$(basename "$2")" || PREFIX="$2"; shift 2 ;;
        --jobs)    JOBS="$2"; shift 2 ;;
        *)         echo "Usage: $0 [--prefix DIR] [--jobs N]"; exit 1 ;;
    esac
done

BUILDDIR="$(pwd)/deps/build"
mkdir -p "$BUILDDIR" "$PREFIX"

echo "=== Building static deps into $PREFIX (jobs=$JOBS) ==="

# ── Armadillo ──────────────────────────────────────────────────────────────
if [ -f "$PREFIX/lib/libarmadillo.a" ]; then
    echo "-- Armadillo: already built, skipping"
else
    echo "-- Armadillo $ARMA_VERSION: cloning and building..."
    rm -rf "$BUILDDIR/armadillo"
    git clone --depth 1 --branch "$ARMA_VERSION" \
        https://gitlab.com/conradsnicta/armadillo-code.git \
        "$BUILDDIR/armadillo"
    cmake -B "$BUILDDIR/armadillo/build" -S "$BUILDDIR/armadillo" \
        -DCMAKE_INSTALL_PREFIX="$PREFIX" \
        -DBUILD_SHARED_LIBS=OFF \
        -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
        -DCMAKE_BUILD_TYPE=Release
    cmake --build "$BUILDDIR/armadillo/build" -j"$JOBS"
    cmake --install "$BUILDDIR/armadillo/build"
    echo "-- Armadillo: installed to $PREFIX"
fi

# ── ISMRMRD ────────────────────────────────────────────────────────────────
if [ -f "$PREFIX/lib/libismrmrd.a" ]; then
    echo "-- ISMRMRD: already built, skipping"
else
    echo "-- ISMRMRD $ISMRMRD_VERSION: cloning and building..."
    rm -rf "$BUILDDIR/ismrmrd"
    git clone --depth 1 --branch "$ISMRMRD_VERSION" \
        https://github.com/ismrmrd/ismrmrd.git \
        "$BUILDDIR/ismrmrd"
    cmake -B "$BUILDDIR/ismrmrd/build" -S "$BUILDDIR/ismrmrd" \
        -DCMAKE_INSTALL_PREFIX="$PREFIX" \
        -DBUILD_SHARED_LIBS=OFF \
        -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
        -DCMAKE_BUILD_TYPE=Release \
        -DISMRMRD_BUILD_UTILITIES=OFF \
        -DISMRMRD_BUILD_TESTS=OFF \
        -DISMRMRD_BUILD_EXAMPLES=OFF
    cmake --build "$BUILDDIR/ismrmrd/build" -j"$JOBS"
    cmake --install "$BUILDDIR/ismrmrd/build"
    echo "-- ISMRMRD: installed to $PREFIX"
fi

echo ""
echo "=== Done. Use with: ==="
echo "  ARMA_HOME=$PREFIX cmake -B build -S . -DSTATIC_DEPS=ON \\"
echo "      -DISMRMRD_LIBRARIES=$PREFIX/lib/libismrmrd.a"
  • [ ] Step 2: Make executable and test
chmod +x scripts/build-deps.sh
./scripts/build-deps.sh --prefix deps/install --jobs 4
ls deps/install/lib/libarmadillo.a deps/install/lib/libismrmrd.a

Expected: both .a files exist.

  • [ ] Step 3: Test idempotency
./scripts/build-deps.sh --prefix deps/install

Expected: output shows "already built, skipping" for both.

  • [ ] Step 4: Add deps/ to .gitignore

Append to .gitignore:

# Static dependency build artifacts
deps/

  • [ ] Step 5: Commit
git add scripts/build-deps.sh .gitignore
git commit -m "build: add build-deps.sh for static Armadillo and ISMRMRD

Builds Armadillo 15.2.3 and ISMRMRD v1.15.0 as static PIC libraries
into a local prefix for distribution builds. Idempotent — skips if
already built."

Task 2: Add STATIC_DEPS option and Boost static linking

Files: - Modify: CMakeLists.txt (lines 56-75, options and Boost section)

  • [ ] Step 1: Add option and modify Boost section

After the existing options block (line 58), add the STATIC_DEPS option. Then modify the Boost section to conditionally use static libs.

Add after line 58 (option(MPISupport ...)):

option(STATIC_DEPS "Statically link third-party dependencies for distribution" OFF)

Replace lines 62-70 (Boost section) with:

# Find Boost
if(STATIC_DEPS)
    set(Boost_USE_STATIC_LIBS ON)
    # Distribution builds don't need MPI
    set(MPISupport OFF CACHE BOOL "" FORCE)
else()
    set(Boost_USE_STATIC_LIBS OFF)
endif()

if(MPISupport)
    find_package(MPI)
    find_package(Boost 1.43 REQUIRED COMPONENTS program_options serialization mpi)
else()
    find_package(Boost 1.43 REQUIRED COMPONENTS program_options serialization)
endif()

  • [ ] Step 2: Verify configure with STATIC_DEPS=OFF (unchanged behavior)
rm -rf build && cmake -B build -S . -DMETAL_COMPUTE=ON 2>&1 | tail -5

Expected: configures as before, no errors.

  • [ ] Step 3: Verify configure with STATIC_DEPS=ON
cmake -B build_static -S . -DSTATIC_DEPS=ON -DMETAL_COMPUTE=ON 2>&1 | grep -i boost

Expected: finds static Boost libraries (.a files), MPI is OFF.

  • [ ] Step 4: Commit
git add CMakeLists.txt
git commit -m "build: add STATIC_DEPS option with static Boost linking

When STATIC_DEPS=ON, set Boost_USE_STATIC_LIBS=ON and force
MPISupport=OFF for distribution builds."

Task 3: Static FFTW linking with scoped suffix override

Files: - Modify: CMakeLists.txt (lines 77-83, FFTW section)

  • [ ] Step 1: Add save/restore around FFTW find_library calls

Replace lines 77-83 (FFTW section) with:

# FFTW (CPU FFT — used on all platforms)
find_path(FFTW_INCLUDE_DIR fftw3.h HINTS $ENV{FFTW_INC})
if(STATIC_DEPS)
    # Save and override library suffixes to find static .a only for FFTW.
    # Restore immediately after so OpenMP and other finds are unaffected.
    set(_SAVED_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES})
    set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
endif()
find_library(FFTW_LIBRARIES NAMES fftw3 HINTS $ENV{FFTW_DIR} /usr/lib/x86_64-linux-gnu/)
find_library(FFTWF_LIBRARIES NAMES fftw3f HINTS $ENV{FFTW_DIR} /usr/lib/x86_64-linux-gnu/)
if(STATIC_DEPS)
    set(CMAKE_FIND_LIBRARY_SUFFIXES ${_SAVED_SUFFIXES})
    unset(_SAVED_SUFFIXES)
endif()
include_directories(${FFTW_INCLUDE_DIR})
set(LIBS ${LIBS} ${FFTW_LIBRARIES})
set(LIBS ${LIBS} ${FFTWF_LIBRARIES})

  • [ ] Step 2: Verify STATIC_DEPS=OFF still finds dynamic FFTW
rm -rf build && cmake -B build -S . -DMETAL_COMPUTE=ON 2>&1 | grep -i fftw

Expected: finds .dylib / .so FFTW libraries.

  • [ ] Step 3: Verify STATIC_DEPS=ON finds static FFTW
rm -rf build_static && cmake -B build_static -S . -DSTATIC_DEPS=ON -DMETAL_COMPUTE=ON 2>&1 | grep -i fftw

Expected: finds .a FFTW libraries.

  • [ ] Step 4: Commit
git add CMakeLists.txt
git commit -m "build: static FFTW linking with scoped suffix override

Save/restore CMAKE_FIND_LIBRARY_SUFFIXES around FFTW find_library
calls so only FFTW is affected. OpenMP and subsequent finds still
use default suffix order."

Task 4: Static HDF5, Armadillo, and ISMRMRD linking with transitive deps

This task modifies the Armadillo, HDF5, and ISMRMRD sections together since they are adjacent and interdependent.

Files: - Modify: CMakeLists.txt (lines 121-136, from Armadillo through ISMRMRD)

  • [ ] Step 1: Replace the Armadillo → HDF5 → ISMRMRD block

Replace the block from # Find Armadillo through set(LIBS ${LIBS} ${ISMRMRD_LIBRARIES}) with:

# Find Armadillo
# When STATIC_DEPS=ON, set the ARMA_HOME environment variable (not a -D
# cache var) to point at the build-deps.sh prefix. FindArmadillo.cmake
# reads $ENV{ARMA_HOME} as a HINT, not CMAKE_PREFIX_PATH.
find_package(Armadillo REQUIRED)
include_directories(${ARMADILLO_INCLUDE_DIRS})
message(${ARMADILLO_INCLUDE_DIRS})
set(LIBS ${LIBS} ${ARMADILLO_LIBRARIES})

# Armadillo static requires BLAS/LAPACK. On macOS, Accelerate provides
# these (linked below). On Linux, explicitly link OpenBLAS or system BLAS.
if(STATIC_DEPS AND NOT APPLE)
    find_library(OPENBLAS_LIBRARY openblas)
    if(OPENBLAS_LIBRARY)
        set(LIBS ${LIBS} ${OPENBLAS_LIBRARY})
    else()
        find_library(BLAS_LIBRARY blas)
        find_library(LAPACK_LIBRARY lapack)
        if(BLAS_LIBRARY AND LAPACK_LIBRARY)
            set(LIBS ${LIBS} ${LAPACK_LIBRARY} ${BLAS_LIBRARY})
        else()
            message(WARNING "STATIC_DEPS=ON but no BLAS/LAPACK found for static Armadillo on Linux")
        endif()
    endif()
endif()

# HDF5
if(STATIC_DEPS)
    set(HDF5_USE_STATIC_LIBRARIES ON)
endif()
find_package(HDF5 COMPONENTS CXX)
include_directories(${HDF5_INCLUDE_DIRS})
set(LIBS ${LIBS} ${HDF5_C_LIBRARIES})
set(LIBS ${LIBS} ${HDF5_CXX_LIBRARIES})
# HDF5 static requires zlib (and possibly szip/libaec)
if(STATIC_DEPS)
    # Verify static HDF5 was actually found
    message(STATUS "HDF5 libraries (STATIC_DEPS=ON): ${HDF5_C_LIBRARIES}")
    find_library(ZLIB_LIBRARY z)
    if(ZLIB_LIBRARY)
        set(LIBS ${LIBS} ${ZLIB_LIBRARY})
    endif()
    # szip/libaec — only needed if HDF5 was built with szip support
    find_library(SZIP_LIBRARY NAMES sz aec)
    if(SZIP_LIBRARY)
        set(LIBS ${LIBS} ${SZIP_LIBRARY})
    endif()
endif()

include_directories(Support/ArmaExtensions)

# ISMRMRD — when STATIC_DEPS=ON, pass -DISMRMRD_LIBRARIES=<path-to-.a>
# on the cmake command line to override the default find_library.
find_library(ISMRMRD_LIBRARIES ismrmrd HINTS /usr/lib/)
set(LIBS ${LIBS} ${ISMRMRD_LIBRARIES})
  • [ ] Step 2: Test STATIC_DEPS=ON with deps prefix
rm -rf build_static
ARMA_HOME=deps/install cmake -B build_static -S . \
    -DSTATIC_DEPS=ON -DMETAL_COMPUTE=ON \
    -DISMRMRD_LIBRARIES=deps/install/lib/libismrmrd.a 2>&1 | grep -iE "(armadillo|ismrmrd|hdf5.*STATIC)"

Expected: finds libarmadillo.a from deps/install, libismrmrd.a from cache override, HDF5 static .a files.

  • [ ] Step 3: Test STATIC_DEPS=OFF still works
rm -rf build && cmake -B build -S . -DMETAL_COMPUTE=ON 2>&1 | tail -5

Expected: normal configure, finds system dynamic libs.

  • [ ] Step 4: Commit
git add CMakeLists.txt
git commit -m "build: static HDF5, Armadillo, ISMRMRD linking for distribution

When STATIC_DEPS=ON:
- HDF5: set HDF5_USE_STATIC_LIBRARIES=ON, link zlib and szip/libaec
- Armadillo: found via ARMA_HOME env var pointing at build-deps.sh prefix;
  on Linux explicitly link OpenBLAS or system BLAS/LAPACK
- ISMRMRD: found via ISMRMRD_LIBRARIES cache override"

Task 5: Disable LTO for ForgeCommon when STATIC_DEPS=ON

Files: - Modify: cmake/ForgeBackend.cmake (library target properties section)

  • [ ] Step 1: Add -fno-lto for static deps builds

In cmake/ForgeBackend.cmake, after the set_target_properties(${LIB_TARGET} ...) block (around line 79), add:

    # Static FFTW contains SIMD assembly that LTO miscompiles.
    # Disable LTO on ForgeCommon when static deps are embedded.
    if(STATIC_DEPS AND APPLE)
        target_compile_options(${LIB_TARGET} PRIVATE -fno-lto)
        target_link_options(${LIB_TARGET} PRIVATE -fno-lto)
    endif()
  • [ ] Step 2: Verify the property is applied
rm -rf build_static
ARMA_HOME=deps/install cmake -B build_static -S . \
    -DSTATIC_DEPS=ON -DMETAL_COMPUTE=ON \
    -DISMRMRD_LIBRARIES=deps/install/lib/libismrmrd.a
grep -r "fno-lto" build_static/CMakeFiles/ForgeCommon_cpu.dir/flags.make 2>/dev/null || echo "Check compile_commands.json"
  • [ ] Step 3: Commit
git add cmake/ForgeBackend.cmake
git commit -m "build: disable LTO for ForgeCommon when STATIC_DEPS=ON on macOS

Static FFTW contains SIMD assembly patterns that LTO miscompiles.
Disable LTO on the library target when static deps are embedded."

Task 6: Build, test, install, and verify

  • [ ] Step 1: Full STATIC_DEPS=OFF build (regression check)
rm -rf build && cmake -B build -S . -DMETAL_COMPUTE=ON
cmake --build build -j4
./build/cpu_tests '~[Benchmark]' '~[Spiral3D]'

Expected: all tests pass (unchanged behavior).

  • [ ] Step 2: Full STATIC_DEPS=ON build
rm -rf build_static
ARMA_HOME=deps/install cmake -B build_static -S . \
    -DSTATIC_DEPS=ON -DMETAL_COMPUTE=ON \
    -DISMRMRD_LIBRARIES=deps/install/lib/libismrmrd.a
cmake --build build_static -j4

Expected: builds successfully with no linker errors.

  • [ ] Step 3: Run tests from static build
./build_static/cpu_tests '~[Benchmark]' '~[Spiral3D]'
./build_static/metal_tests '~[Benchmark]' '~[Spiral3D]'

Expected: all tests pass.

  • [ ] Step 4: Install and verify dynamic deps
cmake --install build_static --prefix /tmp/forge-static-install

# Check libForgeCommon for non-system dynamic deps
otool -L /tmp/forge-static-install/lib/libForgeCommon_metal.dylib \
    | grep -v /usr/lib | grep -v /System | grep -v @rpath

# Check executable
otool -L /tmp/forge-static-install/bin/metal/forgeSense \
    | grep -v /usr/lib | grep -v /System | grep -v @rpath

Expected: - libForgeCommon_metal.dylib should only show libomp.dylib as a non-system dep - forgeSense should only show libForgeCommon_metal and libomp.dylib

No Homebrew paths (/opt/homebrew/...) should appear.

  • [ ] Step 5: Clean up
rm -rf /tmp/forge-static-install build_static
  • [ ] Step 6: Commit any fixups needed

If any issues were found and fixed during verification, commit the fixes.


Task 7: Add .github CI job for distribution build (optional)

Files: - Create or modify: .github/workflows/ci.yml (or new ci-dist.yml)

  • [ ] Step 1: Add distribution build job

Add a new job to the macOS CI workflow that: 1. Runs build-deps.sh (with caching) 2. Configures with STATIC_DEPS=ON 3. Builds and installs 4. Verifies no unexpected dynamic deps

  dist-build:
    name: Distribution build (macOS)
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4

      - name: Cache static deps
        uses: actions/cache@v4
        with:
          path: deps/install
          key: static-deps-arma15.2.3-ismrmrd1.15.0-${{ runner.os }}-${{ runner.arch }}

      - name: Install system dependencies
        run: brew install fftw hdf5 boost libomp

      - name: Build static deps
        run: ./scripts/build-deps.sh --prefix deps/install --jobs 3

      - name: Configure (static)
        run: |
          ARMA_HOME=deps/install cmake -B build -S . \
              -DSTATIC_DEPS=ON -DMETAL_COMPUTE=ON \
              -DISMRMRD_LIBRARIES=deps/install/lib/libismrmrd.a

      - name: Build
        run: cmake --build build -j3

      - name: Install
        run: cmake --install build --prefix dist

      - name: Verify no Homebrew deps
        run: |
          ! otool -L dist/lib/libForgeCommon_metal.dylib | grep /opt/homebrew
          ! otool -L dist/bin/metal/forgeSense | grep /opt/homebrew
  • [ ] Step 2: Commit
git add .github/workflows/
git commit -m "ci: add distribution build job verifying static deps"