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
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- 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
env:
JULIA_DEPOT_PATH: ${{ runner.temp }}/julia-depot-cold
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[]
if isempty(p)
# Print the reason rather than only the symptom. The first run of this said "no
# library on this platform" and nothing about why, which is a dead end in a log.
println("Artifacts.toml as tested:")
println(read("Artifacts.toml", String))
error("the JLL resolved no library. Julia said:\n" * ferrotherm_jll.why())
end
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
- name: Attach the artifacts to the release the manifest names
if: startsWith(github.ref, 'refs/tags/v')
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${GITHUB_REF_NAME}"
gh release view "$TAG" >/dev/null 2>&1 \
|| gh release create "$TAG" --title "$TAG" --notes "See CHANGELOG.md."
gh release upload "$TAG" dist/*.tar.gz --clobber
- name: The published URLs resolve, and the bytes are the ones we hashed
if: startsWith(github.ref, 'refs/tags/v')
run: |
bad=0
n=0
while read -r url; do
n=$((n + 1))
sha="$(grep -B3 "url = \"$url\"" dist/Artifacts.toml | grep -m1 sha256 | cut -d'"' -f2)"
code="$(curl -sL -o /tmp/a.tar.gz -w '%{http_code}' "$url")"
got="$(sha256sum /tmp/a.tar.gz | cut -d' ' -f1)"
if [[ "$code" == "200" && "$got" == "$sha" ]]; then
echo " ok $url"
else
echo " BAD $url (http $code, sha $got, manifest $sha)"
bad=1
fi
done < <(grep -oE 'url = "[^"]+"' dist/Artifacts.toml | cut -d'"' -f2)
# A floor: no URLs found means this passed over nothing.
[[ $n -ge 3 ]] || { echo "found $n URLs in the manifest, expected 3" >&2; exit 2; }
[[ $bad -eq 0 ]] || { echo "the shipped manifest names URLs a user cannot fetch" >&2; exit 1; }
- name: Commit the manifest the release was built from
if: startsWith(github.ref, 'refs/tags/v')
run: |
cp dist/Artifacts.toml julia/ferrotherm_jll/Artifacts.toml
if git diff --quiet julia/ferrotherm_jll/Artifacts.toml; then
echo "manifest unchanged"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add julia/ferrotherm_jll/Artifacts.toml
git commit -m "ferrotherm_jll: the manifest for ${GITHUB_REF_NAME}" \
-m "The hashes and URLs come from the release this tag published, so the package names bytes that exist at the address it names them at."
git push origin HEAD:main
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
with:
skip-existing: true