diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml new file mode 100644 index 0000000..02f125f --- /dev/null +++ b/.github/workflows/wheels.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e38899..1a26e3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 27f4816..28b84a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 287bb9a..8a908be 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -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 diff --git a/release/build_windows_wheel.ps1 b/release/build_windows_wheel.ps1 new file mode 100644 index 0000000..47731b4 --- /dev/null +++ b/release/build_windows_wheel.ps1 @@ -0,0 +1,162 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 Jae Hoon Seo, SMI Lab, Inha University +# Build the Windows x64 cabledyn wheel with the release toolchain: Intel IFX and the MSVC linker +# with the static runtime, and the pinned netlib reference LAPACK built here with IFX (the same +# pins as build_static_windows.ps1, kept equal by tests/test_packaging_metadata.py). The library +# and the driver in the wheel therefore need no runtime DLLs beyond Windows itself, which the +# script checks with dumpbin before it writes the wheel to OutputDir. +[CmdletBinding()] +param( + [string]$CableDynRoot = '', + [string]$OutputDir = '', + [string]$Python = 'python', + [string]$WindowsSdkVersion = '10.0.22621.0', + [int]$Jobs = 8, + [version]$MinimumIfxVersion = '2025.3', + [string]$OneApiSetvars = 'C:\Program Files (x86)\Intel\oneAPI\setvars.bat', + [string]$VcVarsAll = '', + # Local copy of the pinned reference LAPACK source archive; downloaded when empty. + [string]$ReferenceLapackArchive = '' +) +$ErrorActionPreference = 'Stop' +if (-not $CableDynRoot) { $CableDynRoot = Split-Path $PSScriptRoot -Parent } +$CableDynRoot = (Resolve-Path -LiteralPath $CableDynRoot).Path +if (-not $OutputDir) { $OutputDir = Join-Path $CableDynRoot 'build-wheel\dist' } +$OutputDir = [IO.Path]::GetFullPath($OutputDir) +$Python = (Get-Command $Python).Source +$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' +if (-not (Test-Path -LiteralPath $vswhere)) { throw "Visual Studio locator not found: $vswhere" } +$vsRoot = (& $vswhere -latest -products * -requires 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64' ` + -property installationPath | Select-Object -First 1) +if (-not $vsRoot) { throw 'Visual Studio with the x64 C++ toolchain was not found' } +if (-not $VcVarsAll) { $VcVarsAll = Join-Path $vsRoot 'VC\Auxiliary\Build\vcvarsall.bat' } +foreach ($required in @($OneApiSetvars, $VcVarsAll)) { + if (-not (Test-Path -LiteralPath $required)) { throw "required toolchain script not found: $required" } +} + +function Invoke-Toolchain([string]$Command) { + # Run one command in a cmd.exe with the IFX and MSVC environments loaded. + $bat = Join-Path $env:TEMP ("cabledyn-wheel-{0}.bat" -f [guid]::NewGuid().ToString('N')) + try { + @( + '@echo off' + "call `"$OneApiSetvars`" --force >nul" + 'if errorlevel 1 exit /b %errorlevel%' + "call `"$VcVarsAll`" x64 $WindowsSdkVersion >nul" + 'if errorlevel 1 exit /b %errorlevel%' + $Command + ) | Set-Content -LiteralPath $bat -Encoding ascii + & cmd.exe /d /c $bat + if ($LASTEXITCODE -ne 0) { throw "toolchain command failed ($LASTEXITCODE): $Command" } + } finally { + Remove-Item -LiteralPath $bat -Force -ErrorAction SilentlyContinue + } +} + +# The release compiler gate: the IFX generation of the official executables. +$probe = Join-Path $env:TEMP ("cabledyn-ifx-{0}.txt" -f [guid]::NewGuid().ToString('N')) +try { + Invoke-Toolchain "ifx /QV > `"$probe`" 2>&1" + $ifxText = Get-Content -LiteralPath $probe -Raw +} finally { + Remove-Item -LiteralPath $probe -Force -ErrorAction SilentlyContinue +} +if ($ifxText -notmatch 'Version\s+(\d+\.\d+(?:\.\d+)?)') { throw "could not determine the IFX version from:`n$ifxText" } +if ([version]$Matches[1] -lt $MinimumIfxVersion) { + throw "IFX $($Matches[1]) is below the release minimum $MinimumIfxVersion" +} +Write-Host "PASS compiler gate: IFX $($Matches[1])" + +$common = '-G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_Fortran_COMPILER=ifx ' + + '-DCMAKE_C_COMPILER=cl -DCMAKE_LINKER=link -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded' + +# The netlib reference LAPACK/BLAS, static, built with IFX (pins shared with +# build_static_windows.ps1). +$lapackVersion = '3.12.1' +$lapackSha256 = '2ca6407a001a474d4d4d35f3a61550156050c48016d949f0da0529c0aa052422' +$lapackUrl = "https://github.com/Reference-LAPACK/lapack/archive/refs/tags/v$lapackVersion.tar.gz" +$lapackRoot = Join-Path $CableDynRoot 'build-wheel\reference-lapack' +New-Item -ItemType Directory -Path $lapackRoot -Force | Out-Null +if (-not $ReferenceLapackArchive) { + $ReferenceLapackArchive = Join-Path $lapackRoot "lapack-$lapackVersion.tar.gz" + if (-not (Test-Path -LiteralPath $ReferenceLapackArchive)) { + Invoke-WebRequest -UseBasicParsing -Uri $lapackUrl -OutFile $ReferenceLapackArchive + } +} +$archiveHash = (Get-FileHash -LiteralPath $ReferenceLapackArchive -Algorithm SHA256).Hash.ToLowerInvariant() +if ($archiveHash -ne $lapackSha256) { + throw "reference LAPACK archive $ReferenceLapackArchive has SHA256 $archiveHash; expected $lapackSha256" +} +$lapackSource = Join-Path $lapackRoot "lapack-$lapackVersion" +$lapackBuild = Join-Path $lapackRoot 'build' +$lapackInstall = Join-Path $lapackRoot 'install' +foreach ($stale in @($lapackSource, $lapackBuild, $lapackInstall)) { + if (Test-Path -LiteralPath $stale) { Remove-Item -LiteralPath $stale -Recurse -Force } +} +& "$env:SystemRoot\System32\tar.exe" -xzf $ReferenceLapackArchive -C $lapackRoot +if ($LASTEXITCODE -ne 0) { throw "could not extract $ReferenceLapackArchive" } +Invoke-Toolchain ("cmake -S `"$lapackSource`" -B `"$lapackBuild`" $common " + + '-DBUILD_SHARED_LIBS=OFF -DBUILD_TESTING=OFF -DBUILD_SINGLE=OFF -DBUILD_DOUBLE=ON ' + + '-DBUILD_COMPLEX=OFF -DBUILD_COMPLEX16=OFF -DBUILD_INDEX64_EXT_API=OFF ' + + '-DUSE_OPTIMIZED_BLAS=OFF -DUSE_OPTIMIZED_LAPACK=OFF ' + + '"-DCMAKE_Fortran_FLAGS=/nologo /libs:static /fp:precise /arch:CORE-AVX2" ' + + "`"-DCMAKE_INSTALL_PREFIX=$lapackInstall`"") +Invoke-Toolchain "cmake --build `"$lapackBuild`" --target blas lapack -j $Jobs" +Invoke-Toolchain "cmake --install `"$lapackBuild`"" +$lapackLibraries = @('liblapack.lib', 'libblas.lib') | ForEach-Object { + $library = Join-Path $lapackInstall "lib\$_" + if (-not (Test-Path -LiteralPath $library)) { throw "reference LAPACK build did not produce $library" } + $library -replace '\\', '/' +} +Write-Host "PASS reference LAPACK $lapackVersion built with IFX" + +# The wheel: scikit-build-core with the release configuration of CableDyn. +New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null +$settings = @( + 'cmake.args=-GNinja', + 'cmake.define.CMAKE_Fortran_COMPILER=ifx', + 'cmake.define.CMAKE_C_COMPILER=cl', + 'cmake.define.CMAKE_LINKER=link', + 'cmake.define.CMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded', + 'cmake.define.CABLEDYN_STATIC_WINDOWS_RELEASE=ON', + "cmake.define.CABLEDYN_STATIC_LAPACK_LIBRARIES=$($lapackLibraries -join ';')" +) | ForEach-Object { "`"--config-settings=$_`"" } +Invoke-Toolchain ("`"$Python`" -m pip wheel `"$CableDynRoot`" --no-deps -w `"$OutputDir`" " + ($settings -join ' ')) +$wheel = Get-ChildItem -LiteralPath $OutputDir -Filter 'cabledyn-*-win_amd64.whl' | + Sort-Object LastWriteTime | Select-Object -Last 1 +if (-not $wheel) { throw "no win_amd64 wheel in $OutputDir" } + +# Only Windows system DLLs may be imported by the library and the driver. +$inspect = Join-Path $env:TEMP ("cabledyn-wheel-{0}" -f [guid]::NewGuid().ToString('N')) +try { + # A wheel is a ZIP archive, but Windows PowerShell 5.1's Expand-Archive refuses any + # extension other than .zip; the .NET extractor reads it by content. + Add-Type -AssemblyName System.IO.Compression.FileSystem + [IO.Compression.ZipFile]::ExtractToDirectory($wheel.FullName, $inspect) + # Filter on the extension explicitly: with -LiteralPath, Windows PowerShell 5.1 ignores + # -Include and would list every file of the package. + $binaries = @(Get-ChildItem -LiteralPath (Join-Path $inspect 'cabledyn') -Recurse -File | + Where-Object { $_.Extension -in @('.dll', '.exe') }) + $libraries = @($binaries | Where-Object { $_.Extension -eq '.dll' }) + $drivers = @($binaries | Where-Object { $_.Extension -eq '.exe' -and $_.Directory.Name -eq 'bin' }) + if ($libraries.Count -ne 1 -or $drivers.Count -ne 1 -or $binaries.Count -ne 2) { + throw "the wheel carries $($binaries.Count) binaries ($($binaries.Name -join ', ')); " + + 'expected the library beside the package and the driver in cabledyn/bin' + } + foreach ($binary in $binaries) { + $listing = Join-Path $env:TEMP ("cabledyn-deps-{0}.txt" -f [guid]::NewGuid().ToString('N')) + Invoke-Toolchain "dumpbin /nologo /dependents `"$($binary.FullName)`" > `"$listing`"" + $imports = Get-Content -LiteralPath $listing | Where-Object { $_ -match '^\s+\S+\.dll\s*$' } | + ForEach-Object { $_.Trim() } + Remove-Item -LiteralPath $listing -Force + $foreign = $imports | Where-Object { + $_ -notmatch '^(KERNEL32|ADVAPI32|USER32|SHELL32|OLEAUT32|ole32|WS2_32|ntdll|bcrypt|imagehlp|dbghelp|PSAPI|VERSION)\.dll$' -and + $_ -notmatch '^api-ms-win-' + } + if ($foreign) { throw "$($binary.Name) imports non-system DLLs: $($foreign -join ', ')" } + Write-Host "PASS $($binary.Name) imports only system DLLs: $($imports -join ', ')" + } +} finally { + if (Test-Path -LiteralPath $inspect) { Remove-Item -LiteralPath $inspect -Recurse -Force } +} +Write-Host "PASS $($wheel.Name)" diff --git a/release/smoke_test_wheel.py b/release/smoke_test_wheel.py new file mode 100644 index 0000000..f3a2073 --- /dev/null +++ b/release/smoke_test_wheel.py @@ -0,0 +1,73 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 Jae Hoon Seo, SMI Lab, Inha University +"""Smoke-test an installed cabledyn platform wheel, outside the source tree. + +The installed package must load the CableDyn library it carries (not one from a build tree or +CABLEDYN_LIBRARY), select the driver it carries (not one on PATH), and run a standalone deck to +completion. With --expect-build, the driver's --version must report that build ("x86-64-v3" +for the x86-64 release wheels, "baseline" otherwise). Usage: + + python release/smoke_test_wheel.py [--expect-build x86-64-v3|baseline] +""" + +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +import tempfile +from pathlib import Path + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("deck", type=Path) + parser.add_argument("--expect-build", choices=["x86-64-v3", "baseline"]) + args = parser.parse_args() + deck = args.deck.resolve() + for name in ("CABLEDYN_LIBRARY", "CABLEDYN_DRIVER"): + os.environ.pop(name, None) + + import cabledyn + from cabledyn import _discovery, driver + + package = Path(cabledyn.__file__).resolve().parent + library = _discovery.find_library() + if library.parent != package: + print(f"FAIL: the library {library} is not the one in the package {package}", file=sys.stderr) + return 1 + print(f"library : {library}") + print(f"version : {cabledyn.version_string()}") + + executable = driver._resolve_executable(None) + if executable.parent != package / "bin": + print(f"FAIL: the driver {executable} is not the one in {package / 'bin'}", file=sys.stderr) + return 1 + print(f"driver : {executable}") + + if args.expect_build: + banner = subprocess.run([str(executable), "--version"], capture_output=True, text=True, check=False) + build = [line.split(None, 1)[1].strip() for line in banner.stdout.splitlines() if line.split()[:1] == ["Build"]] + if banner.returncode != 0 or not build or not build[0].startswith(args.expect_build): + print( + f"FAIL: the driver reports build {build} (exit {banner.returncode}); expected {args.expect_build}", + file=sys.stderr, + ) + return 1 + print(f"build : {build[0]}") + + with tempfile.TemporaryDirectory() as work: + root = Path(work) / deck.stem + result = driver.CableDynDriver().run(deck, output_root=root) + outputs = sorted(p.name for p in Path(work).iterdir()) + if result is None or not any(name.endswith(".out") for name in outputs): + print(f"FAIL: the driver run wrote no .out file ({outputs})", file=sys.stderr) + return 1 + print(f"deck : {deck.name} ran; outputs {outputs}") + print("PASS: the installed wheel carries a working library and driver") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_packaging_metadata.py b/tests/test_packaging_metadata.py index 049ec46..44cbf9f 100644 --- a/tests/test_packaging_metadata.py +++ b/tests/test_packaging_metadata.py @@ -6,6 +6,7 @@ from __future__ import annotations +import re import unittest from pathlib import Path @@ -61,5 +62,32 @@ def test_the_wheel_classifiers_differ_only_in_the_platforms(self): ) +def _lapack_pins(script: Path) -> dict: + """The reference LAPACK version, archive checksum and Fortran flags a build script pins.""" + text = script.read_text(encoding="utf-8") + pins = {} + for key, pattern in ( + ("version", r"\$lapackVersion = '([^']+)'"), + ("sha256", r"\$lapackSha256 = '([0-9a-f]{64})'"), + ("flags", r'"-DCMAKE_Fortran_FLAGS=([^"]+)"'), + ): + found = re.findall(pattern, text) + if len(found) != 1: + raise AssertionError(f"{script.name}: expected one {key} pin, found {found}") + pins[key] = found[0] + return pins + + +class WindowsWheelLapackTest(unittest.TestCase): + """The Windows wheel links the same reference LAPACK as the static release executables: + one version, one archive checksum, one set of Fortran flags (x86-64-v3 included).""" + + def test_the_wheel_and_the_release_pin_the_same_lapack(self): + release = _lapack_pins(ROOT / "release" / "build_static_windows.ps1") + wheel = _lapack_pins(ROOT / "release" / "build_windows_wheel.ps1") + self.assertEqual(wheel, release) + self.assertIn("/arch:CORE-AVX2", release["flags"]) + + if __name__ == "__main__": unittest.main()