From addaaaa946855ad59c8f5c698aa0891d7e44f018 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 9 Mar 2020 23:45:59 +0100 Subject: [PATCH] bpo-39763: Add _bootsubprocess to build Python on AIX (GH-18872) Add _bootsubprocess module to bootstrap Python: subprocess implementation which only uses the os module. On AIX, distutils.util uses _aix_support which calls subprocess.check_output(), before the _posixsubprocess module is built. Implement check_output() with os.system() in _bootsubprocess. --- Lib/_bootsubprocess.py | 102 +++++++++++++++++++++++++++++++++++++++++ setup.py | 46 ++----------------- 2 files changed, 107 insertions(+), 41 deletions(-) create mode 100644 Lib/_bootsubprocess.py diff --git a/Lib/_bootsubprocess.py b/Lib/_bootsubprocess.py new file mode 100644 index 00000000000..962301ae149 --- /dev/null +++ b/Lib/_bootsubprocess.py @@ -0,0 +1,102 @@ +""" +Basic subprocess implementation for POSIX which only uses os functions. Only +implement features required by setup.py to build C extension modules when +subprocess is unavailable. setup.py is not used on Windows. +""" +import os + + +# distutils.spawn used by distutils.command.build_ext +# calls subprocess.Popen().wait() +class Popen: + def __init__(self, cmd, env=None): + self._cmd = cmd + self._env = env + self.returncode = None + + def wait(self): + pid = os.fork() + if pid == 0: + # Child process + try: + if self._env is not None: + os.execve(self._cmd[0], self._cmd, self._env) + else: + os.execv(self._cmd[0], self._cmd) + finally: + os._exit(1) + else: + # Parent process + pid, status = os.waitpid(pid, 0) + if os.WIFSIGNALED(status): + self.returncode = -os.WTERMSIG(status) + elif os.WIFEXITED(status): + self.returncode = os.WEXITSTATUS(status) + elif os.WIFSTOPPED(status): + self.returncode = -os.WSTOPSIG(status) + else: + raise Exception(f"unknown child process exit status: {status!r}") + + return self.returncode + + +def _check_cmd(cmd): + # Use regex [a-zA-Z0-9./-]+: reject empty string, space, etc. + safe_chars = [] + for first, last in (("a", "z"), ("A", "Z"), ("0", "9")): + for ch in range(ord(first), ord(last) + 1): + safe_chars.append(chr(ch)) + safe_chars.append("./-") + safe_chars = ''.join(safe_chars) + + if isinstance(cmd, (tuple, list)): + check_strs = cmd + elif isinstance(cmd, str): + check_strs = [cmd] + else: + return False + + for arg in check_strs: + if not isinstance(arg, str): + return False + if not arg: + # reject empty string + return False + for ch in arg: + if ch not in safe_chars: + return False + + return True + + +# _aix_support used by distutil.util calls subprocess.check_output() +def check_output(cmd, **kwargs): + if kwargs: + raise NotImplementedError(repr(kwargs)) + + if not _check_cmd(cmd): + raise ValueError(f"unsupported command: {cmd!r}") + + tmp_filename = "check_output.tmp" + if not isinstance(cmd, str): + cmd = " ".join(cmd) + cmd = f"{cmd} >{tmp_filename}" + + try: + # system() spawns a shell + status = os.system(cmd) + if status: + raise ValueError(f"Command {cmd!r} failed with status {status!r}") + + try: + with open(tmp_filename, "rb") as fp: + stdout = fp.read() + except FileNotFoundError: + stdout = b'' + finally: + try: + os.unlink(tmp_filename) + except OSError: + pass + + return stdout diff --git a/setup.py b/setup.py index a3313158179..24ce9a632dd 100644 --- a/setup.py +++ b/setup.py @@ -16,53 +16,17 @@ try: del subprocess SUBPROCESS_BOOTSTRAP = False except ImportError: - SUBPROCESS_BOOTSTRAP = True - # Bootstrap Python: distutils.spawn uses subprocess to build C extensions, # subprocess requires C extensions built by setup.py like _posixsubprocess. # - # Basic subprocess implementation for POSIX (setup.py is not used on - # Windows) which only uses os functions. Only implement features required - # by distutils.spawn. + # Use _bootsubprocess which only uses the os module. # # It is dropped from sys.modules as soon as all C extension modules # are built. - class Popen: - def __init__(self, cmd, env=None): - self._cmd = cmd - self._env = env - self.returncode = None - - def wait(self): - pid = os.fork() - if pid == 0: - # Child process - try: - if self._env is not None: - os.execve(self._cmd[0], self._cmd, self._env) - else: - os.execv(self._cmd[0], self._cmd) - finally: - os._exit(1) - else: - # Parent process - pid, status = os.waitpid(pid, 0) - if os.WIFSIGNALED(status): - self.returncode = -os.WTERMSIG(status) - elif os.WIFEXITED(status): - self.returncode = os.WEXITSTATUS(status) - elif os.WIFSTOPPED(status): - self.returncode = -os.WSTOPSIG(status) - else: - # Should never happen - raise Exception("Unknown child exit status!") - - return self.returncode - - mod = type(sys)('subprocess') - mod.Popen = Popen - sys.modules['subprocess'] = mod - del mod + import _bootsubprocess + sys.modules['subprocess'] = _bootsubprocess + del _bootsubprocess + SUBPROCESS_BOOTSTRAP = True from distutils import log