Skip to content

Commit 72aaa37

Browse files
committed
ClangCL compiler support
1 parent 603b94e commit 72aaa37

File tree

6 files changed

+104
-10
lines changed

6 files changed

+104
-10
lines changed

distutils/_msvccompiler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
__all__ = ["MSVCCompiler"]
66

77
MSVCCompiler = msvc.Compiler
8+
ClangCLCompiler = msvc.ClangCLCompiler
89

910

1011
def __getattr__(name):

distutils/compilers/C/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,6 +1238,7 @@ def get_default_compiler(osname: str | None = None, platform: str | None = None)
12381238
),
12391239
'bcpp': ('bcppcompiler', 'BCPPCompiler', "Borland C++ Compiler"),
12401240
'zos': ('zosccompiler', 'zOSCCompiler', 'IBM XL C/C++ Compilers'),
1241+
'clangcl': ('_msvccompiler', 'ClangCLCompiler', 'LLVM Clang-CL compiler'),
12411242
}
12421243

12431244

distutils/compilers/C/msvc.py

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -176,21 +176,37 @@ def _get_vc_env(plat_spec):
176176
return env
177177

178178

179-
def _find_exe(exe, paths=None):
179+
def _find_exe(exe, paths=None, clangcl=False):
180180
"""Return path to an MSVC executable program.
181181
182182
Tries to find the program in several places: first, one of the
183183
MSVC program search paths from the registry; next, the directories
184184
in the PATH environment variable. If any of those work, return an
185185
absolute path that is known to exist. If none of them work, just
186186
return the original program name, 'exe'.
187+
188+
If clangcl is set to true, look for the LLVM clang-cl executables,
189+
as well as look for them without the extension (eg. on Linux)
187190
"""
191+
if clangcl:
192+
if exe == 'cl.exe':
193+
exe = 'clang-{}'.format(exe)
194+
elif exe == 'link.exe':
195+
exe = 'lld-{}'.format(exe)
196+
elif exe == 'mc.exe':
197+
exe = 'llvm-ml.exe'
198+
else:
199+
exe = 'llvm-{}'.format(exe)
188200
if not paths:
189201
paths = os.getenv('path').split(os.pathsep)
190202
for p in paths:
191203
fn = os.path.join(os.path.abspath(p), exe)
192204
if os.path.isfile(fn):
193205
return fn
206+
elif clangcl:
207+
fn = os.path.splitext(fn)[0]
208+
if os.path.isfile(fn):
209+
return fn
194210
return exe
195211

196212

@@ -201,6 +217,32 @@ def _find_exe(exe, paths=None):
201217
'win-arm64': 'arm64',
202218
}
203219

220+
_clang_targets = {
221+
'win32': 'i686',
222+
'win-amd64': 'x86_64',
223+
'win-arm32': 'armv7',
224+
'win-arm64': 'aarch64',
225+
}
226+
227+
228+
def _get_external_sdk(linker=False):
229+
sdk = []
230+
vctoolsdir = os.environ.get('DISTUTILS_VCTOOLS_DIR', None)
231+
winsdkdir = os.environ.get('DISTUTILS_WINSDK_DIR', None)
232+
if vctoolsdir:
233+
_vctoolsdir = ['/vctoolsdir', vctoolsdir]
234+
if linker:
235+
sdk.append(":".join(_vctoolsdir))
236+
else:
237+
sdk += _vctoolsdir
238+
if winsdkdir:
239+
_winsdkdir = ['/winsdkdir', winsdkdir]
240+
if linker:
241+
sdk.append(":".join(_winsdkdir))
242+
else:
243+
sdk += _winsdkdir
244+
return sdk
245+
204246

205247
def _get_vcvars_spec(host_platform, platform):
206248
"""
@@ -298,14 +340,16 @@ def initialize(self, plat_name: str | None = None) -> None:
298340
)
299341
self._configure(vc_env)
300342

343+
clangcl = True if self.compiler_type == "clangcl" else False
344+
301345
self._paths = vc_env.get('path', '')
302346
paths = self._paths.split(os.pathsep)
303-
self.cc = _find_exe("cl.exe", paths)
304-
self.linker = _find_exe("link.exe", paths)
305-
self.lib = _find_exe("lib.exe", paths)
306-
self.rc = _find_exe("rc.exe", paths) # resource compiler
307-
self.mc = _find_exe("mc.exe", paths) # message compiler
308-
self.mt = _find_exe("mt.exe", paths) # message compiler
347+
self.cc = _find_exe("cl.exe", paths, clangcl)
348+
self.linker = _find_exe("link.exe", paths, clangcl)
349+
self.lib = _find_exe("lib.exe", paths, clangcl)
350+
self.rc = _find_exe("rc.exe", paths, clangcl) # resource compiler
351+
self.mc = _find_exe("mc.exe", paths, clangcl) # message compiler
352+
self.mt = _find_exe("mt.exe", paths, clangcl) # message compiler
309353

310354
self.preprocess_options = None
311355
# bpo-38597: Always compile with dynamic linking
@@ -326,6 +370,16 @@ def initialize(self, plat_name: str | None = None) -> None:
326370

327371
ldflags_debug = ['/nologo', '/INCREMENTAL:NO', '/LTCG', '/DEBUG:FULL']
328372

373+
if clangcl:
374+
target = '--target={}-windows-msvc'.format(_clang_targets[plat_name])
375+
compile_sdk = _get_external_sdk()
376+
self.compile_options.remove('/GL')
377+
self.compile_options += ['/FA', target] + compile_sdk
378+
self.compile_options_debug += ['/FA', target] + compile_sdk
379+
linker_sdk = _get_external_sdk(linker=True)
380+
ldflags += linker_sdk
381+
ldflags_debug += linker_sdk
382+
329383
self.ldflags_exe = [*ldflags, '/MANIFEST:EMBED,ID=1']
330384
self.ldflags_exe_debug = [*ldflags_debug, '/MANIFEST:EMBED,ID=1']
331385
self.ldflags_shared = [
@@ -437,7 +491,11 @@ def compile( # noqa: C901
437491
rc_dir = os.path.dirname(obj)
438492
try:
439493
# first compile .MC to .RC and .H file
440-
self.spawn([self.mc, '-h', h_dir, '-r', rc_dir, src])
494+
mc_cmd = [self.mc]
495+
if clangcl and '64' in plat_name:
496+
mc_cmd.append('--m64')
497+
mc_cmd += ['-h', h_dir, '-r', rc_dir, src]
498+
self.spawn(mc_cmd)
441499
base, _ = os.path.splitext(os.path.basename(src))
442500
rc_file = os.path.join(rc_dir, base + '.rc')
443501
# then compile .RC to .RES file
@@ -612,3 +670,8 @@ def find_library_file(self, dirs, lib, debug=False):
612670
else:
613671
# Oops, didn't find it in *any* of 'dirs'
614672
return None
673+
674+
675+
class ClangCLCompiler(Compiler):
676+
677+
compiler_type = 'clangcl'
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import os
2+
from .. import msvc
3+
4+
5+
class TestClangCLCompiler:
6+
def test_compiler_type(self):
7+
compiler = msvc.ClangCLCompiler()
8+
assert compiler.compiler_type == 'clangcl'
9+
10+
def test_set_executables(self):
11+
compiler = msvc.ClangCLCompiler()
12+
compiler.initialize()
13+
14+
cc, cc_ext = os.path.splitext(compiler.cc)
15+
linker, linker_ext = os.path.splitext(compiler.linker)
16+
lib, lib_ext = os.path.splitext(compiler.lib)
17+
rc, rc_ext = os.path.splitext(compiler.rc)
18+
mc, mc_ext = os.path.splitext(compiler.mc)
19+
mt, mt_ext = os.path.splitext(compiler.mt)
20+
21+
assert compiler.cc == 'clang-cl' + cc_ext
22+
assert compiler.linker == 'lld-link' + linker_ext
23+
assert compiler.lib == 'llvm-lib' + lib_ext
24+
assert compiler.rc == 'llvm-rc' + rc_ext
25+
assert compiler.mc == 'llvm-ml' + mc_ext
26+
assert compiler.mt == 'llvm-mt' + mt_ext

distutils/tests/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ def missing_compiler_executable(cmd_names: Sequence[str] = []): # pragma: no co
2424

2525
compiler = ccompiler.new_compiler()
2626
sysconfig.customize_compiler(compiler)
27-
if compiler.compiler_type == "msvc":
27+
if compiler.compiler_type == "msvc" or compiler.compiler_type == "clangcl":
2828
# MSVC has no executables, so check whether initialization succeeds
2929
try:
3030
compiler.initialize()
3131
except errors.DistutilsPlatformError:
32-
return "msvc"
32+
return compiler.compiler_type
3333
for name in compiler.executables:
3434
if cmd_names and name not in cmd_names:
3535
continue

newsfragments/376.feature.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Added ClangCL compiler support.
2+
Added DISTUTILS_VCTOOLS_DIR environment variable to set a specific vctools directory to ClangCL
3+
Added DISTUTILS_WINSDK_DIR environment variable to set a specific winsdk directory to ClangCL

0 commit comments

Comments
 (0)