diff --git a/README.md b/README.md index b6a978865..cdd561aa5 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,10 @@ build.targets = [] # Verbose printout when building. build.verbose = false +# Additional ``build-system.requires``. Intended to be used in combination with +# ``overrides``. +build.requires = [] + # The components to install. If empty, all default components are installed. install.components = [] diff --git a/docs/configuration/dynamic.md b/docs/configuration/dynamic.md index 49c8a4931..f46d44bd9 100644 --- a/docs/configuration/dynamic.md +++ b/docs/configuration/dynamic.md @@ -112,6 +112,45 @@ metadata.readme.provider = "scikit_build_core.metadata.fancy_pypi_readme" # tool.hatch.metadata.hooks.fancy-pypi-readme options here ``` +## `build-system.requires`: Scikit-build-core's `build.requires` + +If you need to inject and manipulate additional `build-system.requires`, you can +use the `build.requires`. This is intended to be used in combination with +[](./overrides.md). + +This is not technically a dynamic metadata and thus does not have to have the +`dynamic` field defined, and it is not defined under the `metadata` table, but +similar to the other dynamic metadata it injects the additional +`build-system.requires`. + +```toml +[package] +name = "mypackage" + +[tool.scikit-build] +build.requires = ["foo"] + +[[tool.scikit-build.overrides]] +if.from-sdist = false +build.requires = ["foo @ {root:uri}/foo"] +``` + +This example shows a common use-case where the package has a default +`build-system.requires` pointing to the package `foo` in the PyPI index, but +when built from the original git checkout or equivalent, the local folder is +used as dependency instead by resolving the `{root:uri}` to a file uri pointing +to the folder where the `pyproject.toml` is located. + +```{note} +In order to be compliant with the package index, when building from `sdist`, the +`build.requires` **MUST NOT** have any `@` redirects. This rule may be later +enforced explicitly. +``` + +```{versionadded} 0.11 + +``` + ## Generate files with dynamic metadata You can write out metadata to file(s) as well. Other info might become available diff --git a/src/scikit_build_core/builder/get_requires.py b/src/scikit_build_core/builder/get_requires.py index be22448de..26b6d7c4b 100644 --- a/src/scikit_build_core/builder/get_requires.py +++ b/src/scikit_build_core/builder/get_requires.py @@ -5,6 +5,7 @@ import importlib.util import os import sysconfig +from pathlib import Path from typing import TYPE_CHECKING, Literal from packaging.tags import sys_tags @@ -66,6 +67,28 @@ def _load_scikit_build_settings( return SettingsReader.from_file("pyproject.toml", config_settings).settings +@dataclasses.dataclass() +class RootPathResolver: + """Handle ``{root:uri}`` like formatting similar to ``hatchling``.""" + + path: Path = dataclasses.field(default_factory=Path) + + def __post_init__(self) -> None: + self.path = self.path.resolve() + + def __format__(self, fmt: str) -> str: + command, _, rest = fmt.partition(":") + if command == "parent": + parent = RootPathResolver(self.path.parent) + return parent.__format__(rest) + if command == "uri" and rest == "": + return self.path.as_uri() + if command == "" and rest == "": + return str(self) + msg = f"Could not handle format: {fmt}" + raise ValueError(msg) + + @dataclasses.dataclass(frozen=True) class GetRequires: settings: ScikitBuildSettings = dataclasses.field( @@ -140,6 +163,9 @@ def dynamic_metadata(self) -> Generator[str, None, None]: if self.settings.fail: return + for build_require in self.settings.build.requires: + yield build_require.format(root=RootPathResolver()) + for dynamic_metadata in self.settings.metadata.values(): if "provider" in dynamic_metadata: config = dynamic_metadata.copy() diff --git a/src/scikit_build_core/resources/scikit-build.schema.json b/src/scikit_build_core/resources/scikit-build.schema.json index d8e87df88..4518f03ee 100644 --- a/src/scikit_build_core/resources/scikit-build.schema.json +++ b/src/scikit_build_core/resources/scikit-build.schema.json @@ -299,6 +299,13 @@ "type": "boolean", "default": false, "description": "Verbose printout when building." + }, + "requires": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional ``build-system.requires``. Intended to be used in combination with ``overrides``." } } }, @@ -560,6 +567,9 @@ }, "targets": { "$ref": "#/$defs/inherit" + }, + "requires": { + "$ref": "#/$defs/inherit" } } }, diff --git a/src/scikit_build_core/settings/skbuild_model.py b/src/scikit_build_core/settings/skbuild_model.py index af1c7c4f2..9f8c8ddf2 100644 --- a/src/scikit_build_core/settings/skbuild_model.py +++ b/src/scikit_build_core/settings/skbuild_model.py @@ -277,6 +277,12 @@ class BuildSettings: Verbose printout when building. """ + requires: List[str] = dataclasses.field(default_factory=list) + """ + Additional ``build-system.requires``. Intended to be used in combination + with ``overrides``. + """ + @dataclasses.dataclass class InstallSettings: diff --git a/tests/packages/dynamic_metadata/build_requires_project.toml b/tests/packages/dynamic_metadata/build_requires_project.toml new file mode 100644 index 000000000..9bf36232e --- /dev/null +++ b/tests/packages/dynamic_metadata/build_requires_project.toml @@ -0,0 +1,13 @@ +[build-system] +requires = ["scikit-build-core"] +build-backend = "scikit_build_core.build" + +[project] +name = "more_build_requires" + +[tool.scikit-build] +build.requires = ["foo"] + +[[tool.scikit-build.overrides]] +if.env.LOCAL_FOO = true +build.requires = ["foo @ {root:parent:uri}/foo"] diff --git a/tests/test_dynamic_metadata.py b/tests/test_dynamic_metadata.py index de0f0054e..842c14875 100644 --- a/tests/test_dynamic_metadata.py +++ b/tests/test_dynamic_metadata.py @@ -7,7 +7,7 @@ import types import zipfile from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any import pytest from packaging.version import Version @@ -22,6 +22,9 @@ from pathutils import contained +if TYPE_CHECKING: + from typing import Literal + # these are mock plugins returning known results # it turns out to be easier to create EntryPoint objects pointing to real @@ -345,3 +348,41 @@ def test_regex_remove( ) assert version == ("1.2.3dev1" if dev else "1.2.3") + + +@pytest.mark.usefixtures("package_dynamic_metadata") +@pytest.mark.parametrize("override", [None, "env", "sdist"]) +def test_build_requires_field(override, monkeypatch) -> None: + shutil.copy("build_requires_project.toml", "pyproject.toml") + + if override == "env": + monkeypatch.setenv("LOCAL_FOO", "True") + else: + monkeypatch.delenv("LOCAL_FOO", raising=False) + + pyproject_path = Path("pyproject.toml") + with pyproject_path.open("rb") as ft: + pyproject = tomllib.load(ft) + state: Literal["sdist", "metadata_wheel"] = ( + "sdist" if override == "sdist" else "metadata_wheel" + ) + settings_reader = SettingsReader(pyproject, {}, state=state) + + settings_reader.validate_may_exit() + + if override is None: + assert set(GetRequires().dynamic_metadata()) == { + "foo", + } + elif override == "env": + # evaluate ../foo as uri + foo_path = pyproject_path.absolute().parent.parent / "foo" + foo_path = foo_path.absolute() + assert set(GetRequires().dynamic_metadata()) == { + f"foo @ {foo_path.as_uri()}", + } + elif override == "sdist": + assert set(GetRequires().dynamic_metadata()) == { + # TODO: Check if special handling should be done for sdist + "foo", + }