Skip to content

Commit dffcfb8

Browse files
committed
On Windows, get the C compiler version if running _getcompiler fails
1 parent 8feb3ad commit dffcfb8

4 files changed

Lines changed: 153 additions & 27 deletions

File tree

‎Lib/test/test_tools/test_getbuildinfo.py‎

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Test Tools/build/generate_getbuildinfo.py."""
22

3+
import contextlib
34
import locale
45
import os
56
import subprocess
@@ -22,6 +23,23 @@
2223

2324

2425
class TestGetBuildInfo(unittest.TestCase):
26+
@contextlib.contextmanager
27+
def check_log(self, log):
28+
try:
29+
with support.captured_stdout() as stdout:
30+
yield
31+
finally:
32+
self.assertEqual(stdout.getvalue(),
33+
f'{SCRIPT_NAME}: {log}\n')
34+
35+
@contextlib.contextmanager
36+
def check_exit_fail(self, errmsg):
37+
with self.check_log(errmsg):
38+
try:
39+
yield
40+
except SystemExit as exc:
41+
self.assertEqual(exc.code, 1)
42+
2543
def test_get_py_version(self):
2644
with os_helper.change_cwd(SRC_DIR):
2745
PY_VERSION = generate_getbuildinfo.get_py_version()
@@ -151,12 +169,63 @@ def check_unchanged(compiler):
151169
check_unchanged('MSC v.1951 64 bit (AMD64)')
152170
check_unchanged('MSC v.1951 32 bit (Intel)')
153171

154-
with support.captured_stdout() as stdout:
172+
log = ("Truncate compiler string 'Clang 21.0.0 "
173+
"(clang-2100.1.1.101)' to 'Clang 21.0.0'")
174+
with self.check_log(log):
155175
compiler = get_compiler('Clang 21.0.0 (clang-2100.1.1.101)')
156176
self.assertEqual(compiler, '[Clang 21.0.0]')
157-
self.assertEqual(stdout.getvalue(),
158-
f"{SCRIPT_NAME}: Truncate compiler string 'Clang 21.0.0 "
159-
"(clang-2100.1.1.101)' to 'Clang 21.0.0'\n")
177+
178+
def test_get_c_compiler_version_windows(self):
179+
get_compiler_version = generate_getbuildinfo.get_c_compiler_version_windows
180+
C_COMPILER = 'CL.EXE'
181+
182+
tests = []
183+
184+
# Test a full output
185+
output = textwrap.dedent('''
186+
Microsoft (R) C/C++ Optimizing Compiler Version 19.51.36260 for ARM64
187+
Copyright (C) Microsoft Corporation. All rights reserved.
188+
189+
usage: cl [ option... ] filename... [ /link linkoption... ]
190+
''').strip()
191+
tests.append((output, 'MSC v.1951 64 bit (ARM64)'))
192+
193+
output = 'Microsoft (R) C/C++ Optimizing Compiler Version 19.51.36260 for x64'
194+
tests.append((output, 'MSC v.1951 64 bit (AMD64)'))
195+
196+
output = 'Microsoft (R) C/C++ Optimizing Compiler Version 19.51.36260 for x86'
197+
tests.append((output, 'MSC v.1951 32 bit (Intel)'))
198+
199+
output = 'Microsoft (R) C/C++ Optimizing Compiler Version 19.51.36260 for ARM'
200+
tests.append((output, 'MSC v.1951 32 bit (ARM)'))
201+
202+
def check_version(output, expected):
203+
with mock.patch.object(generate_getbuildinfo,
204+
'run_command') as mock_run_command:
205+
mock_run_command.return_value = output
206+
version = get_compiler_version(C_COMPILER)
207+
self.assertEqual(version, expected)
208+
mock_run_command.assert_called_once_with([C_COMPILER],
209+
check=False, stderr=True)
210+
211+
for output, expected in tests:
212+
with self.subTest(output=output, expected=expected):
213+
check_version(output, expected)
214+
215+
def check_error(output, errmsg):
216+
with mock.patch.object(generate_getbuildinfo,
217+
'run_command') as mock_run_command:
218+
mock_run_command.return_value = output
219+
with self.check_exit_fail(errmsg):
220+
get_compiler_version(C_COMPILER)
221+
222+
output = 'xxx'
223+
check_error(output, f'Unable to parse cl.exe version: {output!r}')
224+
225+
platform = 'PLATFORM'
226+
output = ('Microsoft (R) C/C++ Optimizing Compiler '
227+
f'Version 19.51.36260 for {platform}')
228+
check_error(output, f'Unknown cl.exe platform: {platform!r}')
160229

161230
def test_functional(self):
162231
old_locale = locale.setlocale(locale.LC_ALL)
@@ -179,9 +248,8 @@ def test_functional(self):
179248
with support.swap_attr(sys, 'argv', argv):
180249
with os_helper.EnvironmentVarGuard() as env:
181250
env['SOURCE_DATE_EPOCH'] = str(1791419852)
182-
with support.captured_stdout() as stdout:
251+
with self.check_log(f'{filename} updated'):
183252
generate_getbuildinfo.main()
184-
self.assertEqual(stdout.getvalue(), f'{SCRIPT_NAME}: {filename} updated\n')
185253

186254
with open(filename) as fp:
187255
output = fp.read()

‎PCbuild/_freeze_module.vcxproj‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,10 @@
466466
<Message Text="Updated files: @(_UpdatedGetPath->'%(Filename)%(Extension)',', ')"
467467
Condition="'@(_UpdatedGetPath)' != ''" Importance="high" />
468468
</Target>
469-
<Target Name="_GetBuildInfoHeader" BeforeTargets="PrepareForBuild"
469+
<!-- Use AfterTargets to get ClCompilerPath variable -->
470+
<Target Name="_GetBuildInfoHeader"
471+
BeforeTargets="PrepareForBuild"
472+
AfterTargets="ClCompile"
470473
Inputs="Tools\build\generate_getbuildinfo.py"
471474
Outputs="Modules\getbuildinfo.h"
472475
DependsOnTargets="FindPythonForBuild">
@@ -480,7 +483,7 @@
480483
</PropertyGroup>
481484

482485
<Message Text="Run generate_getbuildinfo.py" Importance="high" />
483-
<Exec Command='$(PythonForBuild) Tools\build\generate_getbuildinfo.py --platform=win32 --git-program="$(_GIT)" --getcompiler-program="$(GetCompilerExe)" --free-threading=$(FreeThreading)'
486+
<Exec Command='$(PythonForBuild) Tools\build\generate_getbuildinfo.py --platform=win32 --git-program="$(_GIT)" --c-compiler="$(ClCompilerPath)" --getcompiler-program="$(GetCompilerExe)" --free-threading=$(FreeThreading)'
484487
WorkingDirectory="$(PySourcePath)" />
485488
</Target>
486489
<Target Name="_RebuildFrozen" AfterTargets="AfterBuild" Condition="$(Configuration) != 'PGUpdate'">

‎PCbuild/_getcompiler.vcxproj‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,13 @@
112112
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
113113
<ImportGroup Label="ExtensionTargets">
114114
</ImportGroup>
115+
116+
<Target Name="_DebugGetCompiler" AfterTargets="ClCompile" BeforeTargets="PrepareForBuild">
117+
<Message Text="MSBuildToolsPath: $(MSBuildToolsPath)" Importance="high" />
118+
<Message Text="CLToolExe: $(CLToolExe)" Importance="high" />
119+
<Message Text="CLToolPath: $(CLToolPath)" Importance="high" />
120+
<Message Text="ClCompilerPath: $(ClCompilerPath)" Importance="high" />
121+
<Message Text="VCINSTALLDIR: %VCINSTALLDIR%" Importance="high" />
122+
</Target>
123+
115124
</Project>

‎Tools/build/generate_getbuildinfo.py‎

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ def create_makefile_regex(name):
3636
r'([0-9]+\.[0-9a-z.+]+)' # version
3737
)
3838

39+
# Parse 'Microsoft (R) C/C++ Optimizing Compiler Version 19.51.36260 for ARM64'
40+
CL_VERSION_REGEX = re.compile(r'Compiler Version ([0-9]+).([0-9]+).[0-9]+ for (.*)')
41+
CL_VERSION_SUFFIX = {
42+
'x86': "32 bit (Intel)",
43+
'x64': "64 bit (AMD64)",
44+
'ARM': "32 bit (ARM)",
45+
'ARM64': "64 bit (ARM64)",
46+
}
47+
3948

4049
def log(msg):
4150
print(f"{SCRIPT_NAME}: {msg}")
@@ -127,16 +136,23 @@ def parse_args():
127136
parser.add_argument('--git-tag', type=str)
128137
parser.add_argument('--git-branch', type=str)
129138
parser.add_argument('--getcompiler-program', type=str)
139+
parser.add_argument('--c-compiler', type=str)
130140
parser.add_argument('--compiler', type=str)
131141
parser.add_argument('--free-threading', type=int)
132142
return parser.parse_args()
133143

134144

135-
def run_command(cmd, *, check=True):
145+
def run_command(cmd, *, check=True, stderr=False):
136146
cmd_str = shlex.join(cmd)
137147
log(f"+ {cmd_str}")
148+
kwargs = {}
149+
if stderr:
150+
kwargs['stderr'] = subprocess.STDOUT
138151
try:
139-
proc = subprocess.run(cmd, stdout=subprocess.PIPE, text=True)
152+
proc = subprocess.run(cmd,
153+
stdout=subprocess.PIPE,
154+
text=True,
155+
**kwargs)
140156
except OSError as exc:
141157
error = f'with: {exc!r}'
142158
else:
@@ -157,7 +173,45 @@ def run_command(cmd, *, check=True):
157173
return proc.stdout.rstrip()
158174

159175

160-
def _get_compiler(getcompiler=None):
176+
def get_c_compiler_version_windows(c_compiler):
177+
cmd = [c_compiler]
178+
output = run_command(cmd, check=False, stderr=True)
179+
if not output:
180+
return None
181+
182+
# Get the first line
183+
line = output.splitlines()[0]
184+
match = CL_VERSION_REGEX.search(line)
185+
if not match:
186+
exit_error(f"Unable to parse cl.exe version: {output!r}")
187+
188+
major = match.group(1)
189+
minor = match.group(2)
190+
platform = match.group(3)
191+
suffix = CL_VERSION_SUFFIX.get(platform)
192+
if not suffix:
193+
exit_error(f"Unknown cl.exe platform: {platform!r}")
194+
195+
return f"MSC v.{major}{minor} {suffix}"
196+
197+
198+
def get_c_compiler_version_unix(c_compiler=None):
199+
CC = get_makefile_cc()
200+
if not CC:
201+
exit_error(f"ERROR: Unable to locate CC in Makefile")
202+
203+
cmd = shlex.split(CC)
204+
cmd = [*cmd, '--version']
205+
206+
output = run_command(cmd, check=False)
207+
if output:
208+
# Get the first line
209+
return output.splitlines()[0]
210+
211+
return None
212+
213+
214+
def _get_compiler(getcompiler=None, c_compiler=None):
161215
# Run _getcompiler program
162216
if not getcompiler:
163217
getcompiler = os.path.join('Programs', '_getcompiler')
@@ -174,21 +228,13 @@ def _get_compiler(getcompiler=None):
174228
if compiler:
175229
return compiler
176230

231+
# Running _getcompiler failed, get the C compiler version
177232
if MS_WINDOWS:
233+
if c_compiler:
234+
return get_c_compiler_version_windows(c_compiler)
178235
return None
179-
180-
# Running _getcompiler failed, run the compiler with --version
181-
CC = get_makefile_cc()
182-
if not CC:
183-
exit_error(f"ERROR: Unable to locate CC in Makefile")
184-
185-
cmd = shlex.split(CC)
186-
output = run_command([*cmd, '--version'], check=False)
187-
if output:
188-
# Get the first line
189-
return output.splitlines()[0]
190-
191-
return None
236+
else:
237+
return get_c_compiler_version_unix()
192238

193239

194240
def compact_compiler(compiler):
@@ -215,9 +261,9 @@ def compact_compiler(compiler):
215261
return compiler
216262

217263

218-
def get_compiler(compiler, getcompiler_program=None):
264+
def get_compiler(compiler, getcompiler=None, c_compiler=None):
219265
if not compiler:
220-
compiler = _get_compiler(getcompiler_program)
266+
compiler = _get_compiler(getcompiler, c_compiler)
221267
if not compiler:
222268
# Default compiler name when everything else failed
223269
# (see Programs/_getcompiler.c)
@@ -263,7 +309,7 @@ def main():
263309
if not platform:
264310
platform = "unknown"
265311

266-
compiler = get_compiler(args.compiler, args.getcompiler_program)
312+
compiler = get_compiler(args.compiler, args.getcompiler_program, args.c_compiler)
267313

268314
build_info, git_id = get_build_info(git_tag, git_branch, git_version)
269315

0 commit comments

Comments
 (0)