subprocess.py 8.8 KB

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