spawn.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. """distutils.spawn
  2. Provides the 'spawn()' function, a front-end to various platform-
  3. specific functions for launching another program in a sub-process.
  4. """
  5. from __future__ import annotations
  6. import os
  7. import platform
  8. import shutil
  9. import subprocess
  10. import sys
  11. import warnings
  12. from collections.abc import Mapping, MutableSequence
  13. from typing import TYPE_CHECKING, TypeVar, overload
  14. from ._log import log
  15. from .debug import DEBUG
  16. from .errors import DistutilsExecError
  17. if TYPE_CHECKING:
  18. from subprocess import _ENV
  19. _MappingT = TypeVar("_MappingT", bound=Mapping)
  20. def _debug(cmd):
  21. """
  22. Render a subprocess command differently depending on DEBUG.
  23. """
  24. return cmd if DEBUG else cmd[0]
  25. def _inject_macos_ver(env: _MappingT | None) -> _MappingT | dict[str, str | int] | None:
  26. if platform.system() != 'Darwin':
  27. return env
  28. from .util import MACOSX_VERSION_VAR, get_macosx_target_ver
  29. target_ver = get_macosx_target_ver()
  30. update = {MACOSX_VERSION_VAR: target_ver} if target_ver else {}
  31. return {**_resolve(env), **update}
  32. @overload
  33. def _resolve(env: None) -> os._Environ[str]: ...
  34. @overload
  35. def _resolve(env: _MappingT) -> _MappingT: ...
  36. def _resolve(env: _MappingT | None) -> _MappingT | os._Environ[str]:
  37. return os.environ if env is None else env
  38. def spawn(
  39. cmd: MutableSequence[bytes | str | os.PathLike[str]],
  40. search_path: bool = True,
  41. verbose: bool = False,
  42. env: _ENV | None = None,
  43. ) -> None:
  44. """Run another program, specified as a command list 'cmd', in a new process.
  45. 'cmd' is just the argument list for the new process, ie.
  46. cmd[0] is the program to run and cmd[1:] are the rest of its arguments.
  47. There is no way to run a program with a name different from that of its
  48. executable.
  49. If 'search_path' is true (the default), the system's executable
  50. search path will be used to find the program; otherwise, cmd[0]
  51. must be the exact path to the executable.
  52. Raise DistutilsExecError if running the program fails in any way; just
  53. return on success.
  54. """
  55. log.info(subprocess.list2cmdline(cmd))
  56. if search_path:
  57. executable = shutil.which(cmd[0])
  58. if executable is not None:
  59. cmd[0] = executable
  60. try:
  61. subprocess.check_call(cmd, env=_inject_macos_ver(env))
  62. except OSError as exc:
  63. raise DistutilsExecError(
  64. f"command {_debug(cmd)!r} failed: {exc.args[-1]}"
  65. ) from exc
  66. except subprocess.CalledProcessError as err:
  67. raise DistutilsExecError(
  68. f"command {_debug(cmd)!r} failed with exit code {err.returncode}"
  69. ) from err
  70. def find_executable(executable: str, path: str | None = None) -> str | None:
  71. """Tries to find 'executable' in the directories listed in 'path'.
  72. A string listing directories separated by 'os.pathsep'; defaults to
  73. os.environ['PATH']. Returns the complete filename or None if not found.
  74. """
  75. warnings.warn(
  76. 'Use shutil.which instead of find_executable', DeprecationWarning, stacklevel=2
  77. )
  78. _, ext = os.path.splitext(executable)
  79. if (sys.platform == 'win32') and (ext != '.exe'):
  80. executable = executable + '.exe'
  81. if os.path.isfile(executable):
  82. return executable
  83. if path is None:
  84. path = os.environ.get('PATH', None)
  85. # bpo-35755: Don't fall through if PATH is the empty string
  86. if path is None:
  87. try:
  88. path = os.confstr("CS_PATH")
  89. except (AttributeError, ValueError):
  90. # os.confstr() or CS_PATH is not available
  91. path = os.defpath
  92. # PATH='' doesn't match, whereas PATH=':' looks in the current directory
  93. if not path:
  94. return None
  95. paths = path.split(os.pathsep)
  96. for p in paths:
  97. f = os.path.join(p, executable)
  98. if os.path.isfile(f):
  99. # the file exists, we have a shot at spawn working
  100. return f
  101. return None