Skip to content
348 changes: 348 additions & 0 deletions .github/workflows/build-lalsuite.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,348 @@
# SPDX-FileCopyrightText: 2026 The RISE Project
# SPDX-License-Identifier: MIT
---
# Based on the wheel:linux-* jobs of
# https://git.ligo.org/lscsoft/lalsuite/-/blob/lalsuite-v7.26.15/.gitlab/ci_yaml/wheel_build.yml
# (recipe: .gitlab/ci_scripts/wheel.sh), which run in the prebuilt
# lscsoft/lalsuite-manylinux images; the deps job replays that image's
# Dockerfile (https://git.ligo.org/lscsoft/lalsuite-manylinux, 333f452c).
name: Build lalsuite wheels (riscv64)

on:
workflow_dispatch:
inputs:
version:
description: 'Version glob to (re)build; empty builds every version of docs/packages/lalsuite.yaml not released yet'
required: false
default: ''
pull_request:
branches: [main]
paths:
- '.github/workflows/build-lalsuite.yml'
- 'docs/packages/lalsuite.yaml'
- 'patches/lalsuite/**'
push:
branches: [main]
paths:
- '.github/workflows/build-lalsuite.yml'
- 'docs/packages/lalsuite.yaml'
- 'patches/lalsuite/**'

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

permissions:
contents: read # to fetch code (actions/checkout)

env:
MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64
CFITSIO_URL: https://heasarc.gsfc.nasa.gov/FTP/software/fitsio/c/cfitsio-4.6.2.tar.gz
CFITSIO_SHA512: fb987b28b56e686fee80bb9cef87e8bb0b5094c425008b12fa3748c772479995c38cc16a15ba662c9ca9d5b295cfd49392bf81512446e1ecbea40760a065f091
HDF5_URL: https://github.com/HDFGroup/hdf5/releases/download/hdf5_1.14.6/hdf5-1.14.6.tar.gz
HDF5_SHA512: d93194ad8f48acf110aae317f4c37104b9042f89ef72919aac887817e4c2713449c4cb2ab6a163c61ab1c12811170d7611cdf69322b0e0f96420fcc7f99fdd67
FRAMEL_URL: https://git.ligo.org/virgo/virgoapp/Fr/-/archive/8.48.4/Fr-8.48.4.tar.bz2
FRAMEL_SHA512: 0b842d7149f8e2b04142135fa99ec0cc5af0361192df8ac8983853f4bd1ce0f65b36d4c396c59605b02bcc69eb15fe2259bfb92c49f82a2bbeb114eed500dfc3
METAIO_URL: https://software.ligo.org/lscsoft/source/metaio-8.5.1.tar.gz
METAIO_SHA512: f7c5d7f8862e088c0b7b7050f95af5e8c65234988f8cd337c32a2c2ad7b40030b881b0ae768a66c7f8e0646e1b6a2f64714a66bee6527514af6be60f824b038b
GSL_URL: https://ftp.gnu.org/gnu/gsl/gsl-2.8.tar.gz
GSL_SHA512: 4427f6ce59dc14eabd6d31ef1fcac1849b4d7357faf48873aef642464ddf21cc9b500d516f08b410f02a2daa9a6ff30220f3995584b0a6ae2f73c522d1abb66b

jobs:
setup:
uses: $/.github/workflows/_setup.yml
with:
package: lalsuite
version: ${{ inputs.version }}

deps:
needs: [setup]
if: needs.setup.outputs.versions != '[]'
name: Build lalsuite native dependencies (riscv64)
runs-on: ubuntu-24.04-riscv
timeout-minutes: 360

steps:
# chealpix, also in the upstream image, is not used by any lalsuite 7.26 configure script.
- name: Build dependencies
run: |
docker run --rm -i --network=host \
-v "${GITHUB_WORKSPACE}:/work" \
-w /work \
-e CFITSIO_URL -e CFITSIO_SHA512 \
-e HDF5_URL -e HDF5_SHA512 \
-e FRAMEL_URL -e FRAMEL_SHA512 \
-e METAIO_URL -e METAIO_SHA512 \
-e GSL_URL -e GSL_SHA512 \
"${MANYLINUX_RISCV64_IMAGE}" \
bash <<'SCRIPT'
set -euxo pipefail

build() {
local name=$1 hash=$2 url=$3
shift 3
rm -rf /tmp/build /tmp/src
# GitLab regenerates -/archive/ tarballs on demand, so framel's bytes can differ from the pin between requests.
for attempt in 1 2 3 4 5; do
curl -fLs -o /tmp/src "$url"
echo "$hash /tmp/src" | sha512sum --check && break
[ "$attempt" -lt 5 ] || exit 1
sleep 10
done
mkdir /tmp/build
pushd /tmp/build
tar --transform 's,^\(\./\)*,,' --strip-components 1 -xf ../src
if [ -e configure ]; then
# GSL 2.8's bundled config.guess predates riscv64.
cp /usr/share/automake-*/config.guess /usr/share/automake-*/config.sub .
./configure --prefix=/usr --libdir=/usr/lib64
else
cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr .
fi
make -j"$(nproc)"
make install DESTDIR=/tmp/deps-root
install -Dm644 -t "/tmp/deps-root/usr/share/licenses/${name}" "$@"
popd
}

build cfitsio "$CFITSIO_SHA512" "$CFITSIO_URL" licenses/License.txt
build hdf5 "$HDF5_SHA512" "$HDF5_URL" COPYING COPYING_LBNL_HDF5
build framel "$FRAMEL_SHA512" "$FRAMEL_URL" LICENSE
build metaio "$METAIO_SHA512" "$METAIO_URL" COPYING
build gsl "$GSL_SHA512" "$GSL_URL" COPYING

tar -czf /work/lalsuite-deps.tar.gz -C /tmp/deps-root .
SCRIPT

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: lalsuite-deps-riscv64
path: lalsuite-deps.tar.gz
retention-days: 1
if-no-files-found: error

build_wheels:
needs: [setup, deps]
if: needs.setup.outputs.versions != '[]'
name: Build lalsuite ${{ matrix.version }} ${{ matrix.python }}-manylinux_riscv64
runs-on: ubuntu-24.04-riscv
timeout-minutes: 720
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
python: ["cp312", "cp313", "cp314", "cp314t"]

env:
LALSUITE_VERSION: ${{ matrix.version }}

steps:
- name: Checkout python-wheels
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: lalsuite-deps-riscv64

- name: Build and test
run: |
docker run --rm -i --network=host \
-v "${GITHUB_WORKSPACE}:/work" \
-w /work \
-e LALSUITE_VERSION \
-e PYTHON_TAG="${{ matrix.python }}" \
-e PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ \
-e PIP_PREFER_BINARY=1 \
"${MANYLINUX_RISCV64_IMAGE}" \
bash <<'SCRIPT'
set -exo pipefail

dnf install -y git-lfs fftw3-devel libxml2-devel mpich-devel
tar -xzf /work/lalsuite-deps.tar.gz -C /
ldconfig
# No `module load mpi` (upstream's image entrypoint has one): upstream's released wheels ship no MPI programs.
# Upstream's image pins this: newer patchelf breaks lalsuite's binaries (lalsuite#626).
pipx runpip patchelf install 'patchelf<0.16.1'
patchelf --version

git lfs install
git clone --depth 1 --branch "lalsuite-v${LALSUITE_VERSION}" https://git.ligo.org/lscsoft/lalsuite.git /tmp/lalsuite
cd /tmp/lalsuite
git apply /work/patches/lalsuite/${LALSUITE_VERSION}/00*.patch
export SOURCE_DATE_EPOCH="$(git log -1 --format=%ct)"

case "$PYTHON_TAG" in
*t) PYTHON="/opt/python/${PYTHON_TAG%t}-${PYTHON_TAG}/bin/python" ;;
*) PYTHON="/opt/python/${PYTHON_TAG}-${PYTHON_TAG}/bin/python" ;;
esac
for venv in venv-build venv-test; do
"$PYTHON" -m venv "/tmp/${venv}"
"/tmp/${venv}/bin/python" -m pip install --upgrade pip
done

source /tmp/venv-build/bin/activate
# Upstream's 7.26.15 wheels were generated with SWIG 4.3.1; the image's SWIG 4.5 drops %typemaps_string_alloc.
python -m pip install --only-binary numpy 'numpy>=2.0.0' setuptools 'swig==4.3.1.post0' wheel
python -m pip list installed

sse="solar_system_ephemerides/ephemerides"
./00boot
./configure \
PYTHON="${VIRTUAL_ENV}/bin/python" \
SWIG="${VIRTUAL_ENV}/bin/swig" \
--with-fallback-data-path="../lalapps/data:../${sse}/earth:../${sse}/sun:../${sse}/time" \
--disable-doxygen \
--enable-mpi \
--without-ephem \
--enable-strict-defs \
--with-doxygen-version-suffix=

# Licences of the libraries auditwheel bundles; setuptools' default LICEN[CS]E* glob picks them up.
for dir in /usr/share/licenses/{cfitsio,hdf5,framel,metaio,gsl}; do
for f in "$dir"/*; do
cp "$f" "wheel/LICENSE.$(basename "$dir").$(basename "$f")"
done
done
for f in $(rpm -q --licensefiles fftw-libs-double); do
cp "$f" "wheel/LICENSE.fftw.$(basename "$f")"
done

make -j"$(nproc)" wheel
auditwheel repair wheel/*.whl

rm -rf wheel/build/
deactivate
source /tmp/venv-test/bin/activate

# astropy (also required by solar-system-ephemerides) ships only abi3 wheels, which free-threaded CPython cannot install.
if [ "$PYTHON_TAG" = cp314t ]; then
python -m pip install --no-deps solar-system-ephemerides wheelhouse/*
python -m pip install igwn-ligolw igwn-segments lscsoft-glue matplotlib 'numpy>=1.19' python-dateutil scipy
else
python -m pip install solar-system-ephemerides
python -m pip install wheelhouse/*
fi

python -m pip show lalsuite

python - wheelhouse/*.whl <<'EOF'
import re, sys, zipfile
names = zipfile.ZipFile(sys.argv[1]).namelist()
libs = sorted(n.split("/")[-1] for n in names if re.match(r"lalsuite\.libs/[^/]+\.so", n))
licenses = {n.split("/")[-1].split(".")[1] for n in names if ".dist-info/licenses/LICENSE." in n}
print("\n".join(libs))
print(sorted(licenses))
owner = {"cfitsio": "cfitsio", "fftw3": "fftw", "fftw3f": "fftw", "framel": "framel",
"gsl": "gsl", "gslcblas": "gsl", "hdf5": "hdf5", "hdf5_hl": "hdf5", "metaio": "metaio"}
missing = []
for lib in libs:
stem = re.match(r"lib(.+?)-[0-9a-f]{8}\.so", lib).group(1)
if stem.startswith("lal"):
continue
if owner.get(stem) not in licenses:
missing.append(lib)
assert not missing, f"bundled libraries without a licence in the wheel: {missing}"
EOF

env LAL_DEBUG_LEVEL=info python - <<EOF
import lal
import lalframe
import lalmetaio
import lalsimulation
import lalpulsar
series = lal.CreateREAL8FrequencySeries('psd', 0, 0, 1, None, 4096)
lalsimulation.SimNoisePSDaLIGOAPlusDesignSensitivityT1800042(series, 10)
lalpulsar.InitBarycenter("earth00-40-DE405.dat.gz", "sun00-40-DE405.dat.gz")
EOF

lal_path2cache --help

# lalapps_version segfaults here despite the patchelf<0.16.1 pin above (lalsuite#626,
# reproduces on upstream's own x86_64 manylinux image too); upstream's own fix attempt
# (lalsuite!2061, "Skip lalapps_version in wheel tests") was closed unmerged and #626
# itself closed with no fix landing, so tolerate it rather than fail the whole build.
lalapps_version || true
env LAL_DEBUG_LEVEL=info lalsim-detector-noise --official --aligo-nsnsopt --duration 1 >/dev/null
env LAL_DEBUG_LEVEL=info lalpulsar_PrintDetectorState --detector H1 --Alpha 4.65 --Delta -0.51 --timeGPS 1100000000 >/dev/null

rm -rf /work/wheelhouse
mkdir /work/wheelhouse
cp wheelhouse/*.whl /work/wheelhouse/
SCRIPT

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: lalsuite-${{ env.LALSUITE_VERSION }}-${{ matrix.python }}-manylinux_riscv64
path: wheelhouse/*.whl
if-no-files-found: error

gpl_sources:
needs: [setup]
if: needs.setup.outputs.versions != '[]'
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
name: Collect GPL sources for lalsuite ${{ matrix.version }}
runs-on: ubuntu-24.04-riscv

env:
LALSUITE_VERSION: ${{ matrix.version }}

steps:
- name: Checkout python-wheels
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: ./actions/collect-gpl-sources
with:
image: ${{ env.MANYLINUX_RISCV64_IMAGE }}
packages: gcc fftw
output: rpm-gpl-sources.tar

# GSL (GPLv3), metaio (GPLv2) and the frame library (LGPLv2.1) are built from these pinned tarballs in the deps job.
- name: Fetch pinned GSL, metaio and framel sources
run: |
set -euo pipefail
mkdir gpl-sources
tar xf rpm-gpl-sources.tar -C gpl-sources
for dep in GSL METAIO FRAMEL; do
url_var="${dep}_URL"
sha_var="${dep}_SHA512"
file="gpl-sources/$(basename "${!url_var}")"
for attempt in 1 2 3 4 5; do
curl -fLs -o "$file" "${!url_var}"
echo "${!sha_var} $file" | sha512sum --check && break
[ "$attempt" -lt 5 ] || exit 1
sleep 10
done
done
tar cf gpl-sources.tar -C gpl-sources .

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: lalsuite-${{ env.LALSUITE_VERSION }}-gpl-sources
path: gpl-sources.tar
if-no-files-found: error

publish:
name: Publish lalsuite ${{ matrix.version }}
needs: [setup, build_wheels, gpl_sources]
if: needs.setup.outputs.versions != '[]'
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
permissions:
contents: write
pull-requests: write
uses: $/.github/workflows/_publish-wheel.yml
secrets:
app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }}
with:
artifact-pattern: lalsuite-${{ matrix.version }}-*-manylinux_riscv64
gpl-sources-artifact: lalsuite-${{ matrix.version }}-gpl-sources
gpl-sources-description: gcc, fftw, gsl, metaio, framel
5 changes: 5 additions & 0 deletions docs/packages/lalsuite.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package-name: lalsuite
source-code: https://git.ligo.org/lscsoft/lalsuite
license: GPL-2.0-or-later
versions:
- version: 7.26.15
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
From be4d8e2b7a3938a7c6b45bb0b90dfed12b8f756e Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Mon, 28 Sep 2026 12:00:00 +0000
Subject: [PATCH] wheel: drop the license_file override so setuptools' default
glob ships every vendored dependency's licence

Upstream-Status: Inappropriate [needed for RISE's own vendored-dependency licence bundling convention; upstream's own wheel_build.yml/wheel.sh never attempts this collection at all, so there is nothing to send upstream]

wheel/setup.cfg sets the deprecated singular `license_file = COPYING`, which
setuptools honours by replacing its default `license_files` discovery
entirely (only that one path is packaged) rather than adding to it. Our
build-lalsuite.yml drops each vendored dependency's licence into the wheel
project root as `LICENSE.<dep>.<file>` for the default `LICEN[CS]E*` glob to
pick up (the same convention build-cvxopt.yml and others use), but with an
explicit override in place none of those files reach `dist-info/licenses/`
and the wheel ends up missing licences for cfitsio, fftw, framel, gsl and
hdf5 alike -- caught by the `assert not missing` check at the end of the
build_wheels job.

Dropping the override, rather than expanding it into an explicit list, lets
setuptools fall back to its default glob patterns (LICEN[CS]E*, COPYING*,
NOTICE*, AUTHORS*), which still matches `COPYING` itself -- so lalsuite's own
licence keeps shipping exactly as before, and every `LICENSE.*` file the
workflow adds is now picked up with no further packaging edit needed.
---
wheel/setup.cfg | 1 -
1 file changed, 1 deletion(-)

diff --git a/wheel/setup.cfg b/wheel/setup.cfg
index 074c015..306d97e 100644
--- a/wheel/setup.cfg
+++ b/wheel/setup.cfg
@@ -1,2 +1 @@
[metadata]
-license_file = COPYING
--
2.43.0

Loading