Skip to content

Commit 138b0a3

Browse files
authored
ci: add step to check wheel contents before uploading artifacts (#66)
* ci: add step to check wheel contents before uploading artifacts * add check-wheel test * better stub generation * try fix _windows_dll_dirs * try fix paths
1 parent ad263c7 commit 138b0a3

File tree

6 files changed

+94
-1062
lines changed

6 files changed

+94
-1062
lines changed

.github/workflows/deploy.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ jobs:
4444
# distribute.
4545
CIBW_ENVIRONMENT_LINUX: "LDFLAGS=-Wl,--strip-debug"
4646

47+
- name: check-wheel-contents
48+
run: uvx check-wheel-contents ./wheelhouse
49+
4750
- uses: actions/upload-artifact@v4
4851
with:
4952
name: artifact-wheels-${{ matrix.os }}${{ matrix.macos_arch }}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,6 @@ __pycache__
1616
coverage/
1717
coverage*
1818
wheelhouse/
19+
20+
# autogenerated
21+
src/pymmcore_nano/_pymmcore_nano.pyi

meson.build

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# replace the version by running:
22
# meson rewrite kwargs set project / version $(python scripts/extract_version.py)
33
project(
4-
'pymmcore-nano',
5-
'cpp',
6-
version : '11.10.0.74.0',
7-
meson_version : '>=1.4.0',
8-
default_options : ['cpp_std=c++17']
4+
'pymmcore-nano',
5+
'cpp',
6+
version: '11.10.0.74.0',
7+
meson_version: '>=1.4.0',
8+
default_options: ['cpp_std=c++17'],
99
)
1010

1111
if host_machine.system() == 'darwin'
@@ -52,37 +52,50 @@ ext_module = py.extension_module(
5252
)
5353

5454
# Create stubs using nanobind.stubgen
55-
custom_target(
55+
stub_target = custom_target(
5656
'generate_stubs',
57-
build_by_default: true,
5857
input: [ext_module],
5958
output: '_pymmcore_nano.pyi',
6059
command: [
6160
py.full_path(),
62-
meson.project_source_root() + '/scripts/build_stubs.py',
63-
ext_module.full_path(),
64-
meson.project_source_root()
65-
+ '/src/pymmcore_nano/_pymmcore_nano.pyi',
61+
meson.project_source_root() / 'scripts' / 'build_stubs.py',
62+
'@INPUT@', # path to built extension
63+
'@OUTPUT@', # write stub to this path
6664
],
65+
build_by_default: true,
66+
install: true,
67+
install_dir: py.get_install_dir() / 'pymmcore_nano',
6768
depends: ext_module,
6869
)
6970

70-
# install the Python package into the site-packages directory
71-
install_subdir(
72-
'src/pymmcore_nano',
73-
install_dir: py.get_install_dir() / 'pymmcore_nano',
74-
strip_directory: true,
71+
# Copy stub to source tree for development (so Pyright can find it)
72+
custom_target(
73+
'copy_stubs_to_src',
74+
input: stub_target,
75+
output: 'stub_copied_marker',
76+
command: [
77+
py.full_path(),
78+
'-c',
79+
'import shutil; shutil.copy(r"@INPUT@", r"@0@")'.format(
80+
meson.project_source_root() / 'src' / 'pymmcore_nano' / '_pymmcore_nano.pyi'
81+
)
82+
],
83+
build_by_default: true,
84+
depends: stub_target,
7585
)
7686

77-
# also install the stubs into the site-packages directory
78-
install_data(
79-
'src/pymmcore_nano/_pymmcore_nano.pyi',
80-
install_dir: py.get_install_dir() / 'pymmcore_nano',
87+
# Install the pure-Python package bits
88+
py.install_sources(
89+
files(
90+
'src/pymmcore_nano/__init__.py',
91+
'src/pymmcore_nano/py.typed',
92+
),
93+
subdir: 'pymmcore_nano',
8194
)
8295

8396
test(
8497
'test_script',
8598
py,
8699
args: ['-m', 'pytest', '--color=yes', '-v'],
87100
workdir: meson.current_source_dir(),
88-
)
101+
)

pyproject.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,12 @@ build-verbosity = 1
8383
# is specific to glibc and not available in musl-libc
8484
skip = ["*-manylinux_i686", "*-musllinux*", "*-win32", "pp*"]
8585
build = ["cp39-*", "cp310-*", "cp311-*", "cp312-*", "cp313-*"]
86+
test-requires = ["check-wheel-contents"]
8687
test-groups = ["test"]
87-
test-command = 'pytest "{project}/tests" -v'
88+
test-command = [
89+
"check-wheel-contents {project}/wheelhouse",
90+
"pytest {project}/tests -v",
91+
]
8892

8993
[tool.cibuildwheel.macos]
9094
# https://cibuildwheel.readthedocs.io/en/stable/faq/#apple-silicon
@@ -128,3 +132,8 @@ ignore = [
128132
"examples/*.py" = ["D"]
129133
"_cli.py" = ["B008"]
130134
"docs/*.py" = ["A", "D"]
135+
136+
[tool.check-wheel-contents]
137+
toplevel = "pymmcore_nano"
138+
package = "src/pymmcore_nano"
139+
ignore = ["W102"]

scripts/build_stubs.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,54 @@
55
"""
66

77
import importlib.util
8+
import os
89
import re
910
import subprocess
1011
import sys
12+
from contextlib import ExitStack, contextmanager
1113
from pathlib import Path
1214
from types import ModuleType
1315

1416
from nanobind.stubgen import StubGen
1517

1618

19+
@contextmanager
20+
def _windows_dll_dirs(module_path: Path):
21+
"""Ensure Windows can resolve dependent DLLs for the extension import."""
22+
if os.name != "nt":
23+
yield
24+
return
25+
stack = ExitStack()
26+
try:
27+
# Prefer the extension's directory first (absolute paths only)
28+
parent = module_path.parent.resolve()
29+
candidates = [
30+
parent,
31+
Path(sys.base_prefix, "DLLs").resolve(),
32+
Path(sys.base_prefix).resolve(), # vcruntime*, pythonXY.dll
33+
]
34+
# Deduplicate while preserving order, and filter to existing directories
35+
seen = set()
36+
abs_dirs = []
37+
for d in candidates:
38+
try:
39+
d = d.resolve()
40+
except Exception:
41+
continue
42+
if not d.exists() or not d.is_dir():
43+
continue
44+
if str(d) in seen:
45+
continue
46+
seen.add(str(d))
47+
abs_dirs.append(d)
48+
49+
for d in abs_dirs:
50+
stack.enter_context(os.add_dll_directory(str(d)))
51+
yield
52+
finally:
53+
stack.close()
54+
55+
1756
def load_module_from_filepath(name: str, filepath: str) -> ModuleType:
1857
# Create a module spec
1958
spec = importlib.util.spec_from_file_location(name, filepath)
@@ -26,8 +65,12 @@ def load_module_from_filepath(name: str, filepath: str) -> ModuleType:
2665

2766

2867
def build_stub(module_path: Path, output_path: str):
68+
module_path = module_path.resolve()
2969
module_name = module_path.stem.split(".")[0]
30-
module = load_module_from_filepath(module_name, str(module_path))
70+
# Ensure DLLs are discoverable on Windows before importing the extension
71+
with _windows_dll_dirs(module_path):
72+
module = load_module_from_filepath(module_name, str(module_path))
73+
3174
s = StubGen(module, include_docstrings=True, include_private=False)
3275
s.put(module)
3376
dest = Path(output_path)

0 commit comments

Comments
 (0)