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
191 changes: 191 additions & 0 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2026 Jae Hoon Seo, SMI Lab, Inha University
#
# The published Python distribution: an sdist and one py3-none platform wheel each for Linux
# x86-64 (manylinux_2_28), macOS arm64 and Windows x86-64, every wheel carrying the CableDyn
# library and the standalone driver. The x86-64 wheels target x86-64-v3 (AVX2), as the static
# Windows release does. Each wheel is installed into a clean environment and
# smoke-tested (release/smoke_test_wheel.py) on its own platform. The distributions are workflow
# artifacts; publishing them is a separate, explicit step.
name: wheels

on:
workflow_dispatch:
release:
types: [published]
pull_request:
paths:
- pyproject.toml
- CMakeLists.txt
- python/cabledyn/**
- release/build_windows_wheel.ps1
- release/smoke_test_wheel.py
- .github/workflows/wheels.yml

permissions:
contents: read

jobs:
sdist:
runs-on: ubuntu-latest
steps:
- name: Check out CableDyn
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.13'

- name: Build and check the sdist
run: |
python -m pip install --upgrade pip build twine
export SOURCE_DATE_EPOCH="$(git log -1 --format=%ct)"
python -m build --sdist --outdir dist .
python -m twine check dist/*

- name: Upload the sdist
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cabledyn-sdist
path: dist/*.tar.gz
if-no-files-found: error

linux:
runs-on: ubuntu-latest
steps:
- name: Check out CableDyn
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Build, repair (auditwheel) and test the manylinux wheel
uses: pypa/cibuildwheel@8c2dcd2e96041a619f5ff03c9638d7941e2553a4 # v4.3.3
env:
# one py3-none wheel serves every Python; build it once
CIBW_BUILD: cp312-manylinux_x86_64
CIBW_ARCHS_LINUX: x86_64
CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28
# gfortran comes with the image's GCC toolset; OpenBLAS supplies LAPACK and is bundled
# into the wheel by auditwheel together with the Fortran runtime
CIBW_BEFORE_ALL_LINUX: >-
dnf install -y epel-release && dnf install -y openblas-devel
# x86-64-v3, as the Windows release (CABLEDYN_TARGET_AVX2): the wheel's library and
# driver stop with a message on a processor without AVX2
CIBW_ENVIRONMENT_LINUX: SKBUILD_CMAKE_DEFINE="CABLEDYN_TARGET_AVX2=ON"
CIBW_TEST_COMMAND: >-
python {project}/release/smoke_test_wheel.py {project}/examples/dynamic_chain_current.dat --expect-build x86-64-v3
with:
output-dir: wheelhouse

- name: Upload the Linux wheel
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cabledyn-wheel-linux-x86_64
path: wheelhouse/*.whl
if-no-files-found: error

macos:
runs-on: macos-14
steps:
- name: Check out CableDyn
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Build, repair (delocate) and test the macOS wheel
uses: pypa/cibuildwheel@8c2dcd2e96041a619f5ff03c9638d7941e2553a4 # v4.3.3
env:
CIBW_BUILD: cp312-macosx_arm64
CIBW_ARCHS_MACOS: arm64
# Homebrew gfortran and OpenBLAS; delocate bundles them with the Fortran runtime. The
# deployment target is the runner's macOS, the oldest the Homebrew libraries support.
CIBW_BEFORE_ALL_MACOS: brew install gcc openblas
CIBW_ENVIRONMENT_MACOS: >-
MACOSX_DEPLOYMENT_TARGET=14.0
FC=gfortran
CMAKE_PREFIX_PATH=$(brew --prefix openblas)
CIBW_TEST_COMMAND: >-
python {project}/release/smoke_test_wheel.py {project}/examples/dynamic_chain_current.dat --expect-build baseline
with:
output-dir: wheelhouse

- name: Upload the macOS wheel
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cabledyn-wheel-macos-arm64
path: wheelhouse/*.whl
if-no-files-found: error

windows:
timeout-minutes: 180
runs-on: windows-2022
steps:
- name: Check out CableDyn
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.13'

- name: Install Intel Fortran Essentials
# the release compiler, as in release-windows-static.yml
shell: powershell
env:
URL: https://registrationcenter-download.intel.com/akdlm/IRC_NAS/33676fcf-14a3-4e96-a9b9-72976b1145d9/intel-fortran-essentials-2025.3.1.25_offline.exe
run: |
$installer = Join-Path $env:RUNNER_TEMP 'intel-fortran-essentials-2025.3.1.25.exe'
curl.exe --fail --location --output $installer --url $env:URL --retry 5 --retry-delay 5
if ($LASTEXITCODE -ne 0) { throw "Intel Fortran download failed with exit $LASTEXITCODE" }
if ((Get-Item -LiteralPath $installer).Length -ne 880951016) {
throw 'Intel Fortran installer length differs from the pinned release payload'
}
$signature = Get-AuthenticodeSignature -LiteralPath $installer
if ($signature.Status -ne 'Valid' -or
$signature.SignerCertificate.Subject -notmatch '(^|, )O=Intel Corporation(,|$)') {
throw "Intel Fortran installer signature is not a valid Intel Corporation signature: $($signature.Status)"
}
$arguments = @(
'-s', '-a', '--silent', '--eula', 'accept',
'-p=NEED_VS2017_INTEGRATION=0',
'-p=NEED_VS2019_INTEGRATION=0',
'-p=NEED_VS2022_INTEGRATION=1',
'--log-dir=.'
)
$process = Start-Process -FilePath $installer -ArgumentList $arguments -Wait -PassThru
if ($process.ExitCode -notin @(0, 3010)) {
throw "Intel Fortran installer failed with exit $($process.ExitCode)"
}

- name: Build the static Windows wheel
shell: powershell
run: .\release\build_windows_wheel.ps1 -OutputDir "$PWD\wheelhouse" -Jobs 4

- name: Install the wheel into a clean environment and smoke-test it
shell: powershell
run: |
$venv = Join-Path $env:RUNNER_TEMP 'wheel-test'
python -m venv $venv
& "$venv\Scripts\python.exe" -m pip install --upgrade pip
$wheel = (Get-ChildItem wheelhouse\cabledyn-*-win_amd64.whl).FullName
& "$venv\Scripts\python.exe" -m pip install $wheel
Push-Location $env:RUNNER_TEMP
try {
& "$venv\Scripts\python.exe" "$env:GITHUB_WORKSPACE\release\smoke_test_wheel.py" `
"$env:GITHUB_WORKSPACE\examples\dynamic_chain_current.dat" --expect-build x86-64-v3
if ($LASTEXITCODE -ne 0) { throw "smoke test failed ($LASTEXITCODE)" }
} finally {
Pop-Location
}

- name: Upload the Windows wheel
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cabledyn-wheel-windows-x86_64
path: wheelhouse/*.whl
if-no-files-found: error
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ All notable changes to CableDyn are recorded in this file. The format follows
inside the `cabledyn` package, so the package needs no separate library; the driver search
takes the bundled driver after `CABLEDYN_DRIVER` and before `PATH`. `python/pyproject.toml`
remains the pure package for development against a build tree.
- Release wheels built in CI (`.github/workflows/wheels.yml`): Linux x86-64 (`manylinux_2_28`,
x86-64-v3), macOS arm64 and Windows x86-64 (IFX, static, the release's reference LAPACK), plus
the sdist. Each wheel is installed into a clean environment and smoke-tested
(`release/smoke_test_wheel.py`); publishing is a separate step.

### Changed

Expand Down
5 changes: 4 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,11 @@ if(CABLEDYN_STATIC_WINDOWS_RELEASE)
# driver below, or the OpenFAST Release|x64 project, which enables whole-program
# optimisation).
string(APPEND CMAKE_Fortran_FLAGS " /libs:static /heap-arrays:1024 /Qipo")
# CMake does not pass the compile flags to an IFX executable link on Windows.
# CMake does not pass the compile flags to an IFX executable link on Windows. The shared
# library (the Windows wheel's cabledyn.dll) links the same IPO objects, so it needs the
# flag as well: without it IFX hands the intermediate objects to link.exe (LNK1107).
string(APPEND CMAKE_EXE_LINKER_FLAGS " /Qipo")
string(APPEND CMAKE_SHARED_LINKER_FLAGS " /Qipo")
# LAPACK/BLAS: the netlib reference implementation compiled with this IFX
# (release/build_static_windows.ps1 builds it), not MKL. Every CableDyn system is
# banded with a small bandwidth, and MKL's sequential DGBTRF/DGBTRS/DTBSV spend
Expand Down
20 changes: 20 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,26 @@ python -m pip wheel . --no-deps --no-build-isolation -w dist `
A wheel built this way still needs the toolchain's runtime and LAPACK libraries on the
search path; the release wheels bundle them (or link them statically on Windows).

The release wheels come from `.github/workflows/wheels.yml` (manual runs, published
releases, and pull requests that touch the packaging), as workflow artifacts; publishing
them is a separate step. It builds the sdist (checked with `twine`) and three wheels:

| Wheel | Toolchain | Runtime and LAPACK | Build |
|---|---|---|---|
| Linux x86-64 (`manylinux_2_28`) | cibuildwheel, image GCC toolset | OpenBLAS and the Fortran runtime bundled by auditwheel | x86-64-v3 |
| macOS arm64 (14.0+) | cibuildwheel, Homebrew gfortran | OpenBLAS and the Fortran runtime bundled by delocate | baseline |
| Windows x86-64 | `release/build_windows_wheel.ps1`: IFX and MSVC, static runtime | the release's reference LAPACK, linked statically | x86-64-v3 |

The Windows wheel uses the static release configuration and the same pinned reference
LAPACK as `release/build_static_windows.ps1` (`tests/test_packaging_metadata.py` keeps
the version, checksum and flags equal), and the script checks with `dumpbin` that the
library and the driver import only Windows system DLLs. Each wheel is installed into a
clean environment and checked by `release/smoke_test_wheel.py`: the package loads the
library it carries, selects the driver it carries, the driver reports the expected build
(`--expect-build`), and a standalone deck runs. On a processor without AVX2 the x86-64
wheels' driver stops with the start-up message, and so does a Python process when it
loads the library.

On Windows, run Python from the activated `cabledyn` environment and point
`CABLEDYN_LIBRARY` at the DLL. Do not prepend compiler or OpenBLAS runtime
directories to `PATH` before importing NumPy: mixed runtime DLLs can terminate
Expand Down
Loading
Loading