Skip to content

Commit 466b15e

Browse files
committed
build.script: use build environment for execution
With this change, Poetry now creates an ephemeral build environment with all requirements specified under `build-system.requires` when a build script is specified. Otherwise, project environment is reused.
1 parent 42cfc56 commit 466b15e

File tree

6 files changed

+118
-13
lines changed

6 files changed

+118
-13
lines changed

src/poetry/console/commands/build.py

+9-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from cleo.helpers import option
44

55
from poetry.console.commands.env_command import EnvCommand
6+
from poetry.utils.env import build_environment
67

78

89
class BuildCommand(EnvCommand):
@@ -23,11 +24,12 @@ class BuildCommand(EnvCommand):
2324
def handle(self) -> None:
2425
from poetry.core.masonry.builder import Builder
2526

26-
fmt = self.option("format") or "all"
27-
package = self.poetry.package
28-
self.line(
29-
f"Building <c1>{package.pretty_name}</c1> (<c2>{package.version}</c2>)"
30-
)
27+
with build_environment(poetry=self.poetry, env=self.env, io=self.io) as env:
28+
fmt = self.option("format") or "all"
29+
package = self.poetry.package
30+
self.line(
31+
f"Building <c1>{package.pretty_name}</c1> (<c2>{package.version}</c2>)"
32+
)
3133

32-
builder = Builder(self.poetry)
33-
builder.build(fmt, executable=self.env.python)
34+
builder = Builder(self.poetry)
35+
builder.build(fmt, executable=env.python)

src/poetry/masonry/builders/editable.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@
1515

1616
from poetry.utils._compat import WINDOWS
1717
from poetry.utils._compat import decode
18+
from poetry.utils.env import build_environment
1819
from poetry.utils.helpers import is_dir_writable
1920
from poetry.utils.pip import pip_install
2021

2122

2223
if TYPE_CHECKING:
2324
from cleo.io.io import IO
24-
from poetry.core.poetry import Poetry
2525

26+
from poetry.poetry import Poetry
2627
from poetry.utils.env import Env
2728

2829
SCRIPT_TEMPLATE = """\
@@ -75,8 +76,9 @@ def build(self) -> None:
7576
self._add_dist_info(added_files)
7677

7778
def _run_build_script(self, build_script: str) -> None:
78-
self._debug(f" - Executing build script: <b>{build_script}</b>")
79-
self._env.run("python", str(self._path.joinpath(build_script)), call=True)
79+
with build_environment(poetry=self._poetry, env=self._env, io=self._io) as env:
80+
self._debug(f" - Executing build script: <b>{build_script}</b>")
81+
env.run("python", str(self._path.joinpath(build_script)), call=True)
8082

8183
def _setup_build(self) -> None:
8284
builder = SdistBuilder(self._poetry)

src/poetry/utils/env.py

+45-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from typing import ContextManager
2121
from typing import Iterable
2222
from typing import Iterator
23+
from typing import TypeVar
2324

2425
import packaging.tags
2526
import tomlkit
@@ -30,6 +31,7 @@
3031
from packaging.tags import interpreter_name
3132
from packaging.tags import interpreter_version
3233
from packaging.tags import sys_tags
34+
from poetry.core.poetry import Poetry
3335
from poetry.core.semver.helpers import parse_constraint
3436
from poetry.core.semver.version import Version
3537
from poetry.core.toml.file import TOMLFile
@@ -50,7 +52,8 @@
5052
from cleo.io.io import IO
5153
from poetry.core.version.markers import BaseMarker
5254

53-
from poetry.poetry import Poetry
55+
56+
P = TypeVar("P", bound=Poetry)
5457

5558

5659
GET_SYS_TAGS = f"""
@@ -494,7 +497,7 @@ class EnvManager:
494497

495498
ENVS_FILE = "envs.toml"
496499

497-
def __init__(self, poetry: Poetry) -> None:
500+
def __init__(self, poetry: P) -> None:
498501
self._poetry = poetry
499502

500503
def _full_python_path(self, python: str) -> str:
@@ -1839,6 +1842,46 @@ def ephemeral_environment(
18391842
yield VirtualEnv(venv_dir, venv_dir)
18401843

18411844

1845+
@contextmanager
1846+
def build_environment(
1847+
poetry: P, env: Env | None = None, io: IO | None = None
1848+
) -> Iterator[Env]:
1849+
"""
1850+
If a build script is specified for the project, there could be additional build
1851+
time dependencies, eg: cython, setuptools etc. In these cases, we create an
1852+
ephemeral build environment with all requirements specified under
1853+
`build-system.requires` and return this. Otherwise, the given default project
1854+
environment is returned.
1855+
"""
1856+
if not env or poetry.package.build_script:
1857+
with ephemeral_environment(executable=env.python if env else None) as venv:
1858+
overwrite = io and io.output.is_decorated() and not io.is_debug()
1859+
if io:
1860+
requires = map(
1861+
lambda r: f"<c1>{r}</c1>", poetry.pyproject.build_system.requires
1862+
)
1863+
if not overwrite:
1864+
io.write_line("")
1865+
1866+
io.overwrite(
1867+
"<b>Preparing</b> build environment with build-system requirements"
1868+
f" {', '.join(requires)}"
1869+
)
1870+
venv.run_pip(
1871+
"install",
1872+
"--disable-pip-version-check",
1873+
"--ignore-installed",
1874+
*poetry.pyproject.build_system.requires,
1875+
)
1876+
1877+
if overwrite:
1878+
io.write_line("")
1879+
1880+
yield venv
1881+
else:
1882+
yield env
1883+
1884+
18421885
class MockEnv(NullEnv):
18431886
def __init__(
18441887
self,

tests/fixtures/extended_project_without_setup/pyproject.toml

+4
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,7 @@ generate-setup-file = false
2727
# Requirements
2828
[tool.poetry.dependencies]
2929
python = "~2.7 || ^3.4"
30+
31+
[build-system]
32+
requires = ["poetry-core", "cython"]
33+
build-backend = "poetry.core.masonry.api"

tests/masonry/builders/test_editable_builder.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -267,9 +267,13 @@ def test_builder_installs_proper_files_when_packages_configured(
267267

268268

269269
def test_builder_should_execute_build_scripts(
270-
extended_without_setup_poetry: Poetry, tmp_dir: str
270+
mocker: MockerFixture, extended_without_setup_poetry: Poetry, tmp_dir: str
271271
):
272272
env = MockEnv(path=Path(tmp_dir) / "foo")
273+
mocker.patch(
274+
"poetry.masonry.builders.editable.build_environment"
275+
).return_value.__enter__.return_value = env
276+
273277
builder = EditableBuilder(extended_without_setup_poetry, env, NullIO())
274278

275279
builder.build()

tests/utils/test_env.py

+50
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@
2525
from poetry.utils.env import EnvManager
2626
from poetry.utils.env import GenericEnv
2727
from poetry.utils.env import InvalidCurrentPythonVersionError
28+
from poetry.utils.env import MockEnv
2829
from poetry.utils.env import NoCompatiblePythonVersionFound
2930
from poetry.utils.env import SystemEnv
3031
from poetry.utils.env import VirtualEnv
32+
from poetry.utils.env import build_environment
3133
from poetry.utils.helpers import remove_directory
3234

3335

@@ -1331,3 +1333,51 @@ def test_generate_env_name_ignores_case_for_case_insensitive_fs(tmp_dir: str):
13311333
assert venv_name1 == venv_name2
13321334
else:
13331335
assert venv_name1 != venv_name2
1336+
1337+
1338+
@pytest.fixture()
1339+
def extended_without_setup_poetry() -> Poetry:
1340+
poetry = Factory().create_poetry(
1341+
Path(__file__).parent.parent / "fixtures" / "extended_project_without_setup"
1342+
)
1343+
1344+
return poetry
1345+
1346+
1347+
def test_build_environment_called_build_script_specified(
1348+
mocker: MockerFixture, extended_without_setup_poetry: Poetry, tmp_dir: str
1349+
):
1350+
project_env = MockEnv(path=Path(tmp_dir) / "project")
1351+
ephemeral_env = MockEnv(path=Path(tmp_dir) / "ephemeral")
1352+
1353+
mocker.patch(
1354+
"poetry.utils.env.ephemeral_environment"
1355+
).return_value.__enter__.return_value = ephemeral_env
1356+
1357+
with build_environment(extended_without_setup_poetry, project_env) as env:
1358+
assert env == ephemeral_env
1359+
assert env.executed == [
1360+
[
1361+
"python",
1362+
env.pip_embedded,
1363+
"install",
1364+
"--disable-pip-version-check",
1365+
"--ignore-installed",
1366+
*extended_without_setup_poetry.pyproject.build_system.requires,
1367+
]
1368+
]
1369+
1370+
1371+
def test_build_environment_not_called_without_build_script_specified(
1372+
mocker: MockerFixture, poetry: Poetry, tmp_dir: str
1373+
):
1374+
project_env = MockEnv(path=Path(tmp_dir) / "project")
1375+
ephemeral_env = MockEnv(path=Path(tmp_dir) / "ephemeral")
1376+
1377+
mocker.patch(
1378+
"poetry.utils.env.ephemeral_environment"
1379+
).return_value.__enter__.return_value = ephemeral_env
1380+
1381+
with build_environment(poetry, project_env) as env:
1382+
assert env == project_env
1383+
assert not env.executed

0 commit comments

Comments
 (0)