Skip to content

Commit f3b9753

Browse files
toyobayashicclauss
andauthored
fix: failed to detect flavor if compiler path include white spaces (#240)
Co-authored-by: Christian Clauss <[email protected]>
1 parent 2733180 commit f3b9753

File tree

2 files changed

+86
-47
lines changed

2 files changed

+86
-47
lines changed

pylib/gyp/common.py

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import tempfile
1010
import sys
1111
import subprocess
12+
import shlex
1213

1314
from collections.abc import MutableSet
1415

@@ -424,46 +425,49 @@ def EnsureDirExists(path):
424425

425426
def GetCrossCompilerPredefines(): # -> dict
426427
cmd = []
428+
429+
# shlex.split() will eat '\' in posix mode, but
430+
# setting posix=False will preserve extra '"' cause CreateProcess fail on Windows
431+
# this makes '\' in %CC_target% and %CFLAGS% work
432+
def replace_sep(s):
433+
return s.replace(os.sep, "/") if os.sep != "/" else s
434+
427435
if CC := os.environ.get("CC_target") or os.environ.get("CC"):
428-
cmd += CC.split(" ")
436+
cmd += shlex.split(replace_sep(CC))
429437
if CFLAGS := os.environ.get("CFLAGS"):
430-
cmd += CFLAGS.split(" ")
438+
cmd += shlex.split(replace_sep(CFLAGS))
431439
elif CXX := os.environ.get("CXX_target") or os.environ.get("CXX"):
432-
cmd += CXX.split(" ")
440+
cmd += shlex.split(replace_sep(CXX))
433441
if CXXFLAGS := os.environ.get("CXXFLAGS"):
434-
cmd += CXXFLAGS.split(" ")
442+
cmd += shlex.split(replace_sep(CXXFLAGS))
435443
else:
436444
return {}
437445

438446
if sys.platform == "win32":
439447
fd, input = tempfile.mkstemp(suffix=".c")
448+
real_cmd = [*cmd, "-dM", "-E", "-x", "c", input]
440449
try:
441450
os.close(fd)
442-
out = subprocess.Popen(
443-
[*cmd, "-dM", "-E", "-x", "c", input],
444-
shell=True,
445-
stdout=subprocess.PIPE, stderr=subprocess.STDOUT
446-
)
447-
stdout = out.communicate()[0]
451+
stdout = subprocess.run(
452+
real_cmd, shell=True,
453+
capture_output=True, check=True
454+
).stdout
448455
finally:
449456
os.unlink(input)
450457
else:
451458
input = "/dev/null"
452-
out = subprocess.Popen(
453-
[*cmd, "-dM", "-E", "-x", "c", input],
454-
shell=False,
455-
stdout=subprocess.PIPE, stderr=subprocess.STDOUT
456-
)
457-
stdout = out.communicate()[0]
459+
real_cmd = [*cmd, "-dM", "-E", "-x", "c", input]
460+
stdout = subprocess.run(
461+
real_cmd, shell=False,
462+
capture_output=True, check=True
463+
).stdout
458464

459465
defines = {}
460466
lines = stdout.decode("utf-8").replace("\r\n", "\n").split("\n")
461467
for line in lines:
462-
if not line:
463-
continue
464-
define_directive, key, *value = line.split(" ")
465-
assert define_directive == "#define"
466-
defines[key] = " ".join(value)
468+
if (line or "").startswith("#define "):
469+
_, key, *value = line.split(" ")
470+
defines[key] = " ".join(value)
467471
return defines
468472

469473
def GetFlavorByPlatform():

pylib/gyp/common_test.py

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import unittest
1111
import sys
1212
import os
13-
import subprocess
1413
from unittest.mock import patch, MagicMock
1514

1615
class TestTopologicallySorted(unittest.TestCase):
@@ -26,9 +25,8 @@ def test_Valid(self):
2625
def GetEdge(node):
2726
return tuple(graph[node])
2827

29-
self.assertEqual(
30-
gyp.common.TopologicallySorted(graph.keys(), GetEdge), ["a", "c", "d", "b"]
31-
)
28+
assert gyp.common.TopologicallySorted(
29+
graph.keys(), GetEdge) == ["a", "c", "d", "b"]
3230

3331
def test_Cycle(self):
3432
"""Test that an exception is thrown on a cyclic graph."""
@@ -60,7 +58,7 @@ def tearDown(self):
6058

6159
def assertFlavor(self, expected, argument, param):
6260
sys.platform = argument
63-
self.assertEqual(expected, gyp.common.GetFlavor(param))
61+
assert expected == gyp.common.GetFlavor(param)
6462

6563
def test_platform_default(self):
6664
self.assertFlavor("freebsd", "freebsd9", {})
@@ -90,47 +88,84 @@ def test_GetCrossCompilerPredefines(self, mock_mkstemp, mock_unlink, mock_close)
9088
mock_unlink.return_value = None
9189
mock_mkstemp.return_value = (0, "temp.c")
9290

93-
def mock_run(env, defines_stdout):
94-
with patch("subprocess.Popen") as mock_popen:
91+
def mock_run(env, defines_stdout, expected_cmd):
92+
with patch("subprocess.run") as mock_run:
9593
mock_process = MagicMock()
96-
mock_process.communicate.return_value = (
97-
TestGetFlavor.MockCommunicate(defines_stdout), None)
98-
mock_process.stdout = MagicMock()
99-
mock_popen.return_value = mock_process
94+
mock_process.returncode = 0
95+
mock_process.stdout = TestGetFlavor.MockCommunicate(defines_stdout)
96+
mock_run.return_value = mock_process
10097
expected_input = "temp.c" if sys.platform == "win32" else "/dev/null"
10198
with patch.dict(os.environ, env):
10299
defines = gyp.common.GetCrossCompilerPredefines()
103100
flavor = gyp.common.GetFlavor({})
104101
if env.get("CC_target"):
105-
mock_popen.assert_called_with(
106-
[env["CC_target"], "-dM", "-E", "-x", "c", expected_input],
102+
mock_run.assert_called_with(
103+
[
104+
*expected_cmd,
105+
"-dM", "-E", "-x", "c", expected_input
106+
],
107107
shell=sys.platform == "win32",
108-
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
108+
capture_output=True, check=True)
109109
return [defines, flavor]
110110

111-
[defines1, _] = mock_run({}, "")
112-
self.assertDictEqual({}, defines1)
111+
[defines1, _] = mock_run({}, "", [])
112+
assert {} == defines1
113113

114114
[defines2, flavor2] = mock_run(
115115
{ "CC_target": "/opt/wasi-sdk/bin/clang" },
116-
"#define __wasm__ 1\n#define __wasi__ 1\n"
116+
"#define __wasm__ 1\n#define __wasi__ 1\n",
117+
["/opt/wasi-sdk/bin/clang"]
117118
)
118-
self.assertDictEqual({ "__wasm__": "1", "__wasi__": "1" }, defines2)
119-
self.assertEqual("wasi", flavor2)
119+
assert { "__wasm__": "1", "__wasi__": "1" } == defines2
120+
assert flavor2 == "wasi"
120121

121122
[defines3, flavor3] = mock_run(
122-
{ "CC_target": "/opt/wasi-sdk/bin/clang" },
123-
"#define __wasm__ 1\n"
123+
{ "CC_target": "/opt/wasi-sdk/bin/clang --target=wasm32" },
124+
"#define __wasm__ 1\n",
125+
["/opt/wasi-sdk/bin/clang", "--target=wasm32"]
124126
)
125-
self.assertDictEqual({ "__wasm__": "1" }, defines3)
126-
self.assertEqual("wasm", flavor3)
127+
assert { "__wasm__": "1" } == defines3
128+
assert flavor3 == "wasm"
127129

128130
[defines4, flavor4] = mock_run(
129131
{ "CC_target": "/emsdk/upstream/emscripten/emcc" },
130-
"#define __EMSCRIPTEN__ 1\n"
132+
"#define __EMSCRIPTEN__ 1\n",
133+
["/emsdk/upstream/emscripten/emcc"]
134+
)
135+
assert { "__EMSCRIPTEN__": "1" } == defines4
136+
assert flavor4 == "emscripten"
137+
138+
# Test path which include white space
139+
[defines5, flavor5] = mock_run(
140+
{
141+
"CC_target": "\"/Users/Toyo Li/wasi-sdk/bin/clang\" -O3",
142+
"CFLAGS": "--target=wasm32-wasi-threads -pthread"
143+
},
144+
"#define __wasm__ 1\n#define __wasi__ 1\n#define _REENTRANT 1\n",
145+
[
146+
"/Users/Toyo Li/wasi-sdk/bin/clang",
147+
"-O3",
148+
"--target=wasm32-wasi-threads",
149+
"-pthread"
150+
]
151+
)
152+
assert {
153+
"__wasm__": "1",
154+
"__wasi__": "1",
155+
"_REENTRANT": "1"
156+
} == defines5
157+
assert flavor5 == "wasi"
158+
159+
original_sep = os.sep
160+
os.sep = "\\"
161+
[defines6, flavor6] = mock_run(
162+
{ "CC_target": "\"C:\\Program Files\\wasi-sdk\\clang.exe\"" },
163+
"#define __wasm__ 1\n#define __wasi__ 1\n",
164+
["C:/Program Files/wasi-sdk/clang.exe"]
131165
)
132-
self.assertDictEqual({ "__EMSCRIPTEN__": "1" }, defines4)
133-
self.assertEqual("emscripten", flavor4)
166+
os.sep = original_sep
167+
assert { "__wasm__": "1", "__wasi__": "1" } == defines6
168+
assert flavor6 == "wasi"
134169

135170
if __name__ == "__main__":
136171
unittest.main()

0 commit comments

Comments
 (0)