name: release
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
publish:
description: "Publish to PyPI (otherwise the wheels are built and kept as artifacts)"
type: boolean
default: false
jobs:
wheels:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- { name: macos-arm64, os: macos-14, target: aarch64-apple-darwin }
- { name: windows-x86_64, os: windows-latest, target: x86_64-pc-windows-msvc }
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Build the library
run: cargo build --release --target ${{ matrix.target }}
- name: Stage it inside the package
shell: bash
run: |
case "${{ matrix.target }}" in
*apple-darwin) lib=libferrotherm.dylib ;;
*linux-gnu) lib=libferrotherm.so ;;
*windows-msvc) lib=ferrotherm.dll ;;
esac
cp "target/${{ matrix.target }}/release/$lib" "python/ferrotherm/$lib"
ls -la "python/ferrotherm/$lib"
- name: Build the wheel
shell: bash
working-directory: python
run: |
python -m pip install -q --upgrade pip build
case "${{ matrix.target }}" in
aarch64-apple-darwin) plat=macosx_11_0_arm64 ;;
x86_64-pc-windows-msvc) plat=win_amd64 ;;
esac
python -m build --wheel -C--build-option=--plat-name="$plat"
ls -la dist/
- name: The wheel must work with nothing else present
shell: bash
working-directory: python
run: |
# A wheel that loads a library from outside itself passes every test and helps nobody.
python -m pip install -q dist/*.whl pytest
cp test_model.py "${RUNNER_TEMP}/test_model.py"
python - "${RUNNER_TEMP}/test_model.py" <<'PY'
import sys
p = sys.argv[1]
t = open(p).read().replace(
"sys.path.insert(0, str(Path(__file__).resolve().parent))\n", "")
open(p, "w").write(t)
PY
cd "${RUNNER_TEMP}"
unset FERROTHERM_LIB
python - <<'PY'
import ferrotherm, pathlib, sys
lib = pathlib.Path(ferrotherm.library_path()).resolve()
pkg = pathlib.Path(ferrotherm.__file__).resolve().parent
print("library:", lib)
if lib.parent != pkg:
sys.exit(f"not self-contained: loaded {lib} from outside the package")
PY
python -m pytest -q test_model.py
- name: Stage the library for the Julia artifact
shell: bash
run: |
mkdir -p julia-staging
case "${{ matrix.target }}" in
aarch64-apple-darwin) cp target/${{ matrix.target }}/release/libferrotherm.dylib julia-staging/ ;;
x86_64-pc-windows-msvc) cp target/${{ matrix.target }}/release/ferrotherm.dll julia-staging/ ;;
esac
- uses: actions/upload-artifact@v4
with:
name: lib-${{ matrix.name }}
path: julia-staging/*
- uses: actions/upload-artifact@v4
with:
name: wheel-${{ matrix.name }}
path: python/dist/*.whl
linux-wheel:
name: linux-x86_64
runs-on: ubuntu-latest
container: quay.io/pypa/manylinux_2_28_x86_64
steps:
- uses: actions/checkout@v4
- name: Rust
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --profile minimal --default-toolchain stable
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Build the library
run: cargo build --release
- name: Build and repair the wheel
shell: bash
run: |
# The manylinux images carry their own Pythons; any of them builds the same py3-none
# wheel, since the package is ctypes rather than a C extension.
PY=/opt/python/cp312-cp312/bin/python
cp target/release/libferrotherm.so python/ferrotherm/libferrotherm.so
cd python
"$PY" -m pip install -q --upgrade pip build auditwheel setuptools wheel
# Not --no-isolation: the manylinux image's Python has no setuptools, so the backend has
# to come from the isolated build environment like it does on every other runner.
"$PY" -m build --wheel -C--build-option=--plat-name=linux_x86_64
"$PY" -m auditwheel repair dist/*.whl -w dist/ --plat manylinux_2_28_x86_64
rm -f dist/*-linux_x86_64.whl
ls -la dist/
- name: The wheel must work with nothing else present
shell: bash
run: |
PY=/opt/python/cp312-cp312/bin/python
"$PY" -m pip install -q python/dist/*.whl pytest
cp python/test_model.py /tmp/test_model.py
"$PY" - /tmp/test_model.py <<'PY'
import sys
p = sys.argv[1]
t = open(p).read().replace(
"sys.path.insert(0, str(Path(__file__).resolve().parent))\n", "")
open(p, "w").write(t)
PY
cd /tmp
unset FERROTHERM_LIB
"$PY" - <<'PY'
import ferrotherm, pathlib, sys
lib = pathlib.Path(ferrotherm.library_path()).resolve()
pkg = pathlib.Path(ferrotherm.__file__).resolve().parent
print("library:", lib)
if lib.parent != pkg:
sys.exit(f"not self-contained: loaded {lib} from outside the package")
PY
"$PY" -m pytest -q test_model.py
- uses: actions/upload-artifact@v4
with:
name: wheel-linux-x86_64
path: python/dist/*.whl
- uses: actions/upload-artifact@v4
with:
name: lib-linux-x86_64
path: target/release/libferrotherm.so
julia-artifacts:
needs: [wheels, linux-wheel]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: julia-actions/setup-julia@v2
with:
version: "1.12"
- uses: actions/download-artifact@v4
with:
pattern: lib-*
path: libs
merge-multiple: false
- name: Build the artifacts and their manifest
shell: bash
run: |
ls -R libs
VERSION=$(grep -m1 '^version' Cargo.toml | cut -d'"' -f2)
echo "FERROTHERM_VERSION=$VERSION" >> $GITHUB_ENV
BASE="https://github.com/${{ github.repository }}/releases/download/v$VERSION"
FERROTHERM_VERSION=$VERSION julia scripts/build-julia-artifacts.jl dist "$BASE" \
aarch64-apple-darwin=libs/lib-macos-arm64/libferrotherm.dylib \
x86_64-linux-gnu=libs/lib-linux-x86_64/libferrotherm.so \
x86_64-w64-mingw32=libs/lib-windows-x86_64/ferrotherm.dll
cp dist/Artifacts.toml julia/ferrotherm_jll/Artifacts.toml
ls -la dist/
- name: The JLL must load what it names
run: |
cd julia/ferrotherm_jll
# The URLs point at a release that does not exist until this run publishes one, so serve
# the tarballs locally and check the hashes and the load path against those.
python3 -m http.server 8791 --directory ../../dist &
sleep 2
sed -i "s|https://github.com/[^/]*/[^/]*/releases/download/v[^/]*|http://localhost:8791|" Artifacts.toml
julia --project=. -e '
using Pkg; Pkg.instantiate()
using ferrotherm_jll, Libdl
p = ferrotherm_jll.libferrotherm[]
isempty(p) && error("the JLL resolved no library on this platform")
h = Libdl.dlopen(p)
m = ccall(Libdl.dlsym(h, :ft_onsager), Cdouble, (Cdouble,), 0.5)
println("onsager(0.5) = ", m)
abs(m - 0.911319377877496) < 1e-12 || error("the artifact is not the library we built")
println("the JLL loads the library it names")'
git checkout Artifacts.toml 2>/dev/null || true
- uses: actions/upload-artifact@v4
with:
name: julia-artifacts
path: |
dist/*.tar.gz
dist/Artifacts.toml
publish:
needs: [wheels, linux-wheel]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v') || inputs.publish
environment: pypi
permissions:
id-token: write steps:
- uses: actions/download-artifact@v4
with:
pattern: wheel-*
path: dist
merge-multiple: true
- run: ls -la dist/
- uses: pypa/gh-action-pypi-publish@release/v1