1
1
Fork 0
mirror of https://github.com/pypa/pip synced 2023-12-13 21:30:23 +01:00

Speed up build environment creation

Instead of creating a zip file from the current pip's sources, add the
current copy of pip, to the build environment's interpreter's import
system using `sys.meta_path`. This avoids the overhead of creating the
zipfile, allows us to use the current pip's sources as-is,
meaningfully reduces the size of the build environment and
speeds up the creation of the build environment.
This commit is contained in:
Pradyun Gedam 2022-07-14 13:41:58 +01:00
parent c0fb4bf1ad
commit d36bd5a96e
No known key found for this signature in database
GPG key ID: FF99710C4332258E
2 changed files with 36 additions and 15 deletions

3
news/11257.feature.rst Normal file
View file

@ -0,0 +1,3 @@
Significantly speed up isolated environment creation, by using the same
sources for pip instead of creating a standalone installation for each
environment.

View file

@ -7,7 +7,6 @@ import os
import pathlib
import sys
import textwrap
import zipfile
from collections import OrderedDict
from sysconfig import get_paths
from types import TracebackType
@ -29,6 +28,29 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
PIP_RUNNER = """
import importlib.util
import os
import runpy
import sys
class PipImportRedirectingFinder:
@classmethod
def find_spec(cls, fullname, path=None, target=None):
if not fullname.startswith("pip."):
return None
# Import pip from the current source directory
location = os.path.join({source!r}, *fullname.split("."))
return importlib.util.spec_from_file_location(fullname, location)
sys.meta_path.insert(0, PipImportRedirectingFinder())
runpy.run_module("pip", run_name="__main__")
"""
class _Prefix:
def __init__(self, path: str) -> None:
@ -42,29 +64,25 @@ class _Prefix:
@contextlib.contextmanager
def _create_standalone_pip() -> Generator[str, None, None]:
"""Create a "standalone pip" zip file.
def _create_runnable_pip() -> Generator[str, None, None]:
"""Create a "pip runner" file.
The zip file's content is identical to the currently-running pip.
The runner file ensures that import for pip happens using the currently-running pip.
It will be used to install requirements into the build environment.
"""
source = pathlib.Path(pip_location).resolve().parent
# Return the current instance if `source` is not a directory. We can't build
# a zip from this, and it likely means the instance is already standalone.
# Return the current instance if `source` is not a directory. It likely
# means that this copy of pip is already standalone.
if not source.is_dir():
yield str(source)
return
with TempDirectory(kind="standalone-pip") as tmp_dir:
pip_zip = os.path.join(tmp_dir.path, "__env_pip__.zip")
kwargs = {}
if sys.version_info >= (3, 8):
kwargs["strict_timestamps"] = False
with zipfile.ZipFile(pip_zip, "w", **kwargs) as zf:
for child in source.rglob("*"):
zf.write(child, child.relative_to(source.parent).as_posix())
yield os.path.join(pip_zip, "pip")
pip_runner = os.path.join(tmp_dir.path, "__pip-runner__.py")
with open(pip_runner, "w", encoding="utf8") as f:
f.write(PIP_RUNNER.format(source=os.fsdecode(source)))
yield pip_runner
class BuildEnvironment:
@ -206,7 +224,7 @@ class BuildEnvironment:
if not requirements:
return
with contextlib.ExitStack() as ctx:
pip_runnable = ctx.enter_context(_create_standalone_pip())
pip_runnable = ctx.enter_context(_create_runnable_pip())
self._install_requirements(
pip_runnable,
finder,