Skip to content

Commit 68690fe

Browse files
committed
use pooetry to manage deps for isolated build envs
Previously, isolated build environments needed for both package installation from sdist and for fallback package metadata inspection used to rely on inline scripts and delegated management of build requirements to the build package. With this change, Poetry manages the build requirements. This enables the use of build requirements from the current projects source, Poetry's configurations and also cache. We also, reduce the use of subprocesses and thereby increasing speeds of such builds.
1 parent 52b113d commit 68690fe

File tree

8 files changed

+279
-212
lines changed

8 files changed

+279
-212
lines changed

src/poetry/inspection/info.py

+11-42
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import functools
55
import glob
66
import logging
7+
import tempfile
78

89
from pathlib import Path
910
from typing import TYPE_CHECKING
@@ -13,7 +14,7 @@
1314

1415
import pkginfo
1516

16-
from poetry.core.factory import Factory
17+
from build import BuildBackendException
1718
from poetry.core.packages.dependency import Dependency
1819
from poetry.core.packages.package import Package
1920
from poetry.core.pyproject.toml import PyProjectTOML
@@ -22,9 +23,9 @@
2223
from poetry.core.version.markers import InvalidMarker
2324
from poetry.core.version.requirements import InvalidRequirement
2425

25-
from poetry.utils.env import EnvCommandError
26-
from poetry.utils.env import ephemeral_environment
26+
from poetry.factory import Factory
2727
from poetry.utils.helpers import extractall
28+
from poetry.utils.isolated_build import isolated_builder
2829
from poetry.utils.setup_reader import SetupReader
2930

3031

@@ -38,25 +39,6 @@
3839

3940
logger = logging.getLogger(__name__)
4041

41-
PEP517_META_BUILD = """\
42-
import build
43-
import build.env
44-
import pyproject_hooks
45-
46-
source = '{source}'
47-
dest = '{dest}'
48-
49-
with build.env.DefaultIsolatedEnv() as env:
50-
builder = build.ProjectBuilder.from_isolated_env(
51-
env, source, runner=pyproject_hooks.quiet_subprocess_runner
52-
)
53-
env.install(builder.build_system_requires)
54-
env.install(builder.get_requires_for_build('wheel'))
55-
builder.metadata_path(dest)
56-
"""
57-
58-
PEP517_META_BUILD_DEPS = ["build==1.1.1", "pyproject_hooks==1.0.0"]
59-
6042

6143
class PackageInfoError(ValueError):
6244
def __init__(self, path: Path, *reasons: BaseException | str) -> None:
@@ -577,28 +559,15 @@ def get_pep517_metadata(path: Path) -> PackageInfo:
577559
if all(x is not None for x in (info.version, info.name, info.requires_dist)):
578560
return info
579561

580-
with ephemeral_environment(
581-
flags={"no-pip": False, "no-setuptools": True, "no-wheel": True}
582-
) as venv:
583-
# TODO: cache PEP 517 build environment corresponding to each project venv
584-
dest_dir = venv.path.parent / "dist"
585-
dest_dir.mkdir()
562+
with tempfile.TemporaryDirectory() as dist:
563+
try:
564+
dest = Path(dist)
586565

587-
pep517_meta_build_script = PEP517_META_BUILD.format(
588-
source=path.as_posix(), dest=dest_dir.as_posix()
589-
)
566+
with isolated_builder(path, "wheel") as builder:
567+
builder.metadata_path(dest)
590568

591-
try:
592-
venv.run_pip(
593-
"install",
594-
"--disable-pip-version-check",
595-
"--ignore-installed",
596-
"--no-input",
597-
*PEP517_META_BUILD_DEPS,
598-
)
599-
venv.run_python_script(pep517_meta_build_script)
600-
info = PackageInfo.from_metadata_directory(dest_dir)
601-
except EnvCommandError as e:
569+
info = PackageInfo.from_metadata_directory(dest)
570+
except BuildBackendException as e:
602571
logger.debug("PEP517 build failed: %s", e)
603572
raise PackageInfoError(path, e, "PEP517 build failed")
604573

src/poetry/installation/chef.py

+28-117
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,20 @@
11
from __future__ import annotations
22

3-
import os
43
import tempfile
54

6-
from contextlib import redirect_stdout
7-
from io import StringIO
85
from pathlib import Path
96
from typing import TYPE_CHECKING
107

118
from build import BuildBackendException
12-
from build import ProjectBuilder
13-
from build.env import IsolatedEnv as BaseIsolatedEnv
149
from poetry.core.utils.helpers import temporary_directory
15-
from pyproject_hooks import quiet_subprocess_runner # type: ignore[import-untyped]
1610

1711
from poetry.utils._compat import decode
18-
from poetry.utils.env import ephemeral_environment
1912
from poetry.utils.helpers import extractall
13+
from poetry.utils.isolated_build import IsolatedBuildError
14+
from poetry.utils.isolated_build import isolated_builder
2015

2116

2217
if TYPE_CHECKING:
23-
from collections.abc import Collection
24-
2518
from poetry.repositories import RepositoryPool
2619
from poetry.utils.cache import ArtifactCache
2720
from poetry.utils.env import Env
@@ -30,78 +23,6 @@
3023
class ChefError(Exception): ...
3124

3225

33-
class ChefBuildError(ChefError): ...
34-
35-
36-
class ChefInstallError(ChefError):
37-
def __init__(self, requirements: Collection[str], output: str, error: str) -> None:
38-
message = "\n\n".join(
39-
(
40-
f"Failed to install {', '.join(requirements)}.",
41-
f"Output:\n{output}",
42-
f"Error:\n{error}",
43-
)
44-
)
45-
super().__init__(message)
46-
self._requirements = requirements
47-
48-
@property
49-
def requirements(self) -> Collection[str]:
50-
return self._requirements
51-
52-
53-
class IsolatedEnv(BaseIsolatedEnv):
54-
def __init__(self, env: Env, pool: RepositoryPool) -> None:
55-
self._env = env
56-
self._pool = pool
57-
58-
@property
59-
def python_executable(self) -> str:
60-
return str(self._env.python)
61-
62-
def make_extra_environ(self) -> dict[str, str]:
63-
path = os.environ.get("PATH")
64-
scripts_dir = str(self._env._bin_dir)
65-
return {
66-
"PATH": (
67-
os.pathsep.join([scripts_dir, path])
68-
if path is not None
69-
else scripts_dir
70-
)
71-
}
72-
73-
def install(self, requirements: Collection[str]) -> None:
74-
from cleo.io.buffered_io import BufferedIO
75-
from poetry.core.packages.dependency import Dependency
76-
from poetry.core.packages.project_package import ProjectPackage
77-
78-
from poetry.config.config import Config
79-
from poetry.installation.installer import Installer
80-
from poetry.packages.locker import Locker
81-
from poetry.repositories.installed_repository import InstalledRepository
82-
83-
# We build Poetry dependencies from the requirements
84-
package = ProjectPackage("__root__", "0.0.0")
85-
package.python_versions = ".".join(str(v) for v in self._env.version_info[:3])
86-
for requirement in requirements:
87-
dependency = Dependency.create_from_pep_508(requirement)
88-
package.add_dependency(dependency)
89-
90-
io = BufferedIO()
91-
installer = Installer(
92-
io,
93-
self._env,
94-
package,
95-
Locker(self._env.path.joinpath("poetry.lock"), {}),
96-
self._pool,
97-
Config.create(),
98-
InstalledRepository.load(self._env),
99-
)
100-
installer.update(True)
101-
if installer.run() != 0:
102-
raise ChefInstallError(requirements, io.fetch_output(), io.fetch_error())
103-
104-
10526
class Chef:
10627
def __init__(
10728
self, artifact_cache: ArtifactCache, env: Env, pool: RepositoryPool
@@ -127,46 +48,36 @@ def _prepare(
12748
) -> Path:
12849
from subprocess import CalledProcessError
12950

130-
with ephemeral_environment(
131-
self._env.python,
132-
flags={"no-pip": True, "no-setuptools": True, "no-wheel": True},
133-
) as venv:
134-
env = IsolatedEnv(venv, self._pool)
135-
builder = ProjectBuilder.from_isolated_env(
136-
env, directory, runner=quiet_subprocess_runner
137-
)
138-
env.install(builder.build_system_requires)
139-
140-
stdout = StringIO()
141-
error: Exception | None = None
142-
try:
143-
with redirect_stdout(stdout):
144-
dist_format = "wheel" if not editable else "editable"
145-
env.install(
146-
builder.build_system_requires
147-
| builder.get_requires_for_build(dist_format)
148-
)
149-
path = Path(
150-
builder.build(
151-
dist_format,
152-
destination.as_posix(),
153-
)
51+
distribution = "wheel" if not editable else "editable"
52+
error: Exception | None = None
53+
54+
try:
55+
with isolated_builder(
56+
source=directory,
57+
distribution=distribution,
58+
python_executable=self._env.python,
59+
pool=self._pool,
60+
) as builder:
61+
return Path(
62+
builder.build(
63+
distribution,
64+
destination.as_posix(),
15465
)
155-
except BuildBackendException as e:
156-
message_parts = [str(e)]
157-
if isinstance(e.exception, CalledProcessError):
158-
text = e.exception.stderr or e.exception.stdout
159-
if text is not None:
160-
message_parts.append(decode(text))
161-
else:
162-
message_parts.append(str(e.exception))
66+
)
67+
except BuildBackendException as e:
68+
message_parts = [str(e)]
16369

164-
error = ChefBuildError("\n\n".join(message_parts))
70+
if isinstance(e.exception, CalledProcessError):
71+
text = e.exception.stderr or e.exception.stdout
72+
if text is not None:
73+
message_parts.append(decode(text))
74+
else:
75+
message_parts.append(str(e.exception))
16576

166-
if error is not None:
167-
raise error from None
77+
error = IsolatedBuildError("\n\n".join(message_parts))
16878

169-
return path
79+
if error is not None:
80+
raise error from None
17081

17182
def _prepare_sdist(self, archive: Path, destination: Path | None = None) -> Path:
17283
from poetry.core.packages.utils.link import Link

src/poetry/installation/executor.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
from poetry.core.packages.utils.link import Link
1919

2020
from poetry.installation.chef import Chef
21-
from poetry.installation.chef import ChefBuildError
22-
from poetry.installation.chef import ChefInstallError
2321
from poetry.installation.chooser import Chooser
2422
from poetry.installation.operations import Install
2523
from poetry.installation.operations import Uninstall
@@ -34,6 +32,8 @@
3432
from poetry.utils.helpers import get_highest_priority_hash_type
3533
from poetry.utils.helpers import pluralize
3634
from poetry.utils.helpers import remove_directory
35+
from poetry.utils.isolated_build import IsolatedBuildError
36+
from poetry.utils.isolated_build import IsolatedBuildInstallError
3737
from poetry.utils.pip import pip_install
3838

3939

@@ -310,7 +310,7 @@ def _execute_operation(self, operation: Operation) -> None:
310310
trace = ExceptionTrace(e)
311311
trace.render(io)
312312
pkg = operation.package
313-
if isinstance(e, ChefBuildError):
313+
if isinstance(e, IsolatedBuildError):
314314
pip_command = "pip wheel --no-cache-dir --use-pep517"
315315
if pkg.develop:
316316
requirement = pkg.source_url
@@ -328,7 +328,7 @@ def _execute_operation(self, operation: Operation) -> None:
328328
f" running '{pip_command} \"{requirement}\"'."
329329
"</info>"
330330
)
331-
elif isinstance(e, ChefInstallError):
331+
elif isinstance(e, IsolatedBuildInstallError):
332332
message = (
333333
"<error>"
334334
"Cannot install build-system.requires"

0 commit comments

Comments
 (0)