diff --git a/docs/Config.md b/docs/Config.md index 3b3eea8..08cc871 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -52,7 +52,14 @@ Defines how to build the project to package. If omitted, py-build-cmake will pro | `install_args` | Extra arguments passed to the install step.
For example: `install_args = ["--strip"]` | list+ | `[]` | | `install_components` | List of components to install, the install step is executed once for each component, with the option `--component `.
Use an empty string to specify the default component. | list | `['']` | | `env` | Environment variables to set when running CMake. Supports variable expansion using `${VAR}` (but not `$VAR`).
For example: `env = { "CMAKE_PREFIX_PATH" = "${HOME}/.local" }` | dict | `{}` | -| `pure_python` | Indicate that this package contains no platform-specific binaries, only Python scripts and other platform-agnostic files. It causes the Wheel tags to be set to `py3-none-any`.
For example: `pure_python = true` | bool | `false` | + +## wheel +Defines how to create the Wheel package. + +| Option | Description | Type | Default | +|--------|-------------|------|---------| +| `pure_python` | Indicate that this package contains no platform-specific binaries, only Python scripts and other platform-agnostic files. Setting this value to true causes the Wheel tags to be set to `py3-none-any`. If unset, the value depends on whether the `cmake` option is set.
For example: `pure_python = true` | bool | `none` | +| `python_tag` | Override the default Python tag for the Wheel package.
If your package contains any Python extension modules, you want to set this to `auto`.
For details about platform compatibility tags, see the PyPA specification: https://packaging.python.org/en/latest/specifications/platform-compatibility-tags
For example: `python_tag = 'py2.py3'` | string | `'auto'` | | `python_abi` | Override the default ABI tag for the Wheel package.
For packages with a Python extension module that make use of the full Python C API, this option should be set to `auto`.
If your package does not contain Python extension modules (e.g. because it only includes executables to run as a subprocess, or only shared library files to be loaded using `ctypes`), you can set this to `none`.
If your package only includes Python extension modules that use the CPython stable ABI, set this to `abi3` (see also `abi3_minimum_cpython_version` below).
For details about platform compatibility tags, see the PyPA specification: https://packaging.python.org/en/latest/specifications/platform-compatibility-tags
For example: `python_abi = 'none'` | `'auto'` \| `'none'` \| `'abi3'` | `'auto'` | | `abi3_minimum_cpython_version` | If `python_abi` is set to `abi3`, only use the stable CPython API for CPython version that are newer than `abi3_minimum_version`. Useful for nanobind, which supports the stable ABI for CPython 12 and later.
The Python version is encoded as a single integer, consisting of the major and minor version numbers, without a dot.
For example: `abi3_minimum_cpython_version = 312` | int | `32` | @@ -74,6 +81,7 @@ Override options for Linux. | `editable` | Linux-specific editable options.
Inherits from: `/pyproject.toml/tool/py-build-cmake/editable` | | `none` | | `sdist` | Linux-specific sdist options.
Inherits from: `/pyproject.toml/tool/py-build-cmake/sdist` | | `none` | | `cmake` | Linux-specific CMake options.
Inherits from: `/pyproject.toml/tool/py-build-cmake/cmake` | | `none` | +| `wheel` | Linux-specific Wheel options.
Inherits from: `/pyproject.toml/tool/py-build-cmake/wheel` | | `none` | ## windows Override options for Windows. @@ -83,6 +91,7 @@ Override options for Windows. | `editable` | Windows-specific editable options.
Inherits from: `/pyproject.toml/tool/py-build-cmake/editable` | | `none` | | `sdist` | Windows-specific sdist options.
Inherits from: `/pyproject.toml/tool/py-build-cmake/sdist` | | `none` | | `cmake` | Windows-specific CMake options.
Inherits from: `/pyproject.toml/tool/py-build-cmake/cmake` | | `none` | +| `wheel` | Windows-specific Wheel options.
Inherits from: `/pyproject.toml/tool/py-build-cmake/wheel` | | `none` | ## mac Override options for Mac. @@ -92,6 +101,7 @@ Override options for Mac. | `editable` | Mac-specific editable options.
Inherits from: `/pyproject.toml/tool/py-build-cmake/editable` | | `none` | | `sdist` | Mac-specific sdist options.
Inherits from: `/pyproject.toml/tool/py-build-cmake/sdist` | | `none` | | `cmake` | Mac-specific CMake options.
Inherits from: `/pyproject.toml/tool/py-build-cmake/cmake` | | `none` | +| `wheel` | Mac-specific Wheel options.
Inherits from: `/pyproject.toml/tool/py-build-cmake/wheel` | | `none` | ## cross Causes py-build-cmake to cross-compile the project. See https://tttapa.github.io/py-build-cmake/Cross-compilation.html for more information. @@ -110,6 +120,7 @@ Causes py-build-cmake to cross-compile the project. See https://tttapa.github.io | `editable` | Override editable options when cross-compiling.
Inherits from: `/pyproject.toml/tool/py-build-cmake/editable` | | `none` | | `sdist` | Override sdist options when cross-compiling.
Inherits from: `/pyproject.toml/tool/py-build-cmake/sdist` | | `none` | | `cmake` | Override CMake options when cross-compiling.
Inherits from: `/pyproject.toml/tool/py-build-cmake/cmake` | | `none` | +| `wheel` | Override Wheel options when cross-compiling.
Inherits from: `/pyproject.toml/tool/py-build-cmake/wheel` | | `none` | # Local overrides diff --git a/examples/minimal-program/README.md b/examples/minimal-program/README.md index 0370c26..6d22d8d 100644 --- a/examples/minimal-program/README.md +++ b/examples/minimal-program/README.md @@ -8,7 +8,7 @@ should be in `${PY_BUILD_CMAKE_PACKAGE_NAME}-${PY_BUILD_CMAKE_PACKAGE_VERSION}.d as per [PEP 427](https://peps.python.org/pep-0427/). Pip will then automatically install it to a folder that's in the `PATH`. Since there are no Python extension modules with specific ABI requirements for -the Python interpreter, `tool.py-build-cmake.cmake.python_abi` is set +the Python interpreter, `tool.py-build-cmake.wheel.python_abi` is set to `'none'`. For more information about the file structure and the configuration files, diff --git a/examples/minimal-program/pyproject.toml b/examples/minimal-program/pyproject.toml index 4d975b3..19d7806 100644 --- a/examples/minimal-program/pyproject.toml +++ b/examples/minimal-program/pyproject.toml @@ -41,6 +41,7 @@ build_args = ["-j"] install_components = ["python_binaries"] find_python = false find_python3 = false +[tool.py-build-cmake.wheel] python_abi = "none" [tool.pytest.ini_options] diff --git a/examples/nanobind-project/pyproject.toml b/examples/nanobind-project/pyproject.toml index 20812ef..ced18ae 100644 --- a/examples/nanobind-project/pyproject.toml +++ b/examples/nanobind-project/pyproject.toml @@ -49,6 +49,7 @@ build_tool_args = [] install_args = ["--verbose"] install_components = ["python_modules"] env = {} +[tool.py-build-cmake.wheel] python_abi = 'abi3' abi3_minimum_cpython_version = 312 diff --git a/src/py_build_cmake/build.py b/src/py_build_cmake/build.py index 516703c..e915571 100644 --- a/src/py_build_cmake/build.py +++ b/src/py_build_cmake/build.py @@ -327,7 +327,8 @@ def create_wheel( whl = Wheel() whl.name = package_info.norm_name whl.version = package_info.version - pure = is_pure(cmake_cfg) + wheel_cfg = _BuildBackend.get_wheel_config(cfg) + pure = is_pure(wheel_cfg, cmake_cfg) libdir = "purelib" if pure else "platlib" staging_dir = paths.pkg_staging_dir whl_paths = {"prefix": str(staging_dir), libdir: str(staging_dir)} @@ -336,10 +337,10 @@ def create_wheel( tags = {"pyver": ["py3"]} elif cfg.cross: tags = get_cross_tags(cfg.cross) - tags = convert_wheel_tags(tags, cmake_cfg) + tags = convert_wheel_tags(tags, wheel_cfg, cmake_cfg) else: tags = get_native_tags() - tags = convert_wheel_tags(tags, cmake_cfg) + tags = convert_wheel_tags(tags, wheel_cfg, cmake_cfg) wheel_path = whl.build(whl_paths, tags=tags, wheel_version=(1, 0)) logger.debug("Built Wheel: %s", wheel_path) return str(Path(wheel_path).relative_to(paths.wheel_dir)) @@ -353,6 +354,13 @@ def get_cmake_config(cfg: Config): else: return cfg.cmake["cross"] + @staticmethod + def get_wheel_config(cfg: Config): + if cfg.cross is None: + return cfg.wheel[util.get_os_name()] + else: + return cfg.wheel["cross"] + # --- Building sdists ----------------------------------------------------- def do_build_sdist(self, sdist_directory, config_settings): diff --git a/src/py_build_cmake/common/__init__.py b/src/py_build_cmake/common/__init__.py index 4bf1f4f..b158a37 100644 --- a/src/py_build_cmake/common/__init__.py +++ b/src/py_build_cmake/common/__init__.py @@ -99,6 +99,7 @@ class Config: editable: dict[str, dict[str, Any]] = field(default_factory=dict) sdist: dict[str, dict[str, Any]] = field(default_factory=dict) cmake: dict[str, dict[str, Any]] | None = field(default=None) + wheel: dict[str, dict[str, Any]] = field(default_factory=dict) stubgen: dict[str, Any] | None = field(default=None) cross: dict[str, Any] | None = field(default=None) diff --git a/src/py_build_cmake/config/load.py b/src/py_build_cmake/config/load.py index 8472e06..4792b6f 100644 --- a/src/py_build_cmake/config/load.py +++ b/src/py_build_cmake/config/load.py @@ -292,6 +292,13 @@ def get_sdist_cludes(v: ValueReference) -> dict[str, Any]: } cfg.cmake = cfg.cmake or None + # Store the Wheel configuration + cfg.wheel = { + os: cast(Dict[str, Any], pbc_value_ref.get_value(ConfPath((os, "wheel")))) + for os in ("linux", "windows", "mac", "cross") + if pbc_value_ref.is_value_set(ConfPath((os, "wheel"))) + } + # Store stubgen configuration s = "stubgen" if pbc_value_ref.is_value_set(s): @@ -302,7 +309,7 @@ def get_sdist_cludes(v: ValueReference) -> dict[str, Any]: if pbc_value_ref.is_value_set(s): cfg.cross = copy(cast(Optional[Dict[str, Any]], pbc_value_ref.get_value(s))) if cfg.cross is not None: - for k in ("cmake", "sdist", "editable"): + for k in ("cmake", "wheel", "sdist", "editable"): cfg.cross.pop(k, None) # Check for incompatible options diff --git a/src/py_build_cmake/config/options/pyproject_options.py b/src/py_build_cmake/config/options/pyproject_options.py index 15387c1..22532c1 100644 --- a/src/py_build_cmake/config/options/pyproject_options.py +++ b/src/py_build_cmake/config/options/pyproject_options.py @@ -234,13 +234,35 @@ def get_options(project_path: Path | PurePosixPath, *, test: bool = False): "env = { \"CMAKE_PREFIX_PATH\" " "= \"${HOME}/.local\" }", default=DefaultValueValue({})), + ]) # fmt: skip + + # [tool.py-build-cmake.wheel] + wheel = pbc.insert( + ConfigOption("wheel", + "Defines how to create the Wheel package.", + default=DefaultValueValue({}), + )) # fmt: skip + wheel_pth = ConfPath.from_string("pyproject.toml/tool/py-build-cmake/wheel") + wheel.insert_multiple([ BoolConfigOption("pure_python", "Indicate that this package contains no platform-" "specific binaries, only Python scripts and other " - "platform-agnostic files. It causes the Wheel tags " - "to be set to `py3-none-any`.", - "pure_python = true", - default=DefaultValueValue(False)), + "platform-agnostic files. Setting this value to true " + "causes the Wheel tags to be set to `py3-none-any`. " + "If unset, the value depends on whether the `cmake` " + "option is set.", + "pure_python = true"), + StringConfigOption("python_tag", + "Override the default Python tag for the Wheel " + "package.\n" + "If your package contains any Python extension " + "modules, you want to set this to `auto`.\n" + "For details about platform compatibility tags, " + "see the PyPA specification: " + "https://packaging.python.org/en/latest/" + "specifications/platform-compatibility-tags", + "python_tag = 'py2.py3'", + default=DefaultValueValue("auto")), EnumConfigOption("python_abi", "Override the default ABI tag for the Wheel package.\n" "For packages with a Python extension module that " @@ -318,6 +340,10 @@ def get_options(project_path: Path | PurePosixPath, *, test: bool = False): f"{system}-specific CMake options.", inherit_from=cmake_pth, create_if_inheritance_target_exists=True), + ConfigOption("wheel", + f"{system}-specific Wheel options.", + inherit_from=wheel_pth, + create_if_inheritance_target_exists=True), ]) # fmt: skip # [tool.py-build-cmake.cross] @@ -413,6 +439,10 @@ def get_options(project_path: Path | PurePosixPath, *, test: bool = False): "Override CMake options when cross-compiling.", inherit_from=cmake_pth, create_if_inheritance_target_exists=True), + ConfigOption("wheel", + "Override Wheel options when cross-compiling.", + inherit_from=wheel_pth, + create_if_inheritance_target_exists=True), ]) # fmt: skip return root diff --git a/src/py_build_cmake/export/tags.py b/src/py_build_cmake/export/tags.py index 08eb827..70a6431 100644 --- a/src/py_build_cmake/export/tags.py +++ b/src/py_build_cmake/export/tags.py @@ -19,19 +19,19 @@ def get_cross_tags(crosscfg: dict[str, Any]) -> WheelTags: return tags -def convert_abi_tag(abi_tag: str, cmake_cfg: dict | None) -> str: +def convert_abi_tag(abi_tag: str, wheel_cfg: dict, cmake_cfg: dict | None) -> str: """Set the ABI tag to 'none' or 'abi3', depending on the config options specified by the user.""" if not cmake_cfg: return "none" - elif cmake_cfg["python_abi"] == "auto": + elif wheel_cfg["python_abi"] == "auto": return abi_tag - elif cmake_cfg["python_abi"] == "none": + elif wheel_cfg["python_abi"] == "none": return "none" - elif cmake_cfg["python_abi"] == "abi3": + elif wheel_cfg["python_abi"] == "abi3": # Only use abi3 if we're actually building for CPython m = re.match(r"^cp(\d+).*$", abi_tag) - if m and int(m[1]) >= cmake_cfg["abi3_minimum_cpython_version"]: + if m and int(m[1]) >= wheel_cfg["abi3_minimum_cpython_version"]: return "abi3" return abi_tag else: @@ -39,19 +39,23 @@ def convert_abi_tag(abi_tag: str, cmake_cfg: dict | None) -> str: raise AssertionError(msg) -def convert_wheel_tags(tags: dict[str, list[str]], cmake_cfg: dict | None) -> WheelTags: +def convert_wheel_tags( + tags: dict[str, list[str]], wheel_cfg: dict, cmake_cfg: dict | None +) -> WheelTags: """Apply convert_abi_tag to each of the abi tags.""" tags = copy(tags) - cvt_abi = lambda tag: convert_abi_tag(tag, cmake_cfg) + cvt_abi = lambda tag: convert_abi_tag(tag, wheel_cfg, cmake_cfg) tags["abi"] = list(map(cvt_abi, tags["abi"])) - if "none" in tags["abi"]: + if wheel_cfg["python_tag"] != "auto": + tags["pyver"] = wheel_cfg["python_tag"] + elif "none" in tags["abi"]: tags["pyver"] = ["py3"] return tags -def is_pure(cmake_cfg: dict | None) -> bool: +def is_pure(wheel_cfg: dict, cmake_cfg: dict | None) -> bool: """Check if the package is a pure-Python package without platform- specific binaries.""" - if not cmake_cfg: - return True - return cmake_cfg["pure_python"] + if "pure_python" in wheel_cfg: + return wheel_cfg["pure_python"] + return not cmake_cfg diff --git a/tests/test_config_load.py b/tests/test_config_load.py index d884de8..4884e39 100644 --- a/tests/test_config_load.py +++ b/tests/test_config_load.py @@ -140,9 +140,6 @@ def test_inherit_cross_cmake(): "foo": "bar", "crosscompiling": "true", }, - "pure_python": False, - "python_abi": "auto", - "abi3_minimum_cpython_version": 32, }, "linux": { "build_type": "Release", @@ -162,9 +159,6 @@ def test_inherit_cross_cmake(): "install_components": ["linux_install"], "minimum_version": "3.15", "env": {"foo": "bar"}, - "pure_python": False, - "python_abi": "auto", - "abi3_minimum_cpython_version": 32, }, "windows": { "build_type": "Release", @@ -184,9 +178,6 @@ def test_inherit_cross_cmake(): "install_components": ["all_install", "win_install"], "minimum_version": "3.15", "env": {"foo": "bar"}, - "pure_python": False, - "python_abi": "auto", - "abi3_minimum_cpython_version": 32, }, "mac": { "build_type": "Release", @@ -206,7 +197,26 @@ def test_inherit_cross_cmake(): "install_components": ["all_install"], "minimum_version": "3.15", "env": {"foo": "bar"}, - "pure_python": False, + }, + } + assert conf.wheel == { + "cross": { + "python_tag": "auto", + "python_abi": "auto", + "abi3_minimum_cpython_version": 32, + }, + "linux": { + "python_tag": "auto", + "python_abi": "auto", + "abi3_minimum_cpython_version": 32, + }, + "windows": { + "python_tag": "auto", + "python_abi": "auto", + "abi3_minimum_cpython_version": 32, + }, + "mac": { + "python_tag": "auto", "python_abi": "auto", "abi3_minimum_cpython_version": 32, }, @@ -288,9 +298,6 @@ def test_real_config_no_cross(): "install_components": ["linux_install"], "minimum_version": "3.15", "env": {"foo": "bar"}, - "pure_python": False, - "python_abi": "auto", - "abi3_minimum_cpython_version": 32, }, "windows": { "build_type": "Release", @@ -310,9 +317,6 @@ def test_real_config_no_cross(): "install_components": ["win_install"], "minimum_version": "3.15", "env": {"foo": "bar"}, - "pure_python": False, - "python_abi": "auto", - "abi3_minimum_cpython_version": 32, }, "mac": { "build_type": "Release", @@ -332,7 +336,21 @@ def test_real_config_no_cross(): "install_components": [""], "minimum_version": "3.15", "env": {"foo": "bar"}, - "pure_python": False, + }, + } + assert conf.wheel == { + "linux": { + "python_tag": "auto", + "python_abi": "auto", + "abi3_minimum_cpython_version": 32, + }, + "windows": { + "python_tag": "auto", + "python_abi": "auto", + "abi3_minimum_cpython_version": 32, + }, + "mac": { + "python_tag": "auto", "python_abi": "auto", "abi3_minimum_cpython_version": 32, }, @@ -467,6 +485,8 @@ def test_real_config_cli_override(): "build_tool_args": ["-a", "-b"], "find_python": False, "find_python3": True, + }, + "wheel": { "python_abi": "abi3", }, "linux": { @@ -494,8 +514,8 @@ def test_real_config_cli_override(): linux.cmake.options.FOOBAR=+"xyz" linux.cmake.options.FOOBAR-="def" linux.cmake.args-=["arg3"] - linux.cmake.python_abi=! - mac.cmake.python_abi="none" + linux.wheel.python_abi=! + mac.wheel.python_abi="none" mac.cmake.args=! mac.cmake.env=! mac.cmake.find_python=! @@ -543,9 +563,6 @@ def test_real_config_cli_override(): "install_components": ["linux_install"], "minimum_version": "3.15", "env": {"PATH": "$HOME/opt" + os.pathsep + "/usr/bin", "foo": "bar"}, - "pure_python": False, - "python_abi": "auto", - "abi3_minimum_cpython_version": 32, }, "windows": { "build_type": "Release", @@ -565,9 +582,6 @@ def test_real_config_cli_override(): "install_components": ["win_install"], "minimum_version": "3.15", "env": {"foo": "bar"}, - "pure_python": False, - "python_abi": "abi3", - "abi3_minimum_cpython_version": 32, }, "mac": { "build_type": "Release", @@ -587,7 +601,21 @@ def test_real_config_cli_override(): "install_components": [""], "minimum_version": "3.15", "env": {}, - "pure_python": False, + }, + } + assert conf.wheel == { + "linux": { + "python_tag": "auto", + "python_abi": "auto", + "abi3_minimum_cpython_version": 32, + }, + "windows": { + "python_tag": "auto", + "python_abi": "abi3", + "abi3_minimum_cpython_version": 32, + }, + "mac": { + "python_tag": "auto", "python_abi": "none", "abi3_minimum_cpython_version": 32, },