Skip to content

Commit a09a9a0

Browse files
authored
refactor: custom setuptools command (#312)
I think this is much closer to the way setuptools intends this sort of thing to be done. Editable installs will be much easier this way, I believe. The only minor downside is that multiple top-level CMakeLists would not be natively supported this way. --------- Signed-off-by: Henry Schreiner <[email protected]>
1 parent eff6ded commit a09a9a0

File tree

18 files changed

+347
-192
lines changed

18 files changed

+347
-192
lines changed

.github/CONTRIBUTING.md

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -278,16 +278,6 @@ target_compile_definitions(cmake_example
278278
install(TARGETS cmake_example DESTINATION .)
279279
```
280280

281-
This is built on top of CMakeExtension, which looks like this:
282-
283-
```
284-
from scikit_build_core.setuptoools.extension import CMakeExtension
285-
...
286-
cmake_extensions=[CMakeExtension("cmake_example")],
287-
```
288-
289-
Which should eventually support multiple extensions.
290-
291281
## Patterns
292282

293283
### Backports

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ jobs:
120120
# Skipped on pypy to keep the tests fast
121121
- name: Test min package
122122
if: matrix.python-version != 'pypy-3.8'
123-
run: pytest -ra --showlocals
123+
run: pytest -ra --showlocals -Wdefault
124124

125125
cygwin:
126126
name: Tests on 🐍 3.9 • cygwin

docs/api/scikit_build_core.setuptools.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@ scikit\_build\_core.setuptools package
99
Submodules
1010
----------
1111

12-
scikit\_build\_core.setuptools.build\_meta module
13-
-------------------------------------------------
12+
scikit\_build\_core.setuptools.build\_cmake module
13+
--------------------------------------------------
1414

15-
.. automodule:: scikit_build_core.setuptools.build_meta
15+
.. automodule:: scikit_build_core.setuptools.build_cmake
1616
:members:
1717
:undoc-members:
1818
:show-inheritance:
1919

20-
scikit\_build\_core.setuptools.extension module
21-
-----------------------------------------------
20+
scikit\_build\_core.setuptools.build\_meta module
21+
-------------------------------------------------
2222

23-
.. automodule:: scikit_build_core.setuptools.extension
23+
.. automodule:: scikit_build_core.setuptools.build_meta
2424
:members:
2525
:undoc-members:
2626
:show-inheritance:

noxfile.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import argparse
44
import shutil
55
import sys
6+
from collections.abc import Sequence
67
from pathlib import Path
78

89
import nox
@@ -34,14 +35,12 @@ def pylint(session: nox.Session) -> None:
3435
session.run("pylint", "scikit_build_core", *session.posargs)
3536

3637

37-
@nox.session(reuse_venv=True)
38-
def tests(session: nox.Session) -> None:
39-
"""
40-
Run the unit and regular tests. Includes coverage if --cov passed.
41-
"""
38+
def _run_tests(
39+
session: nox.Session, *, install_args: Sequence[str], run_args: Sequence[str] = ()
40+
) -> None:
4241
posargs = list(session.posargs)
4342
env = {"PIP_DISABLE_PIP_VERSION_CHECK": "1"}
44-
extra = ["hatch-fancy-pypi-readme", "rich", "setuptools-scm"]
43+
extra = []
4544
# This will not work if system CMake is too old (<3.15)
4645
if shutil.which("cmake") is None and shutil.which("cmake3") is None:
4746
extra.append("cmake")
@@ -51,8 +50,30 @@ def tests(session: nox.Session) -> None:
5150
extra.append("numpy")
5251

5352
install_arg = "-e.[test,cov]" if "--cov" in posargs else "-e.[test]"
54-
session.install(install_arg, *extra)
55-
session.run("pytest", *posargs, env=env)
53+
session.install(install_arg, *extra, *install_args)
54+
session.run("pytest", *run_args, *posargs, env=env)
55+
56+
57+
@nox.session
58+
def tests(session: nox.Session) -> None:
59+
"""
60+
Run the unit and regular tests. Includes coverage if --cov passed.
61+
"""
62+
_run_tests(
63+
session, install_args=["hatch-fancy-pypi-readme", "rich", "setuptools-scm"]
64+
)
65+
66+
67+
@nox.session
68+
def minimums(session: nox.Session) -> None:
69+
"""
70+
Test the minimum versions of dependencies.
71+
"""
72+
_run_tests(
73+
session,
74+
install_args=["--constraint=tests/constraints.txt"],
75+
run_args=["-Wdefault"],
76+
)
5677

5778

5879
@nox.session(reuse_venv=True)

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,10 @@ docs = [
8585
Homepage = "https://github.com/scikit-build/scikit-build-core"
8686
Examples = "https://github.com/scikit-build/scikit-build-core/tree/main/tests/packages"
8787

88-
[project.entry-points."distutils.setup_keywords"]
89-
cmake_extensions = "scikit_build_core.setuptools.extension:cmake_extensions"
90-
cmake_source_dir = "scikit_build_core.setuptools.extension:cmake_source_dir"
91-
88+
[project.entry-points]
89+
"distutils.commands".build_cmake = "scikit_build_core.setuptools.build_cmake:BuildCMake"
90+
"distutils.setup_keywords".cmake_source_dir = "scikit_build_core.setuptools.build_cmake:cmake_source_dir"
91+
"setuptools.finalize_distribution_options".scikit_build_entry = "scikit_build_core.setuptools.build_cmake:finalize_distribution_options"
9292

9393
[tool.hatch]
9494
version.source = "vcs"
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import shutil
5+
import sys
6+
from pathlib import Path
7+
8+
import setuptools
9+
from packaging.version import Version
10+
from setuptools.dist import Distribution
11+
12+
from .._compat.typing import Literal
13+
from ..builder.builder import Builder, get_archs
14+
from ..builder.macos import normalize_macos_version
15+
from ..cmake import CMake, CMaker
16+
from ..settings.skbuild_read_settings import SettingsReader
17+
18+
__all__: list[str] = ["BuildCMake", "finalize_distribution_options", "cmake_source_dir"]
19+
20+
21+
def __dir__() -> list[str]:
22+
return __all__
23+
24+
25+
def _validate_settings() -> None:
26+
settings = SettingsReader.from_file("pyproject.toml", {}).settings
27+
28+
assert (
29+
not settings.wheel.expand_macos_universal_tags
30+
), "wheel.expand_macos_universal_tags is not supported in setuptools mode"
31+
assert (
32+
settings.logging.level == "WARNING"
33+
), "Logging is not adjustable in setuptools mode yet"
34+
assert (
35+
not settings.wheel.py_api
36+
), "wheel.py_api is not supported in setuptools mode, use bdist_wheel options instead"
37+
38+
39+
class BuildCMake(setuptools.Command):
40+
source_dir: str | None = None
41+
42+
build_lib: str | None
43+
build_temp: str | None
44+
debug: bool | None
45+
editable_mode: bool
46+
parallel: int | None
47+
plat_name: str | None
48+
49+
user_options = [
50+
("build-lib=", "b", "directory for compiled extension modules"),
51+
("build-temp=", "t", "directory for temporary files (build by-products)"),
52+
("plat-name=", "p", "platform name to cross-compile for, if supported "),
53+
("debug", "g", "compile/link with debugging information"),
54+
("parallel=", "j", "number of parallel build jobs"),
55+
]
56+
57+
def initialize_options(self) -> None:
58+
self.build_lib = None
59+
self.build_temp = None
60+
self.debug = None
61+
self.editable_mode = False
62+
self.parallel = None
63+
self.plat_name = None
64+
65+
def finalize_options(self) -> None:
66+
self.set_undefined_options(
67+
"build_ext",
68+
("build_lib", "build_lib"),
69+
("build_temp", "build_temp"),
70+
("debug", "debug"),
71+
("parallel", "parallel"),
72+
("plat_name", "plat_name"),
73+
)
74+
75+
def run(self) -> None:
76+
assert self.source_dir is not None
77+
assert self.build_lib is not None
78+
assert self.build_temp is not None
79+
assert self.plat_name is not None
80+
81+
_validate_settings()
82+
83+
build_tmp_folder = Path(self.build_temp)
84+
build_temp = build_tmp_folder / "_skbuild" # TODO: include python platform
85+
86+
dist = self.distribution
87+
88+
bdist_wheel = dist.get_command_obj("bdist_wheel")
89+
assert bdist_wheel is not None
90+
limited_api = bdist_wheel.py_limited_api # type: ignore[attr-defined]
91+
92+
# TODO: this is a hack due to moving temporary paths for isolation
93+
if build_temp.exists():
94+
shutil.rmtree(build_temp)
95+
96+
settings = SettingsReader.from_file("pyproject.toml", {}).settings
97+
98+
cmake = CMake.default_search(
99+
minimum_version=Version(settings.cmake.minimum_version)
100+
)
101+
102+
config = CMaker(
103+
cmake,
104+
source_dir=Path(self.source_dir),
105+
build_dir=build_temp,
106+
build_type=settings.cmake.build_type,
107+
)
108+
109+
builder = Builder(
110+
settings=settings,
111+
config=config,
112+
)
113+
114+
# Setuptools requires this be specified if there's a mismatch.
115+
if sys.platform.startswith("darwin"):
116+
arm_only = get_archs(builder.config.env) == ["arm64"]
117+
orig_macos_str = self.plat_name.rsplit("-", 1)[0].split("-", 1)[1]
118+
orig_macos = normalize_macos_version(orig_macos_str, arm_only)
119+
config.env.setdefault("MACOSX_DEPLOYMENT_TARGET", str(orig_macos))
120+
121+
debug = int(os.environ.get("DEBUG", 0)) if self.debug is None else self.debug
122+
builder.config.build_type = "Debug" if debug else "Release"
123+
124+
builder.configure(
125+
name=dist.get_name(),
126+
version=Version(dist.get_version()),
127+
defines={},
128+
limited_abi=limited_api,
129+
)
130+
131+
# Set CMAKE_BUILD_PARALLEL_LEVEL to control the parallel build level
132+
# across all generators.
133+
build_args = []
134+
135+
# self.parallel is a way to set parallel jobs by hand using -j in the
136+
# build_ext call, not supported by pip or PyPA-build.
137+
if "CMAKE_BUILD_PARALLEL_LEVEL" not in builder.config.env and self.parallel:
138+
build_args.append(f"-j{self.parallel}")
139+
140+
builder.build(build_args=build_args)
141+
builder.install(Path(self.build_lib))
142+
143+
# def get_source_files(self) -> list[str]:
144+
# return ["CMakeLists.txt"]
145+
146+
# def get_outputs(self) -> list[str]:
147+
# return []
148+
149+
# def get_output_mapping(self) -> dict[str, str]:
150+
# return {}
151+
152+
153+
def _has_cmake(dist: Distribution) -> bool:
154+
build_cmake = dist.get_command_obj("build_cmake")
155+
assert isinstance(build_cmake, BuildCMake)
156+
return build_cmake.source_dir is not None
157+
158+
159+
def _prepare_extension_detection(dist: Distribution) -> None:
160+
# Setuptools needs to know that it has extensions modules
161+
162+
dist.has_ext_modules = lambda: type(dist).has_ext_modules(dist) or _has_cmake(dist) # type: ignore[method-assign]
163+
164+
# Hack for stdlib distutils
165+
if not setuptools.distutils.__package__.startswith("setuptools"): # type: ignore[attr-defined]
166+
167+
class EvilList(list): # type: ignore[type-arg]
168+
def __len__(self) -> int:
169+
return super().__len__() or int(_has_cmake(dist))
170+
171+
dist.ext_modules = getattr(dist, "ext_modules", []) or EvilList()
172+
173+
174+
def _prepare_build_cmake_command(dist: Distribution) -> None:
175+
# Prepare new build_cmake command and make sure build calls it
176+
build = dist.get_command_obj("build")
177+
assert build is not None
178+
if "build_cmake" not in {x for x, _ in build.sub_commands}:
179+
build.sub_commands.append(
180+
("build_cmake", lambda cmd: _has_cmake(cmd.distribution)) # type: ignore[arg-type]
181+
)
182+
183+
184+
def cmake_source_dir(
185+
dist: Distribution, attr: Literal["cmake_source_dir"], value: str
186+
) -> None:
187+
assert attr == "cmake_source_dir"
188+
assert Path(value).is_dir()
189+
190+
build_cmake = dist.get_command_obj("build_cmake")
191+
assert isinstance(build_cmake, BuildCMake)
192+
193+
build_cmake.source_dir = value
194+
195+
196+
def finalize_distribution_options(dist: Distribution) -> None:
197+
_prepare_extension_detection(dist)
198+
_prepare_build_cmake_command(dist)

0 commit comments

Comments
 (0)