Skip to content

Commit bdfef0f

Browse files
Merge pull request #1077 from AVSLab/feature/bsk-xxxx-build-wheels
Build and Publish wheels across Python 3.9–3.11 for Linux, macOS, and Windows
2 parents 3c5a99c + 15ea56a commit bdfef0f

14 files changed

Lines changed: 337 additions & 77 deletions

File tree

.github/actions/build/action.yml

Lines changed: 2 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -30,47 +30,8 @@ runs:
3030
requirements_doc.txt
3131
requirements.txt
3232
33-
- name: Install Linux System Deps.
34-
if: runner.os == 'Linux'
35-
shell: bash
36-
run: |
37-
sudo apt-get update
38-
sudo apt-get install -y build-essential python3-setuptools python3-tk libgtk2.0
39-
40-
- name: SWIG Install (Linux)
41-
if: runner.os == 'Linux'
42-
uses: mmomtchev/setup-swig@v4
43-
with:
44-
version: v4.2.1
45-
46-
- name: SWIG Install (macOS)
47-
if: runner.os == 'macOS'
48-
shell: bash
49-
env:
50-
HOMEBREW_NO_AUTO_UPDATE: 1
51-
HOMEBREW_NO_INSTALL_UPGRADE: 1
52-
HOMEBREW_NO_ANALYTICS: 1
53-
run: brew install swig || true
54-
55-
- name: SWIG Install (Windows)
56-
if: runner.os == 'Windows'
57-
shell: pwsh
58-
run: |
59-
$swigDir = "C:\Program Files\SWIG"
60-
if (!(Test-Path $swigDir)) {New-Item -ItemType Directory -Path $swigDir | Out-Null}
61-
$swigZip = "$swigDir\swigwin-4.2.1.zip"
62-
$swigUrl = "https://sourceforge.net/projects/swig/files/swigwin/swigwin-4.2.1/swigwin-4.2.1.zip/download"
63-
Start-Process -NoNewWindow -Wait -FilePath "curl.exe" -ArgumentList "-L -o `"$swigZip`" `"$swigUrl`""
64-
if (!(Test-Path $swigZip) -or ((Get-Item $swigZip).Length -lt 500KB)) { Write-Host "Download failed or file is corrupted." }
65-
Expand-Archive -Path $swigZip -DestinationPath $swigDir -Force
66-
67-
- name: "Add Basilisk and SWIG paths"
68-
if: runner.os == 'Windows'
69-
shell: pwsh
70-
run: |
71-
$oldpath = (Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH).path
72-
$newPath = “C:\Program Files\SWIG\swigwin-4.2.1;$oldpath;${{ env.GITHUB_WORKSPACE }}\dist3\Basilisk”
73-
echo "PATH=$newPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
33+
- name: Setup system dependencies
34+
uses: ./.github/actions/setup
7435

7536
- name: Install requirements
7637
shell: bash

.github/actions/setup/action.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: setup
2+
description: >
3+
Sets up the required system dependencies to perform a Basilisk build
4+
5+
runs:
6+
using: "composite"
7+
steps:
8+
- name: Install Linux System Deps.
9+
if: runner.os == 'Linux'
10+
shell: bash
11+
run: |
12+
sudo apt-get update
13+
sudo apt-get install -y build-essential python3-setuptools python3-tk libgtk2.0
14+
15+
- name: SWIG Install (Linux)
16+
if: runner.os == 'Linux'
17+
uses: mmomtchev/setup-swig@v4
18+
with:
19+
version: v4.2.1
20+
21+
- name: SWIG Install (macOS)
22+
if: runner.os == 'macOS'
23+
shell: bash
24+
env:
25+
HOMEBREW_NO_AUTO_UPDATE: 1
26+
HOMEBREW_NO_INSTALL_UPGRADE: 1
27+
HOMEBREW_NO_ANALYTICS: 1
28+
run: brew install swig || true
29+
30+
- name: SWIG Install (Windows)
31+
if: runner.os == 'Windows'
32+
shell: pwsh
33+
run: |
34+
$swigDir = "C:\Program Files\SWIG"
35+
if (!(Test-Path $swigDir)) {New-Item -ItemType Directory -Path $swigDir | Out-Null}
36+
$swigZip = "$swigDir\swigwin-4.2.1.zip"
37+
$swigUrl = "https://sourceforge.net/projects/swig/files/swigwin/swigwin-4.2.1/swigwin-4.2.1.zip/download"
38+
Start-Process -NoNewWindow -Wait -FilePath "curl.exe" -ArgumentList "-L -o `"$swigZip`" `"$swigUrl`""
39+
if (!(Test-Path $swigZip) -or ((Get-Item $swigZip).Length -lt 500KB)) { Write-Host "Download failed or file is corrupted." }
40+
Expand-Archive -Path $swigZip -DestinationPath $swigDir -Force
41+
42+
- name: "Add Basilisk and SWIG paths"
43+
if: runner.os == 'Windows'
44+
shell: pwsh
45+
run: |
46+
$oldpath = (Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH).path
47+
$newPath = “C:\Program Files\SWIG\swigwin-4.2.1;$oldpath;${{ env.GITHUB_WORKSPACE }}\dist3\Basilisk”
48+
echo "PATH=$newPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append

.github/workflows/merge.yml

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ jobs:
2626
token: ${{ secrets.BOT_ACCESS_TOKEN }}
2727

2828
- name: Bump version
29+
id: bump
2930
run: ./.github/workflows/version-bumper.sh ./docs/source/bskVersion.txt
3031

3132
- name: Commit and push
@@ -35,35 +36,12 @@ jobs:
3536
git commit -a -m "[AUTO] Bump version number" || echo "No changes"
3637
git push || true
3738
38-
build-ubuntu-latest-wheels:
39-
name: Build ubuntu-latest wheels
40-
needs: bump_version
41-
# Allow for manual runs to generate new wheels
42-
if: ${{ always() }}
43-
runs-on: ubuntu-latest
44-
strategy:
45-
matrix:
46-
python-version: ["3.9", "3.10", "3.11"]
47-
steps:
48-
- name: Checkout code
49-
uses: actions/checkout@v4
50-
51-
# The 'Build wheel' step will perform the actual build. However, we want
52-
# all pre build setup to still be performed
53-
- uses: ./.github/actions/build
54-
with:
55-
python-version: ${{ matrix.python-version }}
56-
extra-apt: "cmake"
57-
skip-build: true
58-
59-
- name: Build wheel
39+
- name: Create tag
40+
if: ${{ steps.bump.outputs.updated_version != '' }}
6041
run: |
61-
python -m pip wheel . -v --wheel-dir /tmp/wheelhouse
62-
63-
- uses: actions/upload-artifact@v4
64-
with:
65-
name: basilisk-wheels_ubuntu-22.04_python${{ matrix['python-version'] }}
66-
path: /tmp/wheelhouse/**/*asilisk*.whl
42+
git tag -a "v${{ steps.bump.outputs.updated_version }}" \
43+
-m "Release v${{ steps.bump.outputs.updated_version }}"
44+
git push --tags
6745
6846
build_documentation:
6947
name: macOS Docs Deployment
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
name: Publish Wheels
2+
3+
on:
4+
push:
5+
tags:
6+
- "v[0-9]*"
7+
- "test*"
8+
9+
jobs:
10+
build-wheels:
11+
name: Build Basilisk Wheels
12+
runs-on: ${{ matrix.os }}
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
os:
17+
- macos-13 # x86_64
18+
- macos-latest # ARM64
19+
- ubuntu-latest # x86_64
20+
- ubuntu-22.04-arm # ARM64
21+
- windows-latest # x86_64
22+
23+
steps:
24+
- name: Checkout Code
25+
uses: actions/checkout@v5
26+
with:
27+
ref: ${{ github.ref }}
28+
# Workaround for macos-13 (Intel). Ensures the conan build uses the right
29+
# SSL cert.
30+
- name: Set CA bundle (macos-13 only)
31+
if: ${{ matrix.os == 'macos-13' }}
32+
shell: bash
33+
run: |
34+
python -m pip install -U certifi
35+
CA="$(python -c 'import certifi; print(certifi.where())')"
36+
{
37+
echo "REQUESTS_CA_BUNDLE=$CA" # Python requests / Conan
38+
echo "PIP_CERT=$CA" # pip
39+
echo "SSL_CERT_FILE=$CA" # OpenSSL consumers
40+
echo "CURL_CA_BUNDLE=$CA" # curl / CMake file(DOWNLOAD)
41+
echo "CMAKE_TLS_CAINFO=$CA" # CMake
42+
} >> "$GITHUB_ENV"
43+
echo "Using CA: $CA"
44+
45+
- name: Setup system dependencies
46+
uses: ./.github/actions/setup
47+
48+
- name: Build wheels
49+
uses: pypa/cibuildwheel@v3.1.4
50+
env:
51+
CONAN_ARGS: "--opNav True --mujoco True --mujocoReplay True --recorderPropertyRollback True"
52+
CIBW_TEST_REQUIRES_WINDOWS: "numpy>=2.1"
53+
54+
- name: Upload wheels
55+
uses: actions/upload-artifact@v4
56+
with:
57+
name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
58+
path: ./wheelhouse/*.whl
59+
60+
make_sdist:
61+
name: Make SDist
62+
runs-on: ubuntu-latest
63+
steps:
64+
- uses: actions/checkout@v5
65+
with:
66+
ref: ${{ github.ref }}
67+
68+
- uses: actions/setup-python@v5
69+
with:
70+
python-version: 3.13
71+
72+
- name: Build SDist
73+
run: pipx run build --sdist
74+
75+
- uses: actions/upload-artifact@v4
76+
with:
77+
name: cibw-sdist
78+
path: dist/*.tar.gz
79+
80+
publish:
81+
name: Publish to PyPI
82+
needs: [build-wheels, make_sdist]
83+
runs-on: ubuntu-latest
84+
permissions:
85+
id-token: write
86+
contents: read
87+
steps:
88+
- name: Checkout tagged source
89+
uses: actions/checkout@v5
90+
with:
91+
ref: ${{ github.ref }}
92+
fetch-depth: 0
93+
94+
- name: Download wheels
95+
uses: actions/download-artifact@v4
96+
with:
97+
pattern: cibw-wheels-*
98+
merge-multiple: true
99+
path: dist
100+
101+
- name: Download sdist
102+
uses: actions/download-artifact@v4
103+
with:
104+
name: cibw-sdist
105+
path: dist
106+
107+
- name: Verify versions match tag
108+
shell: bash
109+
if: startsWith(github.ref, 'refs/tags/v')
110+
run: |
111+
TAG="${GITHUB_REF##*/}"
112+
VER="${TAG#v}"
113+
echo "Tag: $TAG Version: $VER"
114+
shopt -s nullglob
115+
[[ -e dist/*"$VER"*.whl ]] || { echo "No wheel with $VER"; exit 1; }
116+
[[ -e dist/*"$VER"*.tar.gz ]] || { echo "No sdist with $VER"; exit 1; }
117+
echo "OK — artifacts match $VER"
118+
119+
- name: Show release info
120+
shell: bash
121+
run: |
122+
echo "Release Info:"
123+
TAG="${GITHUB_REF##*/}"
124+
VER="${TAG#v}"
125+
echo "Release tag: $TAG"
126+
echo "Release ver: $VER"
127+
echo
128+
echo "Artifacts in dist/:"
129+
ls -lh dist || true
130+
131+
- name: Publish to TestPyPI (test tags)
132+
if: startsWith(github.ref, 'refs/tags/test')
133+
uses: pypa/gh-action-pypi-publish@release/v1
134+
with:
135+
repository-url: https://test.pypi.org/legacy/
136+
packages-dir: dist
137+
skip-existing: true
138+
verbose: true
139+
140+
- name: Publish to PyPI (real releases)
141+
if: startsWith(github.ref, 'refs/tags/v')
142+
uses: pypa/gh-action-pypi-publish@release/v1
143+
with:
144+
packages-dir: dist

.github/workflows/version-bumper.sh

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,12 @@ while IFS= read -r version || [[ -n "$version" ]]; do
1717
fi
1818
done < $1
1919

20-
echo "$updated_version" > $1
20+
echo "$updated_version" > $1
21+
22+
# Expose the update versions to GitHub Actions
23+
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
24+
{
25+
echo "version=$version"
26+
echo "updated_version=$updated_version"
27+
} >> "$GITHUB_OUTPUT"
28+
fi

MANIFEST.in

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
prune examples
2+
prune docs
3+
prune src/tests
4+
prune **/_UnitTest
5+
prune **/_Documentation
6+
7+
global-exclude *.pdf *.png *.jpg *.jpeg *.gif *.JPG *.svg *.psd
8+
global-exclude *.ipynb *.bib *.tex
9+
global-exclude *.mex* *.o *.a *.so *.dylib *.dll
10+
global-exclude build/** CMakeFiles/** .git/** .github/** .venv/**
11+
12+
# This file is used by SWIG
13+
include src/fswAlgorithms/effectorInterfaces/thrForceMapping/_UnitTest/Support/Results_thrForceMapping.py

conanfile.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import sys
88
from datetime import datetime
99
from typing import Optional, Callable
10+
from glob import glob
1011

1112
import importlib.metadata
1213
from packaging.requirements import Requirement
@@ -252,6 +253,14 @@ def layout(self):
252253
self.folders.build = str(self.options.get_safe("buildFolder"))
253254

254255
def generate(self):
256+
if self.settings.os == "Windows":
257+
# Ensure dependent DLLs are copied into the Basilisk package
258+
# directory inside the build folder so they can be discovered by
259+
# packaging tools (delvewheel) and included in wheels.
260+
basilisk_dst = os.path.join(self.build_folder, "Basilisk")
261+
for dep in self.dependencies.values():
262+
for bindir in dep.cpp_info.bindirs:
263+
copy(self, "*.dll", bindir, basilisk_dst)
255264
if self.settings.os == "Windows":
256265
for dep in self.dependencies.values():
257266
for libdir in dep.cpp_info.bindirs:
@@ -309,6 +318,8 @@ def generate(self):
309318
# Set the minimum buildable MacOS version.
310319
# tc.cache_variables["CMAKE_OSX_DEPLOYMENT_TARGET"] = "10.13"
311320
tc.parallel = True
321+
if self.options.get_safe("pyLimitedAPI"):
322+
tc.cache_variables["PY_LIMITED_API"] = str(self.options.pyLimitedAPI)
312323

313324
# Generate!
314325
tc.generate()
@@ -329,6 +340,41 @@ def build(self):
329340
cmake.build()
330341
print("Total Build Time: " + str(datetime.now() - start))
331342
print(f"{statusColor}The Basilisk build is successful and the scripts are ready to run{endColor}")
343+
# On Windows, copy project-built DLLs next to the Python extension modules
344+
# so they are bundled in the wheel and resolvable at runtime without PATH tweaks.
345+
if self.settings.os == "Windows":
346+
basilisk_dst_root = os.path.join(self.build_folder, "Basilisk")
347+
common_srcs = [
348+
os.path.join(self.build_folder, "bin"),
349+
os.path.join(self.build_folder, "Release"),
350+
os.path.join(self.build_folder, "Debug"),
351+
]
352+
for src in common_srcs:
353+
if os.path.isdir(src):
354+
try:
355+
copy(self, "*.dll", src, basilisk_dst_root)
356+
except Exception as e:
357+
self.output.warning(f"Failed to copy DLLs from {src}: {e}")
358+
359+
# As a fallback, scan the build tree for any remaining DLLs.
360+
for root, _dirs, files in os.walk(self.build_folder):
361+
# Skip the destination to avoid self-copy
362+
if os.path.commonpath([root, basilisk_dst_root]) == basilisk_dst_root:
363+
continue
364+
if any(f.lower().endswith(".dll") for f in files):
365+
try:
366+
copy(self, "*.dll", root, basilisk_dst_root)
367+
except Exception as e:
368+
self.output.warning(f"Failed to copy DLLs from {root}: {e}")
369+
370+
# Rename DLLs to lowercase
371+
for path in glob(os.path.join(basilisk_dst_root, "*.dll")):
372+
base = os.path.basename(path)
373+
lower = base.lower()
374+
if base != lower:
375+
tmp = os.path.join(basilisk_dst_root, f".{lower}.tmp")
376+
os.replace(path, tmp)
377+
os.replace(tmp, os.path.join(basilisk_dst_root, lower))
332378
else:
333379
print(f"{statusColor}Finished configuring the Basilisk project.{endColor}")
334380
if self.settings.os != "Linux":

0 commit comments

Comments
 (0)