Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions .github/workflows/_build-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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

Expand Down
26 changes: 21 additions & 5 deletions .github/workflows/prebuild-cabal-store.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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

Expand Down
26 changes: 21 additions & 5 deletions .github/workflows/prebuild-mumps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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

Expand Down
16 changes: 14 additions & 2 deletions .github/workflows/pyvolca-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,7 +39,7 @@ jobs:
fi

build:
needs: verify-tag
needs: [verify-tag, versions]
runs-on: ubuntu-latest
defaults:
run:
Expand All @@ -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
Expand Down
15 changes: 14 additions & 1 deletion .github/workflows/pyvolca.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions build-mumps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
9 changes: 6 additions & 3 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 9 additions & 4 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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)" \
Expand Down Expand Up @@ -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}"

Expand Down
6 changes: 5 additions & 1 deletion docker/docker-build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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" .
Expand Down
2 changes: 1 addition & 1 deletion gen-cabal-config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ EOF
# Homebrew gcc lays out libgfortran/libquadmath under lib/gcc/<major>/
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.
Expand Down
21 changes: 21 additions & 0 deletions versions.env
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading