diff --git a/src/poetry/console/commands/build.py b/src/poetry/console/commands/build.py
index 9d5545326ca..a691aed0c99 100644
--- a/src/poetry/console/commands/build.py
+++ b/src/poetry/console/commands/build.py
@@ -3,6 +3,7 @@
from cleo.helpers import option
from poetry.console.commands.env_command import EnvCommand
+from poetry.utils.env import build_environment
class BuildCommand(EnvCommand):
@@ -23,11 +24,12 @@ class BuildCommand(EnvCommand):
def handle(self) -> None:
from poetry.core.masonry.builder import Builder
- fmt = self.option("format") or "all"
- package = self.poetry.package
- self.line(
- f"Building {package.pretty_name} ({package.version})"
- )
+ with build_environment(poetry=self.poetry, env=self.env, io=self.io) as env:
+ fmt = self.option("format") or "all"
+ package = self.poetry.package
+ self.line(
+ f"Building {package.pretty_name} ({package.version})"
+ )
- builder = Builder(self.poetry)
- builder.build(fmt, executable=self.env.python)
+ builder = Builder(self.poetry)
+ builder.build(fmt, executable=env.python)
diff --git a/src/poetry/masonry/builders/editable.py b/src/poetry/masonry/builders/editable.py
index 73329b480ec..de4ce1ca5c7 100644
--- a/src/poetry/masonry/builders/editable.py
+++ b/src/poetry/masonry/builders/editable.py
@@ -15,14 +15,15 @@
from poetry.utils._compat import WINDOWS
from poetry.utils._compat import decode
+from poetry.utils.env import build_environment
from poetry.utils.helpers import is_dir_writable
from poetry.utils.pip import pip_install
if TYPE_CHECKING:
from cleo.io.io import IO
- from poetry.core.poetry import Poetry
+ from poetry.poetry import Poetry
from poetry.utils.env import Env
SCRIPT_TEMPLATE = """\
@@ -75,8 +76,9 @@ def build(self) -> None:
self._add_dist_info(added_files)
def _run_build_script(self, build_script: str) -> None:
- self._debug(f" - Executing build script: {build_script}")
- self._env.run("python", str(self._path.joinpath(build_script)), call=True)
+ with build_environment(poetry=self._poetry, env=self._env, io=self._io) as env:
+ self._debug(f" - Executing build script: {build_script}")
+ env.run("python", str(self._path.joinpath(build_script)), call=True)
def _setup_build(self) -> None:
builder = SdistBuilder(self._poetry)
diff --git a/src/poetry/utils/env.py b/src/poetry/utils/env.py
index 856202172b4..3dfe27b2f36 100644
--- a/src/poetry/utils/env.py
+++ b/src/poetry/utils/env.py
@@ -20,6 +20,7 @@
from typing import ContextManager
from typing import Iterable
from typing import Iterator
+from typing import TypeVar
import packaging.tags
import tomlkit
@@ -30,6 +31,7 @@
from packaging.tags import interpreter_name
from packaging.tags import interpreter_version
from packaging.tags import sys_tags
+from poetry.core.poetry import Poetry
from poetry.core.semver.helpers import parse_constraint
from poetry.core.semver.version import Version
from poetry.core.toml.file import TOMLFile
@@ -50,7 +52,8 @@
from cleo.io.io import IO
from poetry.core.version.markers import BaseMarker
- from poetry.poetry import Poetry
+
+P = TypeVar("P", bound=Poetry)
GET_SYS_TAGS = f"""
@@ -494,7 +497,7 @@ class EnvManager:
ENVS_FILE = "envs.toml"
- def __init__(self, poetry: Poetry) -> None:
+ def __init__(self, poetry: P) -> None:
self._poetry = poetry
def _full_python_path(self, python: str) -> str:
@@ -1839,6 +1842,48 @@ def ephemeral_environment(
yield VirtualEnv(venv_dir, venv_dir)
+@contextmanager
+def build_environment(
+ poetry: P, env: Env | None = None, io: IO | None = None
+) -> Iterator[Env]:
+ """
+ If a build script is specified for the project, there could be additional build
+ time dependencies, eg: cython, setuptools etc. In these cases, we create an
+ ephemeral build environment with all requirements specified under
+ `build-system.requires` and return this. Otherwise, the given default project
+ environment is returned.
+ """
+ if not env or poetry.package.build_script:
+ with ephemeral_environment(executable=env.python if env else None) as venv:
+ overwrite = io and io.output.is_decorated() and not io.is_debug()
+ if io:
+ if not overwrite:
+ io.write_line("")
+
+ requires = [
+ f"{requirement}"
+ for requirement in poetry.pyproject.build_system.requires
+ ]
+
+ io.overwrite(
+ "Preparing build environment with build-system requirements"
+ f" {', '.join(requires)}"
+ )
+ venv.run_pip(
+ "install",
+ "--disable-pip-version-check",
+ "--ignore-installed",
+ *poetry.pyproject.build_system.requires,
+ )
+
+ if overwrite:
+ io.write_line("")
+
+ yield venv
+ else:
+ yield env
+
+
class MockEnv(NullEnv):
def __init__(
self,
diff --git a/tests/fixtures/extended_project_without_setup/pyproject.toml b/tests/fixtures/extended_project_without_setup/pyproject.toml
index 5c9dc2774c7..b3f4818f29c 100644
--- a/tests/fixtures/extended_project_without_setup/pyproject.toml
+++ b/tests/fixtures/extended_project_without_setup/pyproject.toml
@@ -27,3 +27,7 @@ generate-setup-file = false
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.4"
+
+[build-system]
+requires = ["poetry-core", "cython"]
+build-backend = "poetry.core.masonry.api"
diff --git a/tests/masonry/builders/test_editable_builder.py b/tests/masonry/builders/test_editable_builder.py
index aa34a6b7808..64b52bddc9a 100644
--- a/tests/masonry/builders/test_editable_builder.py
+++ b/tests/masonry/builders/test_editable_builder.py
@@ -267,9 +267,13 @@ def test_builder_installs_proper_files_when_packages_configured(
def test_builder_should_execute_build_scripts(
- extended_without_setup_poetry: Poetry, tmp_dir: str
+ mocker: MockerFixture, extended_without_setup_poetry: Poetry, tmp_dir: str
):
env = MockEnv(path=Path(tmp_dir) / "foo")
+ mocker.patch(
+ "poetry.masonry.builders.editable.build_environment"
+ ).return_value.__enter__.return_value = env
+
builder = EditableBuilder(extended_without_setup_poetry, env, NullIO())
builder.build()
diff --git a/tests/utils/test_env.py b/tests/utils/test_env.py
index 9db341e3ba8..736df81bb9e 100644
--- a/tests/utils/test_env.py
+++ b/tests/utils/test_env.py
@@ -25,9 +25,11 @@
from poetry.utils.env import EnvManager
from poetry.utils.env import GenericEnv
from poetry.utils.env import InvalidCurrentPythonVersionError
+from poetry.utils.env import MockEnv
from poetry.utils.env import NoCompatiblePythonVersionFound
from poetry.utils.env import SystemEnv
from poetry.utils.env import VirtualEnv
+from poetry.utils.env import build_environment
from poetry.utils.helpers import remove_directory
@@ -1331,3 +1333,51 @@ def test_generate_env_name_ignores_case_for_case_insensitive_fs(tmp_dir: str):
assert venv_name1 == venv_name2
else:
assert venv_name1 != venv_name2
+
+
+@pytest.fixture()
+def extended_without_setup_poetry() -> Poetry:
+ poetry = Factory().create_poetry(
+ Path(__file__).parent.parent / "fixtures" / "extended_project_without_setup"
+ )
+
+ return poetry
+
+
+def test_build_environment_called_build_script_specified(
+ mocker: MockerFixture, extended_without_setup_poetry: Poetry, tmp_dir: str
+):
+ project_env = MockEnv(path=Path(tmp_dir) / "project")
+ ephemeral_env = MockEnv(path=Path(tmp_dir) / "ephemeral")
+
+ mocker.patch(
+ "poetry.utils.env.ephemeral_environment"
+ ).return_value.__enter__.return_value = ephemeral_env
+
+ with build_environment(extended_without_setup_poetry, project_env) as env:
+ assert env == ephemeral_env
+ assert env.executed == [
+ [
+ "python",
+ env.pip_embedded,
+ "install",
+ "--disable-pip-version-check",
+ "--ignore-installed",
+ *extended_without_setup_poetry.pyproject.build_system.requires,
+ ]
+ ]
+
+
+def test_build_environment_not_called_without_build_script_specified(
+ mocker: MockerFixture, poetry: Poetry, tmp_dir: str
+):
+ project_env = MockEnv(path=Path(tmp_dir) / "project")
+ ephemeral_env = MockEnv(path=Path(tmp_dir) / "ephemeral")
+
+ mocker.patch(
+ "poetry.utils.env.ephemeral_environment"
+ ).return_value.__enter__.return_value = ephemeral_env
+
+ with build_environment(poetry, project_env) as env:
+ assert env == project_env
+ assert not env.executed