Skip to content

Commit 76cbc06

Browse files
authored
feat: add new field build.build-requires (#992)
TODO: - [x] Define a new field to be inserted in `build-system.requires` - [x] Document new feature - [x] Add support for `{root:uri}` - should we copy hatchling's implementation or resolve it only if `hatchling` is added to dependency? - [ ] ~~Do we add a special case for `sdist` building or document that the user should define it?~~ Postponed because it would require a more involved process to validate. --------- Signed-off-by: Cristian Le <git@lecris.dev>
1 parent 42f82b4 commit 76cbc06

File tree

7 files changed

+140
-1
lines changed

7 files changed

+140
-1
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,10 @@ build.targets = []
281281
# Verbose printout when building.
282282
build.verbose = false
283283

284+
# Additional ``build-system.requires``. Intended to be used in combination with
285+
# ``overrides``.
286+
build.requires = []
287+
284288
# The components to install. If empty, all default components are installed.
285289
install.components = []
286290

docs/configuration/dynamic.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,45 @@ metadata.readme.provider = "scikit_build_core.metadata.fancy_pypi_readme"
112112
# tool.hatch.metadata.hooks.fancy-pypi-readme options here
113113
```
114114

115+
## `build-system.requires`: Scikit-build-core's `build.requires`
116+
117+
If you need to inject and manipulate additional `build-system.requires`, you can
118+
use the `build.requires`. This is intended to be used in combination with
119+
[](./overrides.md).
120+
121+
This is not technically a dynamic metadata and thus does not have to have the
122+
`dynamic` field defined, and it is not defined under the `metadata` table, but
123+
similar to the other dynamic metadata it injects the additional
124+
`build-system.requires`.
125+
126+
```toml
127+
[package]
128+
name = "mypackage"
129+
130+
[tool.scikit-build]
131+
build.requires = ["foo"]
132+
133+
[[tool.scikit-build.overrides]]
134+
if.from-sdist = false
135+
build.requires = ["foo @ {root:uri}/foo"]
136+
```
137+
138+
This example shows a common use-case where the package has a default
139+
`build-system.requires` pointing to the package `foo` in the PyPI index, but
140+
when built from the original git checkout or equivalent, the local folder is
141+
used as dependency instead by resolving the `{root:uri}` to a file uri pointing
142+
to the folder where the `pyproject.toml` is located.
143+
144+
```{note}
145+
In order to be compliant with the package index, when building from `sdist`, the
146+
`build.requires` **MUST NOT** have any `@` redirects. This rule may be later
147+
enforced explicitly.
148+
```
149+
150+
```{versionadded} 0.11
151+
152+
```
153+
115154
## Generate files with dynamic metadata
116155

117156
You can write out metadata to file(s) as well. Other info might become available

src/scikit_build_core/builder/get_requires.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import importlib.util
66
import os
77
import sysconfig
8+
from pathlib import Path
89
from typing import TYPE_CHECKING, Literal
910

1011
from packaging.tags import sys_tags
@@ -66,6 +67,28 @@ def _load_scikit_build_settings(
6667
return SettingsReader.from_file("pyproject.toml", config_settings).settings
6768

6869

70+
@dataclasses.dataclass()
71+
class RootPathResolver:
72+
"""Handle ``{root:uri}`` like formatting similar to ``hatchling``."""
73+
74+
path: Path = dataclasses.field(default_factory=Path)
75+
76+
def __post_init__(self) -> None:
77+
self.path = self.path.resolve()
78+
79+
def __format__(self, fmt: str) -> str:
80+
command, _, rest = fmt.partition(":")
81+
if command == "parent":
82+
parent = RootPathResolver(self.path.parent)
83+
return parent.__format__(rest)
84+
if command == "uri" and rest == "":
85+
return self.path.as_uri()
86+
if command == "" and rest == "":
87+
return str(self)
88+
msg = f"Could not handle format: {fmt}"
89+
raise ValueError(msg)
90+
91+
6992
@dataclasses.dataclass(frozen=True)
7093
class GetRequires:
7194
settings: ScikitBuildSettings = dataclasses.field(
@@ -140,6 +163,9 @@ def dynamic_metadata(self) -> Generator[str, None, None]:
140163
if self.settings.fail:
141164
return
142165

166+
for build_require in self.settings.build.requires:
167+
yield build_require.format(root=RootPathResolver())
168+
143169
for dynamic_metadata in self.settings.metadata.values():
144170
if "provider" in dynamic_metadata:
145171
config = dynamic_metadata.copy()

src/scikit_build_core/resources/scikit-build.schema.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,13 @@
299299
"type": "boolean",
300300
"default": false,
301301
"description": "Verbose printout when building."
302+
},
303+
"requires": {
304+
"type": "array",
305+
"items": {
306+
"type": "string"
307+
},
308+
"description": "Additional ``build-system.requires``. Intended to be used in combination with ``overrides``."
302309
}
303310
}
304311
},
@@ -560,6 +567,9 @@
560567
},
561568
"targets": {
562569
"$ref": "#/$defs/inherit"
570+
},
571+
"requires": {
572+
"$ref": "#/$defs/inherit"
563573
}
564574
}
565575
},

src/scikit_build_core/settings/skbuild_model.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,12 @@ class BuildSettings:
277277
Verbose printout when building.
278278
"""
279279

280+
requires: List[str] = dataclasses.field(default_factory=list)
281+
"""
282+
Additional ``build-system.requires``. Intended to be used in combination
283+
with ``overrides``.
284+
"""
285+
280286

281287
@dataclasses.dataclass
282288
class InstallSettings:
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[build-system]
2+
requires = ["scikit-build-core"]
3+
build-backend = "scikit_build_core.build"
4+
5+
[project]
6+
name = "more_build_requires"
7+
8+
[tool.scikit-build]
9+
build.requires = ["foo"]
10+
11+
[[tool.scikit-build.overrides]]
12+
if.env.LOCAL_FOO = true
13+
build.requires = ["foo @ {root:parent:uri}/foo"]

tests/test_dynamic_metadata.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import types
88
import zipfile
99
from pathlib import Path
10-
from typing import Any
10+
from typing import TYPE_CHECKING, Any
1111

1212
import pytest
1313
from packaging.version import Version
@@ -22,6 +22,9 @@
2222

2323
from pathutils import contained
2424

25+
if TYPE_CHECKING:
26+
from typing import Literal
27+
2528

2629
# these are mock plugins returning known results
2730
# it turns out to be easier to create EntryPoint objects pointing to real
@@ -345,3 +348,41 @@ def test_regex_remove(
345348
)
346349

347350
assert version == ("1.2.3dev1" if dev else "1.2.3")
351+
352+
353+
@pytest.mark.usefixtures("package_dynamic_metadata")
354+
@pytest.mark.parametrize("override", [None, "env", "sdist"])
355+
def test_build_requires_field(override, monkeypatch) -> None:
356+
shutil.copy("build_requires_project.toml", "pyproject.toml")
357+
358+
if override == "env":
359+
monkeypatch.setenv("LOCAL_FOO", "True")
360+
else:
361+
monkeypatch.delenv("LOCAL_FOO", raising=False)
362+
363+
pyproject_path = Path("pyproject.toml")
364+
with pyproject_path.open("rb") as ft:
365+
pyproject = tomllib.load(ft)
366+
state: Literal["sdist", "metadata_wheel"] = (
367+
"sdist" if override == "sdist" else "metadata_wheel"
368+
)
369+
settings_reader = SettingsReader(pyproject, {}, state=state)
370+
371+
settings_reader.validate_may_exit()
372+
373+
if override is None:
374+
assert set(GetRequires().dynamic_metadata()) == {
375+
"foo",
376+
}
377+
elif override == "env":
378+
# evaluate ../foo as uri
379+
foo_path = pyproject_path.absolute().parent.parent / "foo"
380+
foo_path = foo_path.absolute()
381+
assert set(GetRequires().dynamic_metadata()) == {
382+
f"foo @ {foo_path.as_uri()}",
383+
}
384+
elif override == "sdist":
385+
assert set(GetRequires().dynamic_metadata()) == {
386+
# TODO: Check if special handling should be done for sdist
387+
"foo",
388+
}

0 commit comments

Comments
 (0)