Skip to content

Commit

Permalink
multi: refactor isolated env
Browse files Browse the repository at this point in the history
* `IsolatedEnv` reworked to subsume env creation logic.

* `IsolatedEnvBuilder` renamed to `IsolatedEnvManager`.

* `ProjectBuilder.from_isolated_env` added to auto-fill init args from
  an isolated env.

* Made the `IsolatedEnv` responsible for customising the environ as a prelude
  to fixing pypa#377.  Removed `ProjectBuilder.scripts_dir`.

* `ProjectBuilder`'s @properties made read-only. Mutating the builder is not
  necessary with the addition of `ProjectBuilder.from_isolated_env`.
  • Loading branch information
layday committed Nov 6, 2021
1 parent b160e76 commit e6e92c3
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 295 deletions.
68 changes: 29 additions & 39 deletions src/build/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""
build - A simple, correct PEP 517 build frontend
"""

__version__ = '0.7.0'

import contextlib
Expand All @@ -16,8 +17,8 @@
import warnings
import zipfile

from collections import OrderedDict
from typing import (
TYPE_CHECKING,
AbstractSet,
Any,
Callable,
Expand All @@ -36,6 +37,11 @@
import pep517.wrappers


if TYPE_CHECKING:
# Avoid import cycle.
from . import env


TOMLDecodeError: Type[Exception]
toml_loads: Callable[[str], MutableMapping[str, Any]]

Expand Down Expand Up @@ -231,12 +237,10 @@ def __init__(
self,
srcdir: PathType,
python_executable: str = sys.executable,
scripts_dir: Optional[str] = None,
runner: RunnerType = pep517.wrappers.default_subprocess_runner,
) -> None:
"""
:param srcdir: The source directory
:param scripts_dir: The location of the scripts dir (defaults to the folder where the python executable lives)
:param python_executable: The python executable where the backend lives
:param runner: An alternative runner for backend subprocesses
Expand Down Expand Up @@ -268,53 +272,39 @@ def __init__(
raise BuildException(f'Failed to parse {spec_file}: {e} ')

self._build_system = _parse_build_system_table(spec)
self._python_executable = python_executable
self._requires = set(self._build_system['requires'])
self._backend = self._build_system['build-backend']
self._scripts_dir = scripts_dir
self._hook_runner = runner
self._hook = pep517.wrappers.Pep517HookCaller(
self.srcdir,
self._backend,
backend_path=self._build_system.get('backend-path'),
python_executable=python_executable,
runner=self._runner,
runner=runner,
python_executable=self._python_executable,
)

def _runner(
self, cmd: Sequence[str], cwd: Optional[str] = None, extra_environ: Optional[Mapping[str, str]] = None
) -> None:
# if script dir is specified must be inserted at the start of PATH (avoid duplicate path while doing so)
if self.scripts_dir is not None:
paths: Dict[str, None] = OrderedDict()
paths[str(self.scripts_dir)] = None
if 'PATH' in os.environ:
paths.update((i, None) for i in os.environ['PATH'].split(os.pathsep))
extra_environ = {} if extra_environ is None else dict(extra_environ)
extra_environ['PATH'] = os.pathsep.join(paths)
self._hook_runner(cmd, cwd, extra_environ)

@property
def python_executable(self) -> str:
@classmethod
def from_isolated_env(cls, isolated_env: 'env.IsolatedEnv', srcdir: PathType) -> 'ProjectBuilder':
"""
The Python executable used to invoke the backend.
Instantiate the builder from an isolated environment.
:param isolated_env: The isolated environment instance
:param srcdir: The project source directory
"""
# make mypy happy
exe: str = self._hook.python_executable
return exe

@python_executable.setter
def python_executable(self, value: str) -> None:
self._hook.python_executable = value
def runner(cmd: Sequence[str], cwd: Optional[str] = None, extra_environ: Optional[Mapping[str, str]] = None) -> None:
env = dict(isolated_env.prepare_environ())
if extra_environ:
env.update(extra_environ)

@property
def scripts_dir(self) -> Optional[str]:
"""
The folder where the scripts are stored for the python executable.
"""
return self._scripts_dir
subprocess.check_call(cmd, cwd=cwd, env=env)

@scripts_dir.setter
def scripts_dir(self, value: Optional[str]) -> None:
self._scripts_dir = value
return cls(srcdir, python_executable=isolated_env.python_executable, runner=runner)

@property
def python_executable(self) -> str:
"""Path of Python executable used to invoke PEP 517 hooks."""
return self._python_executable

@property
def build_system_requires(self) -> Set[str]:
Expand All @@ -323,7 +313,7 @@ def build_system_requires(self) -> Set[str]:
``build-system.requires`` field or the default build dependencies
if ``pyproject.toml`` is missing or ``build-system`` is undefined.
"""
return set(self._build_system['requires'])
return self._requires

def get_requires_for_build(self, distribution: str, config_settings: Optional[ConfigSettingsType] = None) -> Set[str]:
"""
Expand Down
43 changes: 23 additions & 20 deletions src/build/__main__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# SPDX-License-Identifier: MIT


import argparse
import contextlib
import os
Expand All @@ -18,7 +17,7 @@
import build

from build import BuildBackendException, BuildException, ConfigSettingsType, PathType, ProjectBuilder
from build.env import IsolatedEnvBuilder
from build.env import IsolatedEnvManager


_COLORS = {
Expand Down Expand Up @@ -85,7 +84,7 @@ def log(message: str) -> None:
print('{bold}* {}{reset}'.format(message, **_STYLES))


class _IsolatedEnvBuilder(IsolatedEnvBuilder):
class _IsolatedEnvManager(IsolatedEnvManager):
@staticmethod
def log(message: str) -> None:
print('{bold}* {}{reset}'.format(message, **_STYLES))
Expand All @@ -96,25 +95,25 @@ def _format_dep_chain(dep_chain: Sequence[str]) -> str:


def _build_in_isolated_env(
builder: ProjectBuilder, outdir: PathType, distribution: str, config_settings: Optional[ConfigSettingsType]
srcdir: PathType, outdir: PathType, distribution: str, config_settings: Optional[ConfigSettingsType]
) -> str:
with _IsolatedEnvBuilder() as env:
builder.python_executable = env.executable
builder.scripts_dir = env.scripts_dir
with _IsolatedEnvManager() as env:
builder = _ProjectBuilder.from_isolated_env(env, srcdir)
# first install the build dependencies
env.install(builder.build_system_requires)
env.install_packages(builder.build_system_requires)
# then get the extra required dependencies from the backend (which was installed in the call above :P)
env.install(builder.get_requires_for_build(distribution))
env.install_packages(builder.get_requires_for_build(distribution))
return builder.build(distribution, outdir, config_settings or {})


def _build_in_current_env(
builder: ProjectBuilder,
srcdir: PathType,
outdir: PathType,
distribution: str,
config_settings: Optional[ConfigSettingsType],
skip_dependency_check: bool = False,
) -> str:
builder = _ProjectBuilder(srcdir)
if not skip_dependency_check:
missing = builder.check_dependencies(distribution)
if missing:
Expand All @@ -127,16 +126,16 @@ def _build_in_current_env(

def _build(
isolation: bool,
builder: ProjectBuilder,
srcdir: PathType,
outdir: PathType,
distribution: str,
config_settings: Optional[ConfigSettingsType],
skip_dependency_check: bool,
) -> str:
if isolation:
return _build_in_isolated_env(builder, outdir, distribution, config_settings)
return _build_in_isolated_env(srcdir, outdir, distribution, config_settings)
else:
return _build_in_current_env(builder, outdir, distribution, config_settings, skip_dependency_check)
return _build_in_current_env(srcdir, outdir, distribution, config_settings, skip_dependency_check)


@contextlib.contextmanager
Expand Down Expand Up @@ -194,9 +193,8 @@ def build_package(
:param skip_dependency_check: Do not perform the dependency check
"""
built: List[str] = []
builder = _ProjectBuilder(srcdir)
for distribution in distributions:
out = _build(isolation, builder, outdir, distribution, config_settings, skip_dependency_check)
out = _build(isolation, srcdir, outdir, distribution, config_settings, skip_dependency_check)
built.append(os.path.basename(out))
return built

Expand All @@ -222,8 +220,7 @@ def build_package_via_sdist(
if 'sdist' in distributions:
raise ValueError('Only binary distributions are allowed but sdist was specified')

builder = _ProjectBuilder(srcdir)
sdist = _build(isolation, builder, outdir, 'sdist', config_settings, skip_dependency_check)
sdist = _build(isolation, srcdir, outdir, 'sdist', config_settings, skip_dependency_check)

sdist_name = os.path.basename(sdist)
sdist_out = tempfile.mkdtemp(prefix='build-via-sdist-')
Expand All @@ -232,11 +229,17 @@ def build_package_via_sdist(
with tarfile.open(sdist) as t:
t.extractall(sdist_out)
try:
builder = _ProjectBuilder(os.path.join(sdist_out, sdist_name[: -len('.tar.gz')]))
if distributions:
builder.log(f'Building {_natural_language_list(distributions)} from sdist')
_ProjectBuilder.log(f'Building {_natural_language_list(distributions)} from sdist')
for distribution in distributions:
out = _build(isolation, builder, outdir, distribution, config_settings, skip_dependency_check)
out = _build(
isolation,
os.path.join(sdist_out, sdist_name[: -len('.tar.gz')]),
outdir,
distribution,
config_settings,
skip_dependency_check,
)
built.append(os.path.basename(out))
finally:
shutil.rmtree(sdist_out, ignore_errors=True)
Expand Down
Loading

0 comments on commit e6e92c3

Please sign in to comment.