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"