Skip to content

Commit 1ec63b6

Browse files
authored
bpo-39763: distutils.spawn now uses subprocess (GH-18743)
Reimplement distutils.spawn.spawn() function with the subprocess module. setup.py now uses a basic implementation of the subprocess module if the subprocess module is not available: before required C extension modules are built.
1 parent dffe4c0 commit 1ec63b6

File tree

5 files changed

+87
-117
lines changed

5 files changed

+87
-117
lines changed

Lib/distutils/spawn.py

+22-106
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,18 @@
88

99
import sys
1010
import os
11+
import subprocess
1112

1213
from distutils.errors import DistutilsPlatformError, DistutilsExecError
1314
from distutils.debug import DEBUG
1415
from distutils import log
1516

17+
18+
if sys.platform == 'darwin':
19+
_cfg_target = None
20+
_cfg_target_split = None
21+
22+
1623
def spawn(cmd, search_path=1, verbose=0, dry_run=0):
1724
"""Run another program, specified as a command list 'cmd', in a new process.
1825
@@ -32,64 +39,16 @@ def spawn(cmd, search_path=1, verbose=0, dry_run=0):
3239
# cmd is documented as a list, but just in case some code passes a tuple
3340
# in, protect our %-formatting code against horrible death
3441
cmd = list(cmd)
35-
if os.name == 'posix':
36-
_spawn_posix(cmd, search_path, dry_run=dry_run)
37-
elif os.name == 'nt':
38-
_spawn_nt(cmd, search_path, dry_run=dry_run)
39-
else:
40-
raise DistutilsPlatformError(
41-
"don't know how to spawn programs on platform '%s'" % os.name)
42-
43-
def _nt_quote_args(args):
44-
"""Quote command-line arguments for DOS/Windows conventions.
45-
46-
Just wraps every argument which contains blanks in double quotes, and
47-
returns a new argument list.
48-
"""
49-
# XXX this doesn't seem very robust to me -- but if the Windows guys
50-
# say it'll work, I guess I'll have to accept it. (What if an arg
51-
# contains quotes? What other magic characters, other than spaces,
52-
# have to be escaped? Is there an escaping mechanism other than
53-
# quoting?)
54-
for i, arg in enumerate(args):
55-
if ' ' in arg:
56-
args[i] = '"%s"' % arg
57-
return args
58-
59-
def _spawn_nt(cmd, search_path=1, verbose=0, dry_run=0):
60-
executable = cmd[0]
61-
cmd = _nt_quote_args(cmd)
62-
if search_path:
63-
# either we find one or it stays the same
64-
executable = find_executable(executable) or executable
65-
log.info(' '.join([executable] + cmd[1:]))
66-
if not dry_run:
67-
# spawn for NT requires a full path to the .exe
68-
try:
69-
rc = os.spawnv(os.P_WAIT, executable, cmd)
70-
except OSError as exc:
71-
# this seems to happen when the command isn't found
72-
if not DEBUG:
73-
cmd = executable
74-
raise DistutilsExecError(
75-
"command %r failed: %s" % (cmd, exc.args[-1]))
76-
if rc != 0:
77-
# and this reflects the command running but failing
78-
if not DEBUG:
79-
cmd = executable
80-
raise DistutilsExecError(
81-
"command %r failed with exit status %d" % (cmd, rc))
82-
83-
if sys.platform == 'darwin':
84-
_cfg_target = None
85-
_cfg_target_split = None
8642

87-
def _spawn_posix(cmd, search_path=1, verbose=0, dry_run=0):
8843
log.info(' '.join(cmd))
8944
if dry_run:
9045
return
91-
executable = cmd[0]
92-
exec_fn = search_path and os.execvp or os.execv
46+
47+
if search_path:
48+
executable = find_executable(cmd[0])
49+
if executable is not None:
50+
cmd[0] = executable
51+
9352
env = None
9453
if sys.platform == 'darwin':
9554
global _cfg_target, _cfg_target_split
@@ -111,60 +70,17 @@ def _spawn_posix(cmd, search_path=1, verbose=0, dry_run=0):
11170
raise DistutilsPlatformError(my_msg)
11271
env = dict(os.environ,
11372
MACOSX_DEPLOYMENT_TARGET=cur_target)
114-
exec_fn = search_path and os.execvpe or os.execve
115-
pid = os.fork()
116-
if pid == 0: # in the child
117-
try:
118-
if env is None:
119-
exec_fn(executable, cmd)
120-
else:
121-
exec_fn(executable, cmd, env)
122-
except OSError as e:
123-
if not DEBUG:
124-
cmd = executable
125-
sys.stderr.write("unable to execute %r: %s\n"
126-
% (cmd, e.strerror))
127-
os._exit(1)
12873

74+
proc = subprocess.Popen(cmd, env=env)
75+
proc.wait()
76+
exitcode = proc.returncode
77+
78+
if exitcode:
12979
if not DEBUG:
130-
cmd = executable
131-
sys.stderr.write("unable to execute %r for unknown reasons" % cmd)
132-
os._exit(1)
133-
else: # in the parent
134-
# Loop until the child either exits or is terminated by a signal
135-
# (ie. keep waiting if it's merely stopped)
136-
while True:
137-
try:
138-
pid, status = os.waitpid(pid, 0)
139-
except OSError as exc:
140-
if not DEBUG:
141-
cmd = executable
142-
raise DistutilsExecError(
143-
"command %r failed: %s" % (cmd, exc.args[-1]))
144-
if os.WIFSIGNALED(status):
145-
if not DEBUG:
146-
cmd = executable
147-
raise DistutilsExecError(
148-
"command %r terminated by signal %d"
149-
% (cmd, os.WTERMSIG(status)))
150-
elif os.WIFEXITED(status):
151-
exit_status = os.WEXITSTATUS(status)
152-
if exit_status == 0:
153-
return # hey, it succeeded!
154-
else:
155-
if not DEBUG:
156-
cmd = executable
157-
raise DistutilsExecError(
158-
"command %r failed with exit status %d"
159-
% (cmd, exit_status))
160-
elif os.WIFSTOPPED(status):
161-
continue
162-
else:
163-
if not DEBUG:
164-
cmd = executable
165-
raise DistutilsExecError(
166-
"unknown error executing %r: termination status %d"
167-
% (cmd, status))
80+
cmd = cmd[0]
81+
raise DistutilsExecError(
82+
"command %r failed with exit code %s" % (cmd, exitcode))
83+
16884

16985
def find_executable(executable, path=None):
17086
"""Tries to find 'executable' in the directories listed in 'path'.

Lib/distutils/tests/test_spawn.py

-11
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from test import support as test_support
99

1010
from distutils.spawn import find_executable
11-
from distutils.spawn import _nt_quote_args
1211
from distutils.spawn import spawn
1312
from distutils.errors import DistutilsExecError
1413
from distutils.tests import support
@@ -17,16 +16,6 @@ class SpawnTestCase(support.TempdirManager,
1716
support.LoggingSilencer,
1817
unittest.TestCase):
1918

20-
def test_nt_quote_args(self):
21-
22-
for (args, wanted) in ((['with space', 'nospace'],
23-
['"with space"', 'nospace']),
24-
(['nochange', 'nospace'],
25-
['nochange', 'nospace'])):
26-
res = _nt_quote_args(args)
27-
self.assertEqual(res, wanted)
28-
29-
3019
@unittest.skipUnless(os.name in ('nt', 'posix'),
3120
'Runs only under posix or nt')
3221
def test_spawn(self):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
setup.py now uses a basic implementation of the :mod:`subprocess` module if
2+
the :mod:`subprocess` module is not available: before required C extension
3+
modules are built.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Reimplement :func:`distutils.spawn.spawn` function with the
2+
:mod:`subprocess` module.

setup.py

+60
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,61 @@
1010
import sysconfig
1111
from glob import glob
1212

13+
14+
try:
15+
import subprocess
16+
del subprocess
17+
SUBPROCESS_BOOTSTRAP = False
18+
except ImportError:
19+
SUBPROCESS_BOOTSTRAP = True
20+
21+
# Bootstrap Python: distutils.spawn uses subprocess to build C extensions,
22+
# subprocess requires C extensions built by setup.py like _posixsubprocess.
23+
#
24+
# Basic subprocess implementation for POSIX (setup.py is not used on
25+
# Windows) which only uses os functions. Only implement features required
26+
# by distutils.spawn.
27+
#
28+
# It is dropped from sys.modules as soon as all C extension modules
29+
# are built.
30+
class Popen:
31+
def __init__(self, cmd, env=None):
32+
self._cmd = cmd
33+
self._env = env
34+
self.returncode = None
35+
36+
def wait(self):
37+
pid = os.fork()
38+
if pid == 0:
39+
# Child process
40+
try:
41+
if self._env is not None:
42+
os.execve(self._cmd[0], self._cmd, self._env)
43+
else:
44+
os.execv(self._cmd[0], self._cmd)
45+
finally:
46+
os._exit(1)
47+
else:
48+
# Parent process
49+
pid, status = os.waitpid(pid, 0)
50+
if os.WIFSIGNALED(status):
51+
self.returncode = -os.WTERMSIG(status)
52+
elif os.WIFEXITED(status):
53+
self.returncode = os.WEXITSTATUS(status)
54+
elif os.WIFSTOPPED(status):
55+
self.returncode = -os.WSTOPSIG(sts)
56+
else:
57+
# Should never happen
58+
raise Exception("Unknown child exit status!")
59+
60+
return self.returncode
61+
62+
mod = type(sys)('subprocess')
63+
mod.Popen = Popen
64+
sys.modules['subprocess'] = mod
65+
del mod
66+
67+
1368
from distutils import log
1469
from distutils.command.build_ext import build_ext
1570
from distutils.command.build_scripts import build_scripts
@@ -391,6 +446,11 @@ def build_extensions(self):
391446

392447
build_ext.build_extensions(self)
393448

449+
if SUBPROCESS_BOOTSTRAP:
450+
# Drop our custom subprocess module:
451+
# use the newly built subprocess module
452+
del sys.modules['subprocess']
453+
394454
for ext in self.extensions:
395455
self.check_extension_import(ext)
396456

0 commit comments

Comments
 (0)