Skip to content

Commit ab9d076

Browse files
committed
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.
1 parent c0fb4bf commit ab9d076

File tree

2 files changed

+36
-15
lines changed

2 files changed

+36
-15
lines changed

news/11257.feature.rst

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

src/pip/_internal/build_env.py

+33-15
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import pathlib
88
import sys
99
import textwrap
10-
import zipfile
1110
from collections import OrderedDict
1211
from sysconfig import get_paths
1312
from types import TracebackType
@@ -29,6 +28,29 @@
2928

3029
logger = logging.getLogger(__name__)
3130

31+
PIP_RUNNER = """
32+
import importlib.util
33+
import os
34+
import runpy
35+
import sys
36+
37+
38+
class PipImportRedirectingFinder:
39+
40+
@classmethod
41+
def find_spec(cls, fullname, path=None, target=None):
42+
if not fullname.startswith("pip."):
43+
return None
44+
45+
# Import pip from the current source directory
46+
location = os.path.join({source!r}, *fullname.split("."))
47+
return importlib.util.spec_from_file_location(fullname, location)
48+
49+
50+
sys.meta_path.insert(0, PipImportRedirectingFinder())
51+
runpy.run_module("pip", run_name="__main__")
52+
"""
53+
3254

3355
class _Prefix:
3456
def __init__(self, path: str) -> None:
@@ -42,29 +64,25 @@ def __init__(self, path: str) -> None:
4264

4365

4466
@contextlib.contextmanager
45-
def _create_standalone_pip() -> Generator[str, None, None]:
46-
"""Create a "standalone pip" zip file.
67+
def _create_runnable_pip() -> Generator[str, None, None]:
68+
"""Create a "pip runner" file.
4769
48-
The zip file's content is identical to the currently-running pip.
70+
The runner file ensures that import for pip happen using the currently-running pip.
4971
It will be used to install requirements into the build environment.
5072
"""
5173
source = pathlib.Path(pip_location).resolve().parent
5274

53-
# Return the current instance if `source` is not a directory. We can't build
54-
# a zip from this, and it likely means the instance is already standalone.
75+
# Return the current instance if `source` is not a directory. It likely
76+
# means that this executable is already standalone.
5577
if not source.is_dir():
5678
yield str(source)
5779
return
5880

5981
with TempDirectory(kind="standalone-pip") as tmp_dir:
60-
pip_zip = os.path.join(tmp_dir.path, "__env_pip__.zip")
61-
kwargs = {}
62-
if sys.version_info >= (3, 8):
63-
kwargs["strict_timestamps"] = False
64-
with zipfile.ZipFile(pip_zip, "w", **kwargs) as zf:
65-
for child in source.rglob("*"):
66-
zf.write(child, child.relative_to(source.parent).as_posix())
67-
yield os.path.join(pip_zip, "pip")
82+
pip_runner = os.path.join(tmp_dir.path, "__pip-runner__.py")
83+
with open(pip_runner, "w", encoding="utf8") as f:
84+
f.write(PIP_RUNNER.format(source=os.fsdecode(source)))
85+
yield pip_runner
6886

6987

7088
class BuildEnvironment:
@@ -206,7 +224,7 @@ def install_requirements(
206224
if not requirements:
207225
return
208226
with contextlib.ExitStack() as ctx:
209-
pip_runnable = ctx.enter_context(_create_standalone_pip())
227+
pip_runnable = ctx.enter_context(_create_runnable_pip())
210228
self._install_requirements(
211229
pip_runnable,
212230
finder,

0 commit comments

Comments
 (0)