Skip to content

Adopt dual licensing #15

Adopt dual licensing

Adopt dual licensing #15

name: Build and publish witwin-maxwell
on:
push:
branches:
- master
- "codex/**"
pull_request:
release:
types: [published]
workflow_dispatch:
jobs:
validate_release:
if: github.event_name != 'release' || (startsWith(github.event.release.tag_name, 'witwin-maxwell-v') && !github.event.release.prerelease)
name: Validate release
runs-on: ubuntu-latest
outputs:
package_dir: ${{ steps.package_dir.outputs.path }}
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Resolve package directory
id: package_dir
shell: bash
run: |
if [ -f pyproject.toml ]; then
echo "path=." >> "$GITHUB_OUTPUT"
elif [ -f maxwell/pyproject.toml ]; then
echo "path=maxwell" >> "$GITHUB_OUTPUT"
else
echo "Unable to find pyproject.toml for the witwin-maxwell package." >&2
exit 1
fi
- name: Validate release tag and package version
if: github.event_name == 'release'
shell: bash
env:
PACKAGE_DIR: ${{ steps.package_dir.outputs.path }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
python - <<'PY'
import os
import re
import sys
import tomllib
from pathlib import Path
tag = os.environ["RELEASE_TAG"]
match = re.fullmatch(r"witwin-maxwell-v(?P<version>[0-9A-Za-z.+_-]+)", tag)
if match is None:
print(f"Release tag '{tag}' must match 'witwin-maxwell-v<version>'.", file=sys.stderr)
raise SystemExit(1)
expected_version = match.group("version")
pyproject_path = Path(os.environ["PACKAGE_DIR"]) / "pyproject.toml"
data = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
project = data["project"]
package_name = project["name"]
package_version = project["version"]
if package_name != "witwin-maxwell":
print(f"Expected package name 'witwin-maxwell', found '{package_name}'.", file=sys.stderr)
raise SystemExit(1)
if package_version != expected_version:
print(
f"Release tag version '{expected_version}' does not match pyproject version '{package_version}'.",
file=sys.stderr,
)
raise SystemExit(1)
print(f"Validated {package_name} {package_version} from {pyproject_path}.")
PY
build_cuda_wheels:
name: Build stable CUDA wheel / ${{ matrix.os }}
needs: validate_release
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, windows-2022]
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Free disk space (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
echo "Disk before cleanup:"; df -h /
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android \
/usr/local/share/boost /opt/hostedtoolcache/CodeQL \
/usr/local/share/powershell /usr/share/swift || true
sudo docker image prune --all --force || true
echo "Disk after cleanup:"; df -h /
- name: Install CUDA Toolkit
uses: Jimver/cuda-toolkit@v0.2.35
with:
cuda: "12.8.1"
method: local
log-file-suffix: "${{ runner.os }}-stable-abi"
- name: Set up MSVC
if: runner.os == 'Windows'
uses: ilammy/msvc-dev-cmd@v1
with:
arch: x64
- name: Export Windows CUDA_HOME
if: runner.os == 'Windows'
shell: pwsh
run: |
$cudaRoot = $env:CUDA_PATH
if (-not $cudaRoot) {
$nvcc = (Get-Command nvcc -ErrorAction Stop).Source
$cudaRoot = Split-Path (Split-Path $nvcc -Parent) -Parent
}
"CUDA_HOME=$cudaRoot" >> $env:GITHUB_ENV
"CUDA_PATH=$cudaRoot" >> $env:GITHUB_ENV
"CUDA_ROOT=$cudaRoot" >> $env:GITHUB_ENV
"CUDA_BIN_PATH=$cudaRoot" >> $env:GITHUB_ENV
- name: Show toolchain
shell: bash
run: |
python --version
nvcc --version
command -v cl || true
- name: Install CUDA extension build dependencies
run: |
python -m pip install --upgrade pip setuptools wheel ninja numpy
python -m pip install torch==2.10.0 --index-url https://download.pytorch.org/whl/cu128
python -m pip install build
- name: Build packaged stable FDTD CUDA extension
shell: bash
env:
MAX_JOBS: "2"
WITWIN_CUDA_GENCODE_ARCHES: "7.0;7.5;8.0;8.6;8.9;9.0;10.0;10.1;12.0+PTX"
PACKAGE_DIR: ${{ needs.validate_release.outputs.package_dir }}
WITWIN_MAXWELL_FDTD_CUDA_BUILD_DIR: ${{ runner.temp }}/witwin_maxwell_fdtd_cuda/stable_abi_v1
run: |
python "$PACKAGE_DIR"/scripts/build_fdtd_cuda_prebuilt.py --verbose
- name: Verify compiled CUDA architectures
shell: bash
env:
PACKAGE_DIR: ${{ needs.validate_release.outputs.package_dir }}
run: |
python "$PACKAGE_DIR"/scripts/verify_cuda_binary_arches.py \
--stem witwin_maxwell_fdtd_cuda \
"$PACKAGE_DIR"/witwin/maxwell/fdtd/cuda/prebuilt
- name: Build platform wheel
shell: bash
env:
PACKAGE_DIR: ${{ needs.validate_release.outputs.package_dir }}
run: |
python -m build --wheel "$PACKAGE_DIR"
- name: Repair Linux wheel platform tag
if: runner.os == 'Linux'
shell: bash
env:
PACKAGE_DIR: ${{ needs.validate_release.outputs.package_dir }}
run: |
python -m pip install auditwheel patchelf
mkdir -p "$PACKAGE_DIR"/wheelhouse
for whl in "$PACKAGE_DIR"/dist/*-linux_x86_64.whl; do
python -m auditwheel repair "$whl" \
--plat manylinux_2_35_x86_64 \
--wheel-dir "$PACKAGE_DIR"/wheelhouse \
--exclude libc10.so --exclude libc10_cuda.so \
--exclude libtorch.so --exclude libtorch_cpu.so \
--exclude libtorch_cuda.so --exclude libtorch_python.so \
--exclude libcudart.so.12 --exclude libcuda.so.1
done
rm -f "$PACKAGE_DIR"/dist/*-linux_x86_64.whl
mv "$PACKAGE_DIR"/wheelhouse/*.whl "$PACKAGE_DIR"/dist/
ls -l "$PACKAGE_DIR"/dist
- name: Smoke install prebuilt wheel
shell: bash
env:
PACKAGE_DIR: ${{ needs.validate_release.outputs.package_dir }}
WITWIN_MAXWELL_FDTD_CUDA_PREBUILT: "1"
WITWIN_MAXWELL_FDTD_CUDA_BUILD_DIR: ${{ runner.temp }}/missing_prebuilt_build_dir
run: |
python -m pip uninstall -y witwin-maxwell || true
python -m pip install --no-deps "$PACKAGE_DIR"/dist/*.whl
python - <<'PY'
import importlib.util
import os
import site
import zipfile
from pathlib import Path
wheel = next((Path(os.environ["PACKAGE_DIR"]) / "dist").glob("*.whl"))
names = zipfile.ZipFile(wheel).namelist()
native = [name for name in names if "witwin_maxwell_fdtd_cuda" in name and name.endswith((".so", ".pyd"))]
assert len(native) == 1, native
assert "/prebuilt/" in f"/{native[0]}"
assert "torch_" not in native[0]
candidates = []
for root in site.getsitepackages() + [site.getusersitepackages()]:
candidates.append(Path(root) / "witwin" / "maxwell" / "fdtd" / "cuda" / "build.py")
build_path = next((path for path in candidates if path.exists()), None)
assert build_path is not None, f"installed build.py not found in {candidates}"
spec = importlib.util.spec_from_file_location("witwin_maxwell_fdtd_cuda_build", build_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
path = module.prebuilt_extension_path()
assert path.exists(), f"missing packaged prebuilt extension: {path}"
assert path.parent.name == "prebuilt"
extension = module.build_extension()
print(f"Loaded packaged FDTD CUDA extension from {extension.__file__}")
PY
- name: Upload wheel artifact
uses: actions/upload-artifact@v4
with:
name: wheel-${{ matrix.os }}
path: ${{ needs.validate_release.outputs.package_dir }}/dist/*.whl
if-no-files-found: error
test_torch_compatibility:
name: Stable ABI / ${{ matrix.os }} / py${{ matrix.compatibility.python_version }} / torch${{ matrix.compatibility.torch_version }}
needs:
- validate_release
- build_cuda_wheels
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, windows-2022]
compatibility:
- {python_version: "3.10", torch_version: "2.10.0", cuda_index: "cu128"}
- {python_version: "3.11", torch_version: "2.10.0", cuda_index: "cu128"}
- {python_version: "3.12", torch_version: "2.10.0", cuda_index: "cu128"}
- {python_version: "3.13", torch_version: "2.10.0", cuda_index: "cu128"}
- {python_version: "3.14", torch_version: "2.10.0", cuda_index: "cu128"}
- {python_version: "3.14", torch_version: "2.11.0", cuda_index: "cu128"}
- {python_version: "3.14", torch_version: "2.12.0", cuda_index: "cu126"}
steps:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.compatibility.python_version }}
- name: Download stable wheel
uses: actions/download-artifact@v4
with:
name: wheel-${{ matrix.os }}
path: dist
- name: Install compatibility environment
shell: bash
run: |
python -m pip install --upgrade pip
python -m pip install --no-cache-dir "torch==${{ matrix.compatibility.torch_version }}" \
--index-url "https://download.pytorch.org/whl/${{ matrix.compatibility.cuda_index }}"
python -m pip install --no-deps dist/*.whl
- name: Load the same stable binary
shell: bash
env:
EXPECTED_TORCH: ${{ matrix.compatibility.torch_version }}
WITWIN_MAXWELL_FDTD_CUDA_PREBUILT: "1"
WITWIN_MAXWELL_FDTD_CUDA_BUILD_DIR: ${{ runner.temp }}/must-not-jit-build
run: |
python - <<'PY'
import importlib.util
import os
import site
from pathlib import Path
import torch
assert torch.__version__.split("+", 1)[0] == os.environ["EXPECTED_TORCH"]
candidates = [
Path(root) / "witwin" / "maxwell" / "fdtd" / "cuda" / "build.py"
for root in site.getsitepackages() + [site.getusersitepackages()]
]
build_path = next((path for path in candidates if path.exists()), None)
assert build_path is not None, candidates
spec = importlib.util.spec_from_file_location("witwin_maxwell_fdtd_cuda_build", build_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
native_path = module.prebuilt_extension_path()
assert native_path.exists(), native_path
extension = module.build_extension()
assert Path(extension.__file__).resolve() == native_path.resolve()
assert extension.update_magnetic_hx_standard is not None
print(f"Loaded {native_path.name} with torch {torch.__version__} on Python {os.sys.version.split()[0]}")
PY
build_sdist:
name: Build source distribution
needs: validate_release
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Build distributions
shell: bash
env:
PACKAGE_DIR: ${{ needs.validate_release.outputs.package_dir }}
run: |
python -m pip install --upgrade pip build
python -m build --sdist "$PACKAGE_DIR"
- name: Upload source distribution artifact
uses: actions/upload-artifact@v4
with:
name: sdist
path: ${{ needs.validate_release.outputs.package_dir }}/dist/*.tar.gz
if-no-files-found: error
publish:
name: Publish to PyPI
if: github.event_name == 'release'
needs:
- build_cuda_wheels
- test_torch_compatibility
- build_sdist
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
environment:
name: pypi
url: https://pypi.org/project/witwin-maxwell/
steps:
- name: Download wheel artifacts
uses: actions/download-artifact@v4
with:
pattern: wheel-*
path: dist
merge-multiple: true
- name: Download source distribution artifact
uses: actions/download-artifact@v4
with:
name: sdist
path: dist
- name: Publish distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist
skip-existing: true