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