1 """Build Environment used for isolation during sdist building
10 from collections
import OrderedDict
11 from types
import TracebackType
12 from typing
import TYPE_CHECKING
, Iterable
, List
, Optional
, Set
, Tuple
, Type
, Union
14 from pip
._vendor
.certifi
import where
15 from pip
._vendor
.packaging
.requirements
import Requirement
16 from pip
._vendor
.packaging
.version
import Version
18 from pip
import __file__
as pip_location
19 from pip
._internal
.cli
.spinners
import open_spinner
20 from pip
._internal
.locations
import get_platlib
, get_purelib
, get_scheme
21 from pip
._internal
.metadata
import get_default_environment
, get_environment
22 from pip
._internal
.utils
.subprocess
import call_subprocess
23 from pip
._internal
.utils
.temp_dir
import TempDirectory
, tempdir_kinds
26 from pip
._internal
.index
.package_finder
import PackageFinder
28 logger
= logging
.getLogger(__name__
)
31 def _dedup(a
: str, b
: str) -> Union
[Tuple
[str], Tuple
[str, str]]:
32 return (a
, b
) if a
!= b
else (a
,)
36 def __init__(self
, path
: str) -> None:
39 scheme
= get_scheme("", prefix
=path
)
40 self
.bin_dir
= scheme
.scripts
41 self
.lib_dirs
= _dedup(scheme
.purelib
, scheme
.platlib
)
44 def get_runnable_pip() -> str:
45 """Get a file to pass to a Python executable, to run the currently-running pip.
47 This is used to run a pip subprocess, for installing requirements into the build
50 source
= pathlib
.Path(pip_location
).resolve().parent
52 if not source
.is_dir():
53 # This would happen if someone is using pip from inside a zip file. In that
54 # case, we can use that directly.
57 return os
.fsdecode(source
/ "__pip-runner__.py")
60 def _get_system_sitepackages() -> Set
[str]:
61 """Get system site packages
63 Usually from site.getsitepackages,
64 but fallback on `get_purelib()/get_platlib()` if unavailable
65 (e.g. in a virtualenv created by virtualenv<20)
67 Returns normalized set of strings.
69 if hasattr(site
, "getsitepackages"):
70 system_sites
= site
.getsitepackages()
72 # virtualenv < 20 overwrites site.py without getsitepackages
73 # fallback on get_purelib/get_platlib.
74 # this is known to miss things, but shouldn't in the cases
75 # where getsitepackages() has been removed (inside a virtualenv)
76 system_sites
= [get_purelib(), get_platlib()]
77 return {os.path.normcase(path) for path in system_sites}
80 class BuildEnvironment
:
81 """Creates and manages an isolated environment to install build deps"""
83 def __init__(self
) -> None:
84 temp_dir
= TempDirectory(kind
=tempdir_kinds
.BUILD_ENV
, globally_managed
=True)
86 self
._prefixes
= OrderedDict(
87 (name
, _Prefix(os
.path
.join(temp_dir
.path
, name
)))
88 for name
in ("normal", "overlay")
91 self
._bin
_dirs
: List
[str] = []
92 self
._lib
_dirs
: List
[str] = []
93 for prefix
in reversed(list(self
._prefixes
.values())):
94 self
._bin
_dirs
.append(prefix
.bin_dir
)
95 self
._lib
_dirs
.extend(prefix
.lib_dirs
)
98 # - ensure .pth files are honored
99 # - prevent access to system site packages
100 system_sites
= _get_system_sitepackages()
102 self
._site
_dir
= os
.path
.join(temp_dir
.path
, "site")
103 if not os
.path
.exists(self
._site
_dir
):
104 os
.mkdir(self
._site
_dir
)
106 os
.path
.join(self
._site
_dir
, "sitecustomize.py"), "w", encoding
="utf-8"
113 # First, drop system-sites related paths.
114 original_sys_path = sys.path[:]
116 for path in {system_sites!r}:
117 site.addsitedir(path, known_paths=known_paths)
119 os.path.normcase(path)
120 for path in sys.path[len(original_sys_path):]
122 original_sys_path = [
123 path for path in original_sys_path
124 if os.path.normcase(path) not in system_paths
126 sys.path = original_sys_path
128 # Second, add lib directories.
129 # ensuring .pth file are processed.
130 for path in {lib_dirs!r}:
131 assert not path in sys.path
132 site.addsitedir(path)
134 ).format(system_sites
=system_sites
, lib_dirs
=self
._lib
_dirs
)
137 def __enter__(self
) -> None:
139 name
: os
.environ
.get(name
, None)
140 for name
in ("PATH", "PYTHONNOUSERSITE", "PYTHONPATH")
143 path
= self
._bin
_dirs
[:]
144 old_path
= self
._save
_env
["PATH"]
146 path
.extend(old_path
.split(os
.pathsep
))
148 pythonpath
= [self
._site
_dir
]
152 "PATH": os
.pathsep
.join(path
),
153 "PYTHONNOUSERSITE": "1",
154 "PYTHONPATH": os
.pathsep
.join(pythonpath
),
160 exc_type
: Optional
[Type
[BaseException
]],
161 exc_val
: Optional
[BaseException
],
162 exc_tb
: Optional
[TracebackType
],
164 for varname
, old_value
in self
._save
_env
.items():
165 if old_value
is None:
166 os
.environ
.pop(varname
, None)
168 os
.environ
[varname
] = old_value
170 def check_requirements(
171 self
, reqs
: Iterable
[str]
172 ) -> Tuple
[Set
[Tuple
[str, str]], Set
[str]]:
174 - conflicting requirements: set of (installed, wanted) reqs tuples
175 - missing requirements: set of reqs
181 get_environment(self
._lib
_dirs
)
182 if hasattr(self
, "_lib_dirs")
183 else get_default_environment()
186 req
= Requirement(req_str
)
187 # We're explicitly evaluating with an empty extra value, since build
188 # environments are not provided any mechanism to select specific extras.
189 if req
.marker
is not None and not req
.marker
.evaluate({"extra": ""}
):
191 dist
= env
.get_distribution(req
.name
)
195 if isinstance(dist
.version
, Version
):
196 installed_req_str
= f
"{req.name}=={dist.version}"
198 installed_req_str
= f
"{req.name}==={dist.version}"
199 if not req
.specifier
.contains(dist
.version
, prereleases
=True):
200 conflicting
.add((installed_req_str
, req_str
))
201 # FIXME: Consider direct URL?
202 return conflicting
, missing
204 def install_requirements(
206 finder
: "PackageFinder",
207 requirements
: Iterable
[str],
208 prefix_as_string
: str,
212 prefix
= self
._prefixes
[prefix_as_string
]
213 assert not prefix
.setup
217 self
._install
_requirements
(
226 def _install_requirements(
228 finder
: "PackageFinder",
229 requirements
: Iterable
[str],
238 "--ignore-installed",
242 "--no-warn-script-location",
244 if logger
.getEffectiveLevel() <= logging
.DEBUG
:
246 for format_control
in ("no_binary", "only_binary"):
247 formats
= getattr(finder
.format_control
, format_control
)
250 "--" + format_control
.replace("_", "-"),
251 ",".join(sorted(formats
or {":none:"}
)),
255 index_urls
= finder
.index_urls
257 args
.extend(["-i", index_urls
[0]])
258 for extra_index
in index_urls
[1:]:
259 args
.extend(["--extra-index-url", extra_index
])
261 args
.append("--no-index")
262 for link
in finder
.find_links
:
263 args
.extend(["--find-links", link
])
265 for host
in finder
.trusted_hosts
:
266 args
.extend(["--trusted-host", host
])
267 if finder
.allow_all_prereleases
:
269 if finder
.prefer_binary
:
270 args
.append("--prefer-binary")
272 args
.extend(requirements
)
273 extra_environ
= {"_PIP_STANDALONE_CERT": where()}
274 with open_spinner(f
"Installing {kind}") as spinner
:
277 command_desc
=f
"pip subprocess to install {kind}",
279 extra_environ
=extra_environ
,
283 class NoOpBuildEnvironment(BuildEnvironment
):
284 """A no-op drop-in replacement for BuildEnvironment"""
286 def __init__(self
) -> None:
289 def __enter__(self
) -> None:
294 exc_type
: Optional
[Type
[BaseException
]],
295 exc_val
: Optional
[BaseException
],
296 exc_tb
: Optional
[TracebackType
],
300 def cleanup(self
) -> None:
303 def install_requirements(
305 finder
: "PackageFinder",
306 requirements
: Iterable
[str],
307 prefix_as_string
: str,
311 raise NotImplementedError()