Skip to content

Commit

Permalink
build.script: use build environment for execution
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
abn committed Apr 2, 2022
1 parent 720c358 commit 58ae681
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 11 deletions.
23 changes: 16 additions & 7 deletions src/poetry/console/commands/build.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from cleo.helpers import option

from poetry.console.commands.env_command import EnvCommand
from poetry.utils.env import build_environment


if TYPE_CHECKING:
from poetry.utils.env import Env


class BuildCommand(EnvCommand):
Expand All @@ -23,11 +30,13 @@ 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 <c1>{package.pretty_name}</c1> (<c2>{package.version}</c2>)"
)
env: Env
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 <c1>{package.pretty_name}</c1> (<c2>{package.version}</c2>)"
)

builder = Builder(self.poetry)
builder.build(fmt, executable=self.env.python)
builder = Builder(self.poetry)
builder.build(fmt, executable=env.python) # type: ignore[attr-defined]
11 changes: 8 additions & 3 deletions src/poetry/masonry/builders/editable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """\
Expand Down Expand Up @@ -75,8 +76,12 @@ def build(self) -> None:
self._add_dist_info(added_files)

def _run_build_script(self, build_script: Path) -> None:
self._debug(f" - Executing build script: <b>{build_script}</b>")
self._env.run("python", str(self._path.joinpath(build_script)), call=True)
env: Env
with build_environment(poetry=self._poetry, env=self._env, io=self._io) as env:
self._debug(f" - Executing build script: <b>{build_script}</b>")
env.run( # type: ignore[attr-defined]
"python", str(self._path.joinpath(build_script)), call=True
)

def _setup_build(self) -> None:
builder = SdistBuilder(self._poetry)
Expand Down
40 changes: 40 additions & 0 deletions src/poetry/utils/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -1852,6 +1852,46 @@ def ephemeral_environment(
yield VirtualEnv(venv_dir, venv_dir)


@contextmanager
def build_environment(
poetry: Poetry, env: Env | None = None, io: IO | None = None
) -> ContextManager[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:
requires = map(
lambda r: f"<c1>{r}</c1>", poetry.pyproject.build_system.requires
)
if not overwrite:
io.write_line("")

io.overwrite(
"<b>Preparing</b> 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,
Expand Down
4 changes: 4 additions & 0 deletions tests/fixtures/extended_project_without_setup/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
6 changes: 5 additions & 1 deletion tests/masonry/builders/test_editable_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,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()
Expand Down
50 changes: 50 additions & 0 deletions tests/utils/test_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


if TYPE_CHECKING:
Expand Down Expand Up @@ -1270,3 +1272,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

0 comments on commit 58ae681

Please sign in to comment.