subprocess.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. import logging
  2. import os
  3. import shlex
  4. import subprocess
  5. from typing import (
  6. TYPE_CHECKING,
  7. Any,
  8. Callable,
  9. Iterable,
  10. List,
  11. Mapping,
  12. Optional,
  13. Union,
  14. )
  15. from pip._vendor.rich.markup import escape
  16. from pip._internal.cli.spinners import SpinnerInterface, open_spinner
  17. from pip._internal.exceptions import InstallationSubprocessError
  18. from pip._internal.utils.logging import VERBOSE, subprocess_logger
  19. from pip._internal.utils.misc import HiddenText
  20. if TYPE_CHECKING:
  21. # Literal was introduced in Python 3.8.
  22. #
  23. # TODO: Remove `if TYPE_CHECKING` when dropping support for Python 3.7.
  24. from typing import Literal
  25. CommandArgs = List[Union[str, HiddenText]]
  26. def make_command(*args: Union[str, HiddenText, CommandArgs]) -> CommandArgs:
  27. """
  28. Create a CommandArgs object.
  29. """
  30. command_args: CommandArgs = []
  31. for arg in args:
  32. # Check for list instead of CommandArgs since CommandArgs is
  33. # only known during type-checking.
  34. if isinstance(arg, list):
  35. command_args.extend(arg)
  36. else:
  37. # Otherwise, arg is str or HiddenText.
  38. command_args.append(arg)
  39. return command_args
  40. def format_command_args(args: Union[List[str], CommandArgs]) -> str:
  41. """
  42. Format command arguments for display.
  43. """
  44. # For HiddenText arguments, display the redacted form by calling str().
  45. # Also, we don't apply str() to arguments that aren't HiddenText since
  46. # this can trigger a UnicodeDecodeError in Python 2 if the argument
  47. # has type unicode and includes a non-ascii character. (The type
  48. # checker doesn't ensure the annotations are correct in all cases.)
  49. return " ".join(
  50. shlex.quote(str(arg)) if isinstance(arg, HiddenText) else shlex.quote(arg)
  51. for arg in args
  52. )
  53. def reveal_command_args(args: Union[List[str], CommandArgs]) -> List[str]:
  54. """
  55. Return the arguments in their raw, unredacted form.
  56. """
  57. return [arg.secret if isinstance(arg, HiddenText) else arg for arg in args]
  58. def call_subprocess(
  59. cmd: Union[List[str], CommandArgs],
  60. show_stdout: bool = False,
  61. cwd: Optional[str] = None,
  62. on_returncode: 'Literal["raise", "warn", "ignore"]' = "raise",
  63. extra_ok_returncodes: Optional[Iterable[int]] = None,
  64. extra_environ: Optional[Mapping[str, Any]] = None,
  65. unset_environ: Optional[Iterable[str]] = None,
  66. spinner: Optional[SpinnerInterface] = None,
  67. log_failed_cmd: Optional[bool] = True,
  68. stdout_only: Optional[bool] = False,
  69. *,
  70. command_desc: str,
  71. ) -> str:
  72. """
  73. Args:
  74. show_stdout: if true, use INFO to log the subprocess's stderr and
  75. stdout streams. Otherwise, use DEBUG. Defaults to False.
  76. extra_ok_returncodes: an iterable of integer return codes that are
  77. acceptable, in addition to 0. Defaults to None, which means [].
  78. unset_environ: an iterable of environment variable names to unset
  79. prior to calling subprocess.Popen().
  80. log_failed_cmd: if false, failed commands are not logged, only raised.
  81. stdout_only: if true, return only stdout, else return both. When true,
  82. logging of both stdout and stderr occurs when the subprocess has
  83. terminated, else logging occurs as subprocess output is produced.
  84. """
  85. if extra_ok_returncodes is None:
  86. extra_ok_returncodes = []
  87. if unset_environ is None:
  88. unset_environ = []
  89. # Most places in pip use show_stdout=False. What this means is--
  90. #
  91. # - We connect the child's output (combined stderr and stdout) to a
  92. # single pipe, which we read.
  93. # - We log this output to stderr at DEBUG level as it is received.
  94. # - If DEBUG logging isn't enabled (e.g. if --verbose logging wasn't
  95. # requested), then we show a spinner so the user can still see the
  96. # subprocess is in progress.
  97. # - If the subprocess exits with an error, we log the output to stderr
  98. # at ERROR level if it hasn't already been displayed to the console
  99. # (e.g. if --verbose logging wasn't enabled). This way we don't log
  100. # the output to the console twice.
  101. #
  102. # If show_stdout=True, then the above is still done, but with DEBUG
  103. # replaced by INFO.
  104. if show_stdout:
  105. # Then log the subprocess output at INFO level.
  106. log_subprocess: Callable[..., None] = subprocess_logger.info
  107. used_level = logging.INFO
  108. else:
  109. # Then log the subprocess output using VERBOSE. This also ensures
  110. # it will be logged to the log file (aka user_log), if enabled.
  111. log_subprocess = subprocess_logger.verbose
  112. used_level = VERBOSE
  113. # Whether the subprocess will be visible in the console.
  114. showing_subprocess = subprocess_logger.getEffectiveLevel() <= used_level
  115. # Only use the spinner if we're not showing the subprocess output
  116. # and we have a spinner.
  117. use_spinner = not showing_subprocess and spinner is not None
  118. log_subprocess("Running command %s", command_desc)
  119. env = os.environ.copy()
  120. if extra_environ:
  121. env.update(extra_environ)
  122. for name in unset_environ:
  123. env.pop(name, None)
  124. try:
  125. proc = subprocess.Popen(
  126. # Convert HiddenText objects to the underlying str.
  127. reveal_command_args(cmd),
  128. stdin=subprocess.PIPE,
  129. stdout=subprocess.PIPE,
  130. stderr=subprocess.STDOUT if not stdout_only else subprocess.PIPE,
  131. cwd=cwd,
  132. env=env,
  133. errors="backslashreplace",
  134. )
  135. except Exception as exc:
  136. if log_failed_cmd:
  137. subprocess_logger.critical(
  138. "Error %s while executing command %s",
  139. exc,
  140. command_desc,
  141. )
  142. raise
  143. all_output = []
  144. if not stdout_only:
  145. assert proc.stdout
  146. assert proc.stdin
  147. proc.stdin.close()
  148. # In this mode, stdout and stderr are in the same pipe.
  149. while True:
  150. line: str = proc.stdout.readline()
  151. if not line:
  152. break
  153. line = line.rstrip()
  154. all_output.append(line + "\n")
  155. # Show the line immediately.
  156. log_subprocess(line)
  157. # Update the spinner.
  158. if use_spinner:
  159. assert spinner
  160. spinner.spin()
  161. try:
  162. proc.wait()
  163. finally:
  164. if proc.stdout:
  165. proc.stdout.close()
  166. output = "".join(all_output)
  167. else:
  168. # In this mode, stdout and stderr are in different pipes.
  169. # We must use communicate() which is the only safe way to read both.
  170. out, err = proc.communicate()
  171. # log line by line to preserve pip log indenting
  172. for out_line in out.splitlines():
  173. log_subprocess(out_line)
  174. all_output.append(out)
  175. for err_line in err.splitlines():
  176. log_subprocess(err_line)
  177. all_output.append(err)
  178. output = out
  179. proc_had_error = proc.returncode and proc.returncode not in extra_ok_returncodes
  180. if use_spinner:
  181. assert spinner
  182. if proc_had_error:
  183. spinner.finish("error")
  184. else:
  185. spinner.finish("done")
  186. if proc_had_error:
  187. if on_returncode == "raise":
  188. error = InstallationSubprocessError(
  189. command_description=command_desc,
  190. exit_code=proc.returncode,
  191. output_lines=all_output if not showing_subprocess else None,
  192. )
  193. if log_failed_cmd:
  194. subprocess_logger.error("[present-rich] %s", error)
  195. subprocess_logger.verbose(
  196. "[bold magenta]full command[/]: [blue]%s[/]",
  197. escape(format_command_args(cmd)),
  198. extra={"markup": True},
  199. )
  200. subprocess_logger.verbose(
  201. "[bold magenta]cwd[/]: %s",
  202. escape(cwd or "[inherit]"),
  203. extra={"markup": True},
  204. )
  205. raise error
  206. elif on_returncode == "warn":
  207. subprocess_logger.warning(
  208. 'Command "%s" had error code %s in %s',
  209. command_desc,
  210. proc.returncode,
  211. cwd,
  212. )
  213. elif on_returncode == "ignore":
  214. pass
  215. else:
  216. raise ValueError(f"Invalid value: on_returncode={on_returncode!r}")
  217. return output
  218. def runner_with_spinner_message(message: str) -> Callable[..., None]:
  219. """Provide a subprocess_runner that shows a spinner message.
  220. Intended for use with for BuildBackendHookCaller. Thus, the runner has
  221. an API that matches what's expected by BuildBackendHookCaller.subprocess_runner.
  222. """
  223. def runner(
  224. cmd: List[str],
  225. cwd: Optional[str] = None,
  226. extra_environ: Optional[Mapping[str, Any]] = None,
  227. ) -> None:
  228. with open_spinner(message) as spinner:
  229. call_subprocess(
  230. cmd,
  231. command_desc=message,
  232. cwd=cwd,
  233. extra_environ=extra_environ,
  234. spinner=spinner,
  235. )
  236. return runner