diff --git a/docs/api/scikit_build_core.builder.rst b/docs/api/scikit_build_core.builder.rst index 79ef03b8..8db6bb42 100644 --- a/docs/api/scikit_build_core.builder.rst +++ b/docs/api/scikit_build_core.builder.rst @@ -17,6 +17,14 @@ scikit\_build\_core.builder.builder module :undoc-members: :show-inheritance: +scikit\_build\_core.builder.cross\_compile module +------------------------------------------------- + +.. automodule:: scikit_build_core.builder.cross_compile + :members: + :undoc-members: + :show-inheritance: + scikit\_build\_core.builder.generator module -------------------------------------------- diff --git a/src/scikit_build_core/builder/builder.py b/src/scikit_build_core/builder/builder.py index 395466a7..3c5d97a0 100644 --- a/src/scikit_build_core/builder/builder.py +++ b/src/scikit_build_core/builder/builder.py @@ -15,6 +15,7 @@ from ..cmake import CMaker from ..resources import find_python from ..settings.skbuild_model import ScikitBuildSettings +from .cross_compile import auto_cross_compile_env from .generator import set_environment_for_gen from .sysconfig import ( get_platform, @@ -183,10 +184,11 @@ def configure( # Add the pre-defined or passed CMake defines cmake_defines.update(self.settings.cmake.define) - self.config.configure( - defines=cmake_defines, - cmake_args=[*self.get_cmake_args(), *configure_args], - ) + with auto_cross_compile_env(self.config.env): + self.config.configure( + defines=cmake_defines, + cmake_args=[*self.get_cmake_args(), *configure_args], + ) def build(self, build_args: list[str]) -> None: self.config.build(build_args=build_args, verbose=self.settings.cmake.verbose) diff --git a/src/scikit_build_core/builder/cross_compile.py b/src/scikit_build_core/builder/cross_compile.py new file mode 100644 index 00000000..f1f8a1ba --- /dev/null +++ b/src/scikit_build_core/builder/cross_compile.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +import contextlib +import os +import sysconfig +import tempfile +from collections.abc import Generator, MutableMapping +from pathlib import Path + +from .._logging import logger + +__all__ = ["set_cross_compile_env", "auto_cross_compile_env"] + + +def __dir__() -> list[str]: + return __all__ + + +@contextlib.contextmanager +def auto_cross_compile_env( + env: MutableMapping[str, str] +) -> Generator[None, None, None]: + if "SETUPTOOLS_EXT_SUFFIX" not in env: + yield + return + + with set_cross_compile_env(env["SETUPTOOLS_EXT_SUFFIX"], env): + yield + + +@contextlib.contextmanager +def set_cross_compile_env( + ext_suffix: str, + env: MutableMapping[str, str], +) -> Generator[None, None, None]: + """ + Generate python file and set environment variables to cross-compile Python + extensions. Do not call if _PYTHON_SYSCONFIGDATA_NAME is already set. + """ + + if "_PYTHON_SYSCONFIGDATA_NAME" in env: + logger.debug( + "Not setting up cross compiling explicitly due to _PYTHON_SYSCONFIGDATA_NAME already set." + ) + yield + return + + with tempfile.TemporaryDirectory() as tmpdir: + tmp_dir = Path(tmpdir).resolve() + cross_compile_file = ( + tmp_dir / f"_cross_compile_{ext_suffix.replace('.', '_')}.py" + ) + build_time_vars = sysconfig.get_config_vars() + build_time_vars["EXT_SUFFIX"] = ext_suffix + build_time_vars["SOABI"] = ext_suffix.rsplit(maxsplit=1)[0] + output_text = f"build_time_vars = {build_time_vars!r}\n" + cross_compile_file.write_text(output_text) + current_path = env.get("PYTHONPATH", "") + env["PYTHONPATH"] = ( + os.pathsep.join([current_path, str(tmp_dir)]) + if current_path + else str(tmp_dir) + ) + env["_PYTHON_SYSCONFIGDATA_NAME"] = cross_compile_file.stem + logger.info("Cross-compiling is enabled to {!r}.", ext_suffix) + logger.debug( + "Setting _PYTHON_SYSCONFIGDATA_NAME to {!r}.", + env["_PYTHON_SYSCONFIGDATA_NAME"], + ) + logger.debug("Setting PYTHONPATH to {!r}.", env["PYTHONPATH"]) + logger.debug("Cross compile output file contents: {}", output_text) + try: + yield + finally: + del env["_PYTHON_SYSCONFIGDATA_NAME"] + if current_path: + env["PYTHONPATH"] = current_path + else: + del env["PYTHONPATH"] diff --git a/tests/test_cross_compile.py b/tests/test_cross_compile.py new file mode 100644 index 00000000..8f9cfd6a --- /dev/null +++ b/tests/test_cross_compile.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import os +import subprocess +import sys +import sysconfig + +import pytest + +from scikit_build_core.builder.cross_compile import set_cross_compile_env + +ext_suffix = sysconfig.get_config_var("EXT_SUFFIX") + + +@pytest.mark.skipif( + ext_suffix != ".cp311-win_amd64.pyd", + reason=f"Only tests '.cp311-win_amd64.pyd', got {ext_suffix!r}", +) +def test_environment(): + env = os.environ.copy() + cmd = [ + sys.executable, + "-c", + "import sysconfig; print(sysconfig.get_config_var('SOABI'), sysconfig.get_config_var('EXT_SUFFIX'))", + ] + + with set_cross_compile_env(".cp311-win_arm64.pyd", env): + result = subprocess.run( + cmd, check=True, capture_output=True, text=True, env=env + ) + soabi, ext_suffix = result.stdout.strip().split() + print(soabi, ext_suffix) + assert soabi == "cp311-win_arm64" + assert ext_suffix == ".cp311-win_arm64.pyd" + + result = subprocess.run(cmd, check=True, capture_output=True, text=True, env=env) + soabi, ext_suffix = result.stdout.strip().split() + assert soabi == "cp311-win_amd64" + assert ext_suffix == ".cp311-win_amd64.pyd" diff --git a/tests/test_module_dir.py b/tests/test_module_dir.py index 39d3c65a..f9a7013b 100644 --- a/tests/test_module_dir.py +++ b/tests/test_module_dir.py @@ -29,7 +29,11 @@ def on_all_modules( def test_all_modules_filter_all(): all_modules = on_all_modules("scikit_build_core", pkg=False) - all_modules = (n for n in all_modules if not n.split(".")[-1].startswith("__")) + all_modules = ( + n + for n in all_modules + if not n.split(".")[-1].startswith("__") and "resources" not in n + ) for name in all_modules: module = importlib.import_module(name) @@ -45,7 +49,11 @@ def test_all_modules_filter_all(): def test_all_modules_has_all(): all_modules = on_all_modules("scikit_build_core", pkg=True) - all_modules = (n for n in all_modules if not n.split(".")[-1].startswith("_")) + all_modules = ( + n + for n in all_modules + if not n.split(".")[-1].startswith("_") and "resources" not in n + ) for name in all_modules: module = importlib.import_module(name)