Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pip/_internal/build_env/installer.py: 20%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

132 statements  

1from __future__ import annotations 

2 

3import logging 

4import sys 

5import textwrap 

6from collections.abc import Iterable, Sequence 

7from contextlib import AbstractContextManager as ContextManager 

8from contextlib import nullcontext 

9from io import StringIO 

10from typing import TYPE_CHECKING 

11 

12from pip._internal.build_env.base import Prefix 

13from pip._internal.cli.spinners import open_rich_spinner, open_spinner 

14from pip._internal.exceptions import ( 

15 BuildDependencyInstallError, 

16 DiagnosticPipError, 

17 InstallWheelBuildError, 

18 PipError, 

19) 

20from pip._internal.metadata import get_environment 

21from pip._internal.utils.logging import VERBOSE, capture_logging 

22from pip._internal.utils.misc import get_runnable_pip 

23from pip._internal.utils.subprocess import call_subprocess 

24from pip._internal.utils.temp_dir import TempDirectory 

25 

26if TYPE_CHECKING: 

27 from pip._internal.cache import WheelCache 

28 from pip._internal.index.package_finder import PackageFinder 

29 from pip._internal.operations.build.build_tracker import BuildTracker 

30 from pip._internal.req.req_install import InstallRequirement 

31 from pip._internal.resolution.base import BaseResolver 

32 

33 

34logger = logging.getLogger(__name__) 

35 

36 

37class SubprocessBuildEnvironmentInstaller: 

38 """ 

39 Install build dependencies by calling pip in a subprocess. 

40 """ 

41 

42 def __init__( 

43 self, 

44 finder: PackageFinder, 

45 build_constraints: list[str] | None = None, 

46 ) -> None: 

47 self.finder = finder 

48 self._build_constraints = build_constraints or [] 

49 

50 def install( 

51 self, 

52 requirements: Iterable[str], 

53 prefix: Prefix, 

54 *, 

55 kind: str, 

56 for_req: InstallRequirement | None, 

57 ) -> None: 

58 finder = self.finder 

59 args: list[str] = [ 

60 get_runnable_pip(), 

61 "install", 

62 # HACK: --prefix shouldn't be necessary for venv environments, but 

63 # we set it anyway so if it's set via an envvar or configuration 

64 # file, it won't break things, *sigh*. 

65 "--prefix", 

66 prefix.path, 

67 "--no-user", 

68 "--no-warn-script-location", 

69 "--disable-pip-version-check", 

70 # As the build environment is ephemeral, it's wasteful to 

71 # pre-compile everything, especially as not every Python 

72 # module will be used/compiled in most cases. 

73 "--no-compile", 

74 # The prefix specified two lines above, thus 

75 # target from config file or env var should be ignored 

76 "--target", 

77 "", 

78 ] 

79 if prefix.venv_executable: 

80 args.insert(0, prefix.venv_executable) 

81 else: 

82 args.insert(0, sys.executable) 

83 args.append("--ignore-installed") 

84 

85 if logger.getEffectiveLevel() <= logging.DEBUG: 

86 args.append("-vv") 

87 elif logger.getEffectiveLevel() <= VERBOSE: 

88 args.append("-v") 

89 for format_control in ("no_binary", "only_binary"): 

90 formats = getattr(finder.format_control, format_control) 

91 args.extend( 

92 ( 

93 "--" + format_control.replace("_", "-"), 

94 ",".join(sorted(formats or {":none:"})), 

95 ) 

96 ) 

97 

98 if finder.release_control is not None: 

99 # Use ordered args to preserve the user's original command-line order 

100 # This is important because later flags can override earlier ones 

101 for attr_name, value in finder.release_control.get_ordered_args(): 

102 args.extend(("--" + attr_name.replace("_", "-"), value)) 

103 

104 index_urls = finder.index_urls 

105 if index_urls: 

106 args.extend(["-i", index_urls[0]]) 

107 for extra_index in index_urls[1:]: 

108 args.extend(["--extra-index-url", extra_index]) 

109 else: 

110 args.append("--no-index") 

111 for link in finder.find_links: 

112 args.extend(["--find-links", link]) 

113 

114 # is not None: forward an empty --proxy "" (disable proxying) too. 

115 if finder.proxy is not None: 

116 args.extend(["--proxy", finder.proxy]) 

117 if finder.no_proxy_env: 

118 args.append("--no-proxy-env") 

119 for host in finder.trusted_hosts: 

120 args.extend(["--trusted-host", host]) 

121 if finder.custom_cert: 

122 args.extend(["--cert", finder.custom_cert]) 

123 if finder.client_cert: 

124 args.extend(["--client-cert", finder.client_cert]) 

125 if finder.prefer_binary: 

126 args.append("--prefer-binary") 

127 if finder.refresh_package: 

128 args.extend(["--refresh-package", ",".join(finder.refresh_package)]) 

129 

130 # Only build constraints apply in the isolated build environment. 

131 # _PIP_IN_BUILD_IGNORE_CONSTRAINTS tells the subprocess to ignore the 

132 # regular constraints it inherits (via PIP_CONSTRAINT or config files). 

133 # Build constraints reach it through --build-constraint, which also 

134 # constrains any nested builds. 

135 for constraint_file in self._build_constraints: 

136 args.extend(["--build-constraint", constraint_file]) 

137 

138 if finder.uploaded_prior_to: 

139 args.extend(["--uploaded-prior-to", finder.uploaded_prior_to.isoformat()]) 

140 args.append("--") 

141 args.extend(requirements) 

142 

143 identify_requirement = ( 

144 f" for {for_req.name}" if for_req and for_req.name else "" 

145 ) 

146 with open_spinner(f"Installing {kind}") as spinner: 

147 call_subprocess( 

148 args, 

149 command_desc=f"installing {kind}{identify_requirement}", 

150 spinner=spinner, 

151 extra_environ={"_PIP_IN_BUILD_IGNORE_CONSTRAINTS": "1"}, 

152 ) 

153 

154 

155class InprocessBuildEnvironmentInstaller: 

156 """ 

157 Install build dependencies via the already running pip process. 

158 

159 This contains a stripped down version of the install command with 

160 only the logic necessary for installing build dependencies. The 

161 finder, session, build tracker, and wheel cache are reused, but new 

162 instances of everything else are created as needed. 

163 

164 Options are inherited from the parent install command unless 

165 they don't make sense for build dependencies (in which case, they 

166 are hard-coded, see comments below). 

167 """ 

168 

169 # TODO: this plays poorly with venv-based build environments, but cannot be 

170 # fixed until pip gains better support for operating within a Python 

171 # environment that isn't the running environment. 

172 

173 def __init__( 

174 self, 

175 *, 

176 finder: PackageFinder, 

177 build_tracker: BuildTracker, 

178 wheel_cache: WheelCache, 

179 build_constraints: Sequence[InstallRequirement] = (), 

180 verbosity: int = 0, 

181 ) -> None: 

182 from pip._internal.operations.prepare import RequirementPreparer 

183 

184 self._finder = finder 

185 self._build_constraints = build_constraints 

186 self._wheel_cache = wheel_cache 

187 self._level = 0 

188 

189 build_dir = TempDirectory(kind="build-env-install", globally_managed=True) 

190 self._preparer = RequirementPreparer( 

191 build_isolation_installer=self, 

192 # Inherited options or state. 

193 finder=finder, 

194 session=finder._link_collector.session, 

195 build_dir=build_dir.path, 

196 build_tracker=build_tracker, 

197 verbosity=verbosity, 

198 # This is irrelevant as it only applies to editable requirements. 

199 src_dir="", 

200 # Hard-coded options (that should NOT be inherited). 

201 download_dir=None, 

202 build_isolation="virtual", 

203 check_build_deps=False, 

204 progress_bar="off", 

205 # TODO: hash-checking should be extended to build deps, but that is 

206 # deferred for later as it'd be a breaking change. 

207 require_hashes=False, 

208 use_user_site=False, 

209 lazy_wheel=False, 

210 legacy_resolver=False, 

211 allow_editables=True, 

212 ) 

213 

214 def install( 

215 self, 

216 requirements: Iterable[str], 

217 prefix: Prefix, 

218 *, 

219 kind: str, 

220 for_req: InstallRequirement | None, 

221 ) -> None: 

222 """Install entrypoint. Manages output capturing and error handling.""" 

223 capture_logs = not logger.isEnabledFor(VERBOSE) and self._level == 0 

224 if capture_logs: 

225 # Hide the logs from the installation of build dependencies. 

226 # They will be shown only if an error occurs. 

227 capture_ctx: ContextManager[StringIO] = capture_logging() 

228 spinner: ContextManager[None] = open_rich_spinner(f"Installing {kind}") 

229 else: 

230 # Otherwise, pass-through all logs (with a header). 

231 capture_ctx, spinner = nullcontext(StringIO()), nullcontext() 

232 logger.info("Installing %s ...", kind) 

233 

234 try: 

235 self._level += 1 

236 with spinner, capture_ctx as stream: 

237 self._install_impl(requirements, prefix) 

238 

239 except DiagnosticPipError as exc: 

240 # Format similar to a nested subprocess error, where the 

241 # causing error is shown first, followed by the build error. 

242 logger.info(textwrap.dedent(stream.getvalue())) 

243 logger.error("%s", exc, extra={"rich": True}) 

244 logger.info("") 

245 raise BuildDependencyInstallError( 

246 for_req, requirements, cause=exc, log_lines=None 

247 ) 

248 

249 except Exception as exc: 

250 logs: list[str] | None = textwrap.dedent(stream.getvalue()).splitlines() 

251 if not capture_logs: 

252 # If logs aren't being captured, then display the error inline 

253 # with the rest of the logs. 

254 logs = None 

255 if isinstance(exc, PipError): 

256 logger.error("%s", exc) 

257 else: 

258 logger.exception("pip crashed unexpectedly") 

259 raise BuildDependencyInstallError( 

260 for_req, requirements, cause=exc, log_lines=logs 

261 ) 

262 

263 finally: 

264 self._level -= 1 

265 

266 def _install_impl(self, requirements: Iterable[str], prefix: Prefix) -> None: 

267 """Core build dependency install logic.""" 

268 from pip._internal.commands.install import installed_packages_summary 

269 from pip._internal.req import install_given_reqs 

270 from pip._internal.req.constructors import install_req_from_line 

271 from pip._internal.wheel_builder import build 

272 

273 ireqs = [install_req_from_line(req, user_supplied=True) for req in requirements] 

274 ireqs.extend(self._build_constraints) 

275 

276 resolver = self._make_resolver() 

277 resolved_set = resolver.resolve(ireqs, check_supported_wheels=True) 

278 self._preparer.prepare_linked_requirements_more( 

279 resolved_set.requirements.values() 

280 ) 

281 

282 reqs_to_build = [ 

283 r for r in resolved_set.requirements_to_install if not r.is_wheel 

284 ] 

285 _, build_failures = build( 

286 reqs_to_build, self._wheel_cache, verify=True, allow_editables=True 

287 ) 

288 if build_failures: 

289 raise InstallWheelBuildError(build_failures) 

290 

291 installed = install_given_reqs( 

292 resolver.get_installation_order(resolved_set), 

293 prefix=prefix.path, 

294 # Hard-coded options (that should NOT be inherited). 

295 root=None, 

296 home=None, 

297 warn_script_location=False, 

298 use_user_site=False, 

299 # As the build environment is ephemeral, it's wasteful to 

300 # pre-compile everything since not all modules will be used. 

301 pycompile=False, 

302 progress_bar="off", 

303 # Link console scripts to the build env's interpreter, not pip's. 

304 script_executable=prefix.venv_executable, 

305 ) 

306 

307 env = get_environment(list(prefix.lib_dirs)) 

308 if summary := installed_packages_summary(installed, env): 

309 logger.info(summary) 

310 

311 def _make_resolver(self) -> BaseResolver: 

312 """Create a new resolver for one time use.""" 

313 # Legacy installer never used the legacy resolver so create a 

314 # resolvelib resolver directly. Yuck. 

315 from pip._internal.req.constructors import install_req_from_req_string 

316 from pip._internal.resolution.resolvelib.resolver import Resolver 

317 

318 return Resolver( 

319 make_install_req=install_req_from_req_string, 

320 # Inherited state. 

321 preparer=self._preparer, 

322 finder=self._finder, 

323 wheel_cache=self._wheel_cache, 

324 # Hard-coded options (that should NOT be inherited). 

325 ignore_requires_python=False, 

326 use_user_site=False, 

327 ignore_dependencies=False, 

328 only_dependencies=False, 

329 ignore_installed=True, 

330 force_reinstall=False, 

331 upgrade_strategy="to-satisfy-only", 

332 py_version_info=None, 

333 )