From 4a501a5663ebf12773eb626d9404e9878ad6b31a Mon Sep 17 00:00:00 2001 From: Christophe Combelles Date: Sat, 16 May 2026 11:36:09 +0200 Subject: [PATCH] deps: centralize toolchain pins (Alpine, macOS target, Python) in versions.env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit versions.env was only the source of truth for MUMPS / OpenBLAS / GHC. Other toolchain pins (Alpine base image, MACOSX_DEPLOYMENT_TARGET, Python version) were duplicated as literals across the Dockerfile, three build/prebuild workflows, two pyvolca workflows, and three shell scripts. A real drift case just hit us: docker/Dockerfile pinned OPENBLAS_VERSION=0.3.27 as an ARG default while versions.env had moved to 0.3.33, so `./docker-build.sh` silently kept building the older release until a fresh container build failed unrelatedly. Make versions.env the single source for every toolchain pin: * Dockerfile now sources OPENBLAS_VERSION from /tmp/versions.env (same pattern MUMPS_VERSION already used). ALPINE_VERSION is an ARG with no default — docker/docker-build.sh sources versions.env and passes --build-arg. No default on purpose: a stale default is exactly how OpenBLAS drifted. * The three build/prebuild workflows (_build-matrix, prebuild-mumps, prebuild-cabal-store) gain a tiny `versions` job that reads versions.env and exposes alpine + macos_target as job outputs. The build matrix references those at job level for `container:` and the workflow-level `MACOSX_DEPLOYMENT_TARGET` env. matrix.container is gone — `if: matrix.os == 'linux'` replaces the brittle `matrix.container == 'alpine:3.23'` string compare. * pyvolca.yml and pyvolca-release.yml gain the same versions job and reference its python output for setup-python. * build.sh, build-mumps.sh and gen-cabal-config.sh replace the `${MACOSX_DEPLOYMENT_TARGET:-13.0}` fallbacks with `:?`, so a standalone invocation without sourcing versions.env now fails fast with a clear message instead of silently reverting to a stale default. Bumping any toolchain pin is now a one-line edit in versions.env. --- .github/workflows/_build-matrix.yml | 34 +++++++++++++++++----- .github/workflows/prebuild-cabal-store.yml | 26 +++++++++++++---- .github/workflows/prebuild-mumps.yml | 26 +++++++++++++---- .github/workflows/pyvolca-release.yml | 16 ++++++++-- .github/workflows/pyvolca.yml | 15 +++++++++- build-mumps.sh | 4 +-- build.sh | 9 ++++-- docker/Dockerfile | 13 ++++++--- docker/docker-build.sh | 6 +++- gen-cabal-config.sh | 2 +- versions.env | 21 +++++++++++++ 11 files changed, 140 insertions(+), 32 deletions(-) diff --git a/.github/workflows/_build-matrix.yml b/.github/workflows/_build-matrix.yml index ea4a9085..232eb0e0 100644 --- a/.github/workflows/_build-matrix.yml +++ b/.github/workflows/_build-matrix.yml @@ -22,15 +22,35 @@ permissions: contents: read jobs: + # Single source of truth for toolchain pins. Each downstream job + # references needs.versions.outputs.* so a versions.env edit propagates + # everywhere without touching workflow YAML. + versions: + name: Read pinned versions + runs-on: ubuntu-latest + outputs: + alpine: ${{ steps.r.outputs.alpine }} + macos_target: ${{ steps.r.outputs.macos_target }} + steps: + - uses: actions/checkout@v4 + - id: r + shell: bash + run: | + source versions.env + echo "alpine=alpine:$ALPINE_VERSION" >> "$GITHUB_OUTPUT" + echo "macos_target=$MACOSX_DEPLOYMENT_TARGET" >> "$GITHUB_OUTPUT" + build: name: ${{ matrix.name }} + needs: versions runs-on: ${{ matrix.runner }} # Linux jobs run in an Alpine container so the released binary links # against musl libc — truly portable across distros (vs. glibc-static, # which leaks dlopen/getaddrinfo and forces ABI-specific shims). Windows/macOS # keep running on the bare runner — GH macOS hosts cannot run containers, - # and Windows uses MSYS2 instead of a Linux container. - container: ${{ matrix.container }} + # and Windows uses MSYS2 instead of a Linux container. Empty string ⇒ + # no container; GHA accepts that idiom for conditionally-containerized jobs. + container: ${{ matrix.os == 'linux' && needs.versions.outputs.alpine || '' }} # Catches genuine hangs without false-failing legitimate cold builds. # Windows cold path (MSYS2 + GHCup install + cold cabal compile + 25-min # test step) tops out around 50 min; 60 min gives ~10 min headroom while @@ -42,11 +62,9 @@ jobs: include: - name: linux-amd64 runner: ubuntu-latest - container: alpine:3.23 os: linux - name: linux-arm64 runner: ubuntu-24.04-arm - container: alpine:3.23 os: linux - name: windows-amd64 runner: windows-latest @@ -55,7 +73,7 @@ jobs: runner: macos-15 os: macos env: - MACOSX_DEPLOYMENT_TARGET: '13.0' + MACOSX_DEPLOYMENT_TARGET: ${{ needs.versions.outputs.macos_target }} # Skip the brew formula refresh and post-install cleanup on every # invocation — the runner image is ephemeral, the disk savings # do not matter, and the refresh alone can add 30-60s. @@ -71,9 +89,9 @@ jobs: # Linux runners"). This composite action drops a patched gcompat shim # into /lib and masks /etc/os-release so the runner stops detecting # Alpine. The action's own `runner.arch == 'ARM64'` guard makes it a - # no-op on amd64. Gated on the Alpine container in case a future matrix - # row reintroduces a non-Alpine Linux entry. - - if: matrix.container == 'alpine:3.23' + # no-op on amd64. Every Linux matrix row runs in Alpine; future + # non-Alpine Linux rows would need a separate selector. + - if: matrix.os == 'linux' uses: laverdet/alpine-arm64@v1 - uses: actions/checkout@v4 diff --git a/.github/workflows/prebuild-cabal-store.yml b/.github/workflows/prebuild-cabal-store.yml index 80f2820a..5f565d24 100644 --- a/.github/workflows/prebuild-cabal-store.yml +++ b/.github/workflows/prebuild-cabal-store.yml @@ -25,24 +25,40 @@ on: workflow_dispatch: jobs: + # Read toolchain pins once; downstream jobs reference needs.versions.outputs.*. + # See _build-matrix.yml for the rationale. + versions: + name: Read pinned versions + runs-on: ubuntu-latest + outputs: + alpine: ${{ steps.r.outputs.alpine }} + macos_target: ${{ steps.r.outputs.macos_target }} + steps: + - uses: actions/checkout@v4 + - id: r + shell: bash + run: | + source versions.env + echo "alpine=alpine:$ALPINE_VERSION" >> "$GITHUB_OUTPUT" + echo "macos_target=$MACOSX_DEPLOYMENT_TARGET" >> "$GITHUB_OUTPUT" + build: name: ${{ matrix.name }} + needs: versions runs-on: ${{ matrix.runner }} # Linux jobs prebuild the cabal store in Alpine so the cached # ~/.cabal/store is linked against musl libc — matches what # _build-matrix.yml consumes. See _build-matrix.yml for context. - container: ${{ matrix.container }} + container: ${{ matrix.os == 'linux' && needs.versions.outputs.alpine || '' }} strategy: fail-fast: false matrix: include: - name: linux-amd64 runner: ubuntu-latest - container: alpine:3.23 os: linux - name: linux-arm64 runner: ubuntu-24.04-arm - container: alpine:3.23 os: linux - name: windows-amd64 runner: windows-latest @@ -51,7 +67,7 @@ jobs: runner: macos-15 os: macos env: - MACOSX_DEPLOYMENT_TARGET: '13.0' + MACOSX_DEPLOYMENT_TARGET: ${{ needs.versions.outputs.macos_target }} HOMEBREW_NO_AUTO_UPDATE: '1' HOMEBREW_NO_INSTALL_CLEANUP: '1' defaults: @@ -60,7 +76,7 @@ jobs: steps: # See _build-matrix.yml for context: GH's JS actions need a glibc Node # and won't run in Alpine arm64 without this gcompat + os-release mask. - - if: matrix.container == 'alpine:3.23' + - if: matrix.os == 'linux' uses: laverdet/alpine-arm64@v1 - uses: actions/checkout@v4 diff --git a/.github/workflows/prebuild-mumps.yml b/.github/workflows/prebuild-mumps.yml index 14a615ac..1b48fa47 100644 --- a/.github/workflows/prebuild-mumps.yml +++ b/.github/workflows/prebuild-mumps.yml @@ -20,24 +20,40 @@ on: workflow_dispatch: jobs: + # Read toolchain pins once; downstream jobs reference needs.versions.outputs.*. + # See _build-matrix.yml for the rationale. + versions: + name: Read pinned versions + runs-on: ubuntu-latest + outputs: + alpine: ${{ steps.r.outputs.alpine }} + macos_target: ${{ steps.r.outputs.macos_target }} + steps: + - uses: actions/checkout@v4 + - id: r + shell: bash + run: | + source versions.env + echo "alpine=alpine:$ALPINE_VERSION" >> "$GITHUB_OUTPUT" + echo "macos_target=$MACOSX_DEPLOYMENT_TARGET" >> "$GITHUB_OUTPUT" + build: name: ${{ matrix.name }} + needs: versions runs-on: ${{ matrix.runner }} # Linux jobs build MUMPS in Alpine so the archive is musl-compatible # (no glibc symbols leaking into libdmumps_seq.a). See _build-matrix.yml # for the wider context. Windows/macOS keep running on the bare runner. - container: ${{ matrix.container }} + container: ${{ matrix.os == 'linux' && needs.versions.outputs.alpine || '' }} strategy: fail-fast: false matrix: include: - name: linux-amd64 runner: ubuntu-latest - container: alpine:3.23 os: linux - name: linux-arm64 runner: ubuntu-24.04-arm - container: alpine:3.23 os: linux - name: windows-amd64 runner: windows-latest @@ -46,7 +62,7 @@ jobs: runner: macos-15 os: macos env: - MACOSX_DEPLOYMENT_TARGET: '13.0' + MACOSX_DEPLOYMENT_TARGET: ${{ needs.versions.outputs.macos_target }} HOMEBREW_NO_AUTO_UPDATE: '1' HOMEBREW_NO_INSTALL_CLEANUP: '1' defaults: @@ -55,7 +71,7 @@ jobs: steps: # See _build-matrix.yml for context: GH's JS actions need a glibc Node # and won't run in Alpine arm64 without this gcompat + os-release mask. - - if: matrix.container == 'alpine:3.23' + - if: matrix.os == 'linux' uses: laverdet/alpine-arm64@v1 - uses: actions/checkout@v4 diff --git a/.github/workflows/pyvolca-release.yml b/.github/workflows/pyvolca-release.yml index 661207fb..f7f29180 100644 --- a/.github/workflows/pyvolca-release.yml +++ b/.github/workflows/pyvolca-release.yml @@ -11,6 +11,18 @@ on: - 'pyvolca-v[0-9]*' jobs: + versions: + name: Read pinned versions + runs-on: ubuntu-latest + outputs: + python: ${{ steps.r.outputs.python }} + steps: + - uses: actions/checkout@v4 + - id: r + run: | + source versions.env + echo "python=$PYTHON_VERSION" >> "$GITHUB_OUTPUT" + verify-tag: name: Verify tag matches pyproject.toml runs-on: ubuntu-latest @@ -27,7 +39,7 @@ jobs: fi build: - needs: verify-tag + needs: [verify-tag, versions] runs-on: ubuntu-latest defaults: run: @@ -36,7 +48,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: ${{ needs.versions.outputs.python }} - name: Build sdist + wheel run: | pip install build diff --git a/.github/workflows/pyvolca.yml b/.github/workflows/pyvolca.yml index 55093d90..c363e413 100644 --- a/.github/workflows/pyvolca.yml +++ b/.github/workflows/pyvolca.yml @@ -18,8 +18,21 @@ permissions: actions: read jobs: + versions: + name: Read pinned versions + runs-on: ubuntu-latest + outputs: + python: ${{ steps.r.outputs.python }} + steps: + - uses: actions/checkout@v4 + - id: r + run: | + source versions.env + echo "python=$PYTHON_VERSION" >> "$GITHUB_OUTPUT" + test: name: Test pyvolca + needs: versions runs-on: ubuntu-latest defaults: run: @@ -29,7 +42,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: ${{ needs.versions.outputs.python }} cache: pip cache-dependency-path: pyvolca/pyproject.toml diff --git a/build-mumps.sh b/build-mumps.sh index 3dfaee30..485828cb 100755 --- a/build-mumps.sh +++ b/build-mumps.sh @@ -41,8 +41,8 @@ if [[ "$(uname -s)" == "Darwin" ]]; then if [[ -n "$HOMEBREW_GCC" ]]; then CC_DEFAULT="$HOMEBREW_GCC" fi - EXTRA_FFLAGS="-mmacosx-version-min=${MACOSX_DEPLOYMENT_TARGET:-13.0}" - EXTRA_CFLAGS="-mmacosx-version-min=${MACOSX_DEPLOYMENT_TARGET:-13.0}" + EXTRA_FFLAGS="-mmacosx-version-min=${MACOSX_DEPLOYMENT_TARGET:?MACOSX_DEPLOYMENT_TARGET must be set (source versions.env)}" + EXTRA_CFLAGS="-mmacosx-version-min=${MACOSX_DEPLOYMENT_TARGET:?MACOSX_DEPLOYMENT_TARGET must be set (source versions.env)}" fi CC="${CC:-$CC_DEFAULT}" FC="${FC:-$FC_DEFAULT}" diff --git a/build.sh b/build.sh index aaba7bc5..5a849286 100755 --- a/build.sh +++ b/build.sh @@ -68,10 +68,13 @@ set +a OS=$(detect_os) # On macOS, pin the deployment target floor for every link step so binaries -# produced on a recent Mac still run on Ventura (13.0). GHC, rustc, clang, -# and Homebrew gcc all honor this env var natively. +# produced on a recent Mac still run on the version pinned in versions.env +# (currently Ventura 13.0). GHC, rustc, clang, and Homebrew gcc all honor +# this env var natively. `set -a` above auto-exported the versions.env value; +# the explicit export here is just for readability. if [[ "$OS" == "macos" ]]; then - export MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-13.0}" + : "${MACOSX_DEPLOYMENT_TARGET:?MACOSX_DEPLOYMENT_TARGET must be set in versions.env}" + export MACOSX_DEPLOYMENT_TARGET fi # Strip + UPX + (re-)sign $1 in place. Idempotent: a binary already UPX'd diff --git a/docker/Dockerfile b/docker/Dockerfile index 90d59d0b..f7d62154 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -13,7 +13,11 @@ # file dist/volca # → "statically linked", "ELF 64-bit" # ldd dist/volca # → "not a dynamic executable" -FROM alpine:3.23 AS haskell-builder +# ALPINE_VERSION is the single source of truth — set in versions.env and +# passed by docker/docker-build.sh. No default here on purpose: a stale +# fallback is exactly how OPENBLAS_VERSION drifted out of sync before. +ARG ALPINE_VERSION +FROM alpine:${ALPINE_VERSION} AS haskell-builder # musl toolchain + everything MUMPS / GHC need to build statically. # `*-static` packages ship the .a variants required by `executable-static`. @@ -80,8 +84,8 @@ COPY build-mumps.sh /tmp/ # archive resolution already drops those objects at link time). Adding # them also breaks OpenBLAS's bundled tests (which reference cblas_*), # making the default `make` target fail. -ARG OPENBLAS_VERSION=0.3.27 -RUN curl -fsSL "https://github.com/OpenMathLib/OpenBLAS/releases/download/v${OPENBLAS_VERSION}/OpenBLAS-${OPENBLAS_VERSION}.tar.gz" \ +RUN . /tmp/versions.env \ + && curl -fsSL "https://github.com/OpenMathLib/OpenBLAS/releases/download/v${OPENBLAS_VERSION}/OpenBLAS-${OPENBLAS_VERSION}.tar.gz" \ | tar -xz -C /tmp \ && cd "/tmp/OpenBLAS-${OPENBLAS_VERSION}" \ && make -j"$(nproc)" \ @@ -167,7 +171,8 @@ RUN mkdir -p /build/output \ # needed) so `scratch` would work, but Alpine gives us /etc/passwd, a shell # for debugging, and the few utilities volca shells out to (7z for SimaPro # CSV archives) at negligible size cost. -FROM alpine:3.23 +ARG ALPINE_VERSION +FROM alpine:${ALPINE_VERSION} ARG GIT_HASH=unknown LABEL org.opencontainers.image.revision="${GIT_HASH}" diff --git a/docker/docker-build.sh b/docker/docker-build.sh index 89fc067d..fba3bffe 100755 --- a/docker/docker-build.sh +++ b/docker/docker-build.sh @@ -39,10 +39,14 @@ if ! git diff --quiet HEAD 2>/dev/null; then fi GIT_TAG=$(git describe --tags --exact-match HEAD 2>/dev/null || echo "") -echo "Building Docker image: tag=$TAG hash=$GIT_HASH git-tag=${GIT_TAG:-none}" +# shellcheck source=../versions.env +source versions.env + +echo "Building Docker image: tag=$TAG hash=$GIT_HASH git-tag=${GIT_TAG:-none} alpine=$ALPINE_VERSION" docker build \ -f docker/Dockerfile \ + --build-arg ALPINE_VERSION="$ALPINE_VERSION" \ --build-arg GIT_HASH="$GIT_HASH" \ --build-arg GIT_TAG="$GIT_TAG" \ -t "$TAG" . diff --git a/gen-cabal-config.sh b/gen-cabal-config.sh index 890eea1f..94092b0e 100755 --- a/gen-cabal-config.sh +++ b/gen-cabal-config.sh @@ -84,7 +84,7 @@ EOF # Homebrew gcc lays out libgfortran/libquadmath under lib/gcc// GFORTRAN_LIB_DIR=$(ls -d "${BREW_PREFIX}/Cellar/gcc/"*/lib/gcc/*/ 2>/dev/null | sort -V | tail -1) : "${GFORTRAN_LIB_DIR:?Could not locate Homebrew gcc libgfortran — install with: brew install gcc}" - DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-13.0}" + DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:?MACOSX_DEPLOYMENT_TARGET must be set (source versions.env)}" # -Wl,-dead_strip and -Wl,-dead_strip_dylibs let ld64 prune unreferenced # sections and unused dylib load commands. Pairs with `split-sections: True` # below for a meaningful (5–15 %) size win before strip even runs. diff --git a/versions.env b/versions.env index ab1da47c..fa09b28c 100644 --- a/versions.env +++ b/versions.env @@ -25,6 +25,27 @@ MUMPS_PREBUILT_REVISION=4 # branch and by docker/Dockerfile's musl runtime image. OPENBLAS_VERSION=0.3.33 +# Alpine base image — pinned at major.minor so the apk package set +# (musl, gcc, gfortran, …) the Dockerfile installs stays reproducible. +# Consumed by docker/Dockerfile (via --build-arg ALPINE_VERSION) and by +# the Linux matrix rows of prebuild-mumps.yml, prebuild-cabal-store.yml, +# _build-matrix.yml. +ALPINE_VERSION=3.23 + +# macOS deployment target — minimum macOS version the released binary +# will run on. Threaded into: +# * build.sh / build-mumps.sh / gen-cabal-config.sh as +# -mmacosx-version-min / -optl-mmacosx-version-min +# * the workflow-level MACOSX_DEPLOYMENT_TARGET env, which Homebrew / +# Apple toolchain tools also pick up automatically +# Bumping forces a rebuild of the macOS prebuilts (different target ABI). +MACOSX_DEPLOYMENT_TARGET=13.0 + +# Python — consumed by actions/setup-python in pyvolca.yml and +# pyvolca-release.yml. Matches the floor of pyvolca's pyproject.toml +# requires-python. +PYTHON_VERSION=3.12 + # Build tool versions (exact versions for reproducible builds) # These are checked at build start - mismatches are warnings, not errors GHC_VERSION=9.12.4