diff --git a/distutils/_msvccompiler.py b/distutils/_msvccompiler.py index d07c86ef..d6b30265 100644 --- a/distutils/_msvccompiler.py +++ b/distutils/_msvccompiler.py @@ -5,6 +5,7 @@ __all__ = ["MSVCCompiler"] MSVCCompiler = msvc.Compiler +ClangCLCompiler = msvc.ClangCLCompiler def __getattr__(name): diff --git a/distutils/compilers/C/base.py b/distutils/compilers/C/base.py index 93385e13..82f3b7f2 100644 --- a/distutils/compilers/C/base.py +++ b/distutils/compilers/C/base.py @@ -1238,6 +1238,7 @@ def get_default_compiler(osname: str | None = None, platform: str | None = None) ), 'bcpp': ('bcppcompiler', 'BCPPCompiler', "Borland C++ Compiler"), 'zos': ('zosccompiler', 'zOSCCompiler', 'IBM XL C/C++ Compilers'), + 'clangcl': ('_msvccompiler', 'ClangCLCompiler', 'LLVM Clang-CL compiler'), } diff --git a/distutils/compilers/C/msvc.py b/distutils/compilers/C/msvc.py index 6db062a9..060be00c 100644 --- a/distutils/compilers/C/msvc.py +++ b/distutils/compilers/C/msvc.py @@ -176,7 +176,7 @@ def _get_vc_env(plat_spec): return env -def _find_exe(exe, paths=None): +def _find_exe(exe, paths=None, clangcl=False): """Return path to an MSVC executable program. Tries to find the program in several places: first, one of the @@ -184,13 +184,29 @@ def _find_exe(exe, paths=None): in the PATH environment variable. If any of those work, return an absolute path that is known to exist. If none of them work, just return the original program name, 'exe'. + + If clangcl is set to true, look for the LLVM clang-cl executables, + as well as look for them without the extension (eg. on Linux) """ + if clangcl: + if exe == 'cl.exe': + exe = 'clang-{}'.format(exe) + elif exe == 'link.exe': + exe = 'lld-{}'.format(exe) + elif exe == 'mc.exe': + exe = 'llvm-ml.exe' + else: + exe = 'llvm-{}'.format(exe) if not paths: paths = os.getenv('path').split(os.pathsep) for p in paths: fn = os.path.join(os.path.abspath(p), exe) if os.path.isfile(fn): return fn + elif clangcl: + fn = os.path.splitext(fn)[0] + if os.path.isfile(fn): + return fn return exe @@ -201,6 +217,32 @@ def _find_exe(exe, paths=None): 'win-arm64': 'arm64', } +_clang_targets = { + 'win32': 'i686', + 'win-amd64': 'x86_64', + 'win-arm32': 'armv7', + 'win-arm64': 'aarch64', +} + + +def _get_external_sdk(linker=False): + sdk = [] + vctoolsdir = os.environ.get('VCTOOLSINSTALLDIR', None) + winsdkdir = os.environ.get('WINDOWSSDKDIR', None) + if vctoolsdir: + _vctoolsdir = ['/vctoolsdir', vctoolsdir] + if linker: + sdk.append(":".join(_vctoolsdir)) + else: + sdk += _vctoolsdir + if winsdkdir: + _winsdkdir = ['/winsdkdir', winsdkdir] + if linker: + sdk.append(":".join(_winsdkdir)) + else: + sdk += _winsdkdir + return sdk + def _get_vcvars_spec(host_platform, platform): """ @@ -298,14 +340,16 @@ def initialize(self, plat_name: str | None = None) -> None: ) self._configure(vc_env) + clangcl = True if self.compiler_type == "clangcl" else False + self._paths = vc_env.get('path', '') paths = self._paths.split(os.pathsep) - self.cc = _find_exe("cl.exe", paths) - self.linker = _find_exe("link.exe", paths) - self.lib = _find_exe("lib.exe", paths) - self.rc = _find_exe("rc.exe", paths) # resource compiler - self.mc = _find_exe("mc.exe", paths) # message compiler - self.mt = _find_exe("mt.exe", paths) # message compiler + self.cc = _find_exe("cl.exe", paths, clangcl) + self.linker = _find_exe("link.exe", paths, clangcl) + self.lib = _find_exe("lib.exe", paths, clangcl) + self.rc = _find_exe("rc.exe", paths, clangcl) # resource compiler + self.mc = _find_exe("mc.exe", paths, clangcl) # message compiler + self.mt = _find_exe("mt.exe", paths, clangcl) # message compiler self.preprocess_options = None # bpo-38597: Always compile with dynamic linking @@ -326,6 +370,16 @@ def initialize(self, plat_name: str | None = None) -> None: ldflags_debug = ['/nologo', '/INCREMENTAL:NO', '/LTCG', '/DEBUG:FULL'] + if clangcl: + target = '--target={}-windows-msvc'.format(_clang_targets[plat_name]) + compile_sdk = _get_external_sdk() + self.compile_options.remove('/GL') + self.compile_options += ['/FA', target] + compile_sdk + self.compile_options_debug += ['/FA', target] + compile_sdk + linker_sdk = _get_external_sdk(linker=True) + ldflags += linker_sdk + ldflags_debug += linker_sdk + self.ldflags_exe = [*ldflags, '/MANIFEST:EMBED,ID=1'] self.ldflags_exe_debug = [*ldflags_debug, '/MANIFEST:EMBED,ID=1'] self.ldflags_shared = [ @@ -437,7 +491,11 @@ def compile( # noqa: C901 rc_dir = os.path.dirname(obj) try: # first compile .MC to .RC and .H file - self.spawn([self.mc, '-h', h_dir, '-r', rc_dir, src]) + mc_cmd = [self.mc] + if clangcl and '64' in plat_name: + mc_cmd.append('--m64') + mc_cmd += ['-h', h_dir, '-r', rc_dir, src] + self.spawn(mc_cmd) base, _ = os.path.splitext(os.path.basename(src)) rc_file = os.path.join(rc_dir, base + '.rc') # then compile .RC to .RES file @@ -612,3 +670,8 @@ def find_library_file(self, dirs, lib, debug=False): else: # Oops, didn't find it in *any* of 'dirs' return None + + +class ClangCLCompiler(Compiler): + + compiler_type = 'clangcl' diff --git a/distutils/compilers/C/tests/test_clangcl.py b/distutils/compilers/C/tests/test_clangcl.py new file mode 100644 index 00000000..5dcd7c4f --- /dev/null +++ b/distutils/compilers/C/tests/test_clangcl.py @@ -0,0 +1,26 @@ +import os +from .. import msvc + + +class TestClangCLCompiler: + def test_compiler_type(self): + compiler = msvc.ClangCLCompiler() + assert compiler.compiler_type == 'clangcl' + + def test_set_executables(self): + compiler = msvc.ClangCLCompiler() + compiler.initialize() + + cc, cc_ext = os.path.splitext(compiler.cc) + linker, linker_ext = os.path.splitext(compiler.linker) + lib, lib_ext = os.path.splitext(compiler.lib) + rc, rc_ext = os.path.splitext(compiler.rc) + mc, mc_ext = os.path.splitext(compiler.mc) + mt, mt_ext = os.path.splitext(compiler.mt) + + assert compiler.cc == 'clang-cl' + cc_ext + assert compiler.linker == 'lld-link' + linker_ext + assert compiler.lib == 'llvm-lib' + lib_ext + assert compiler.rc == 'llvm-rc' + rc_ext + assert compiler.mc == 'llvm-ml' + mc_ext + assert compiler.mt == 'llvm-mt' + mt_ext diff --git a/distutils/tests/__init__.py b/distutils/tests/__init__.py index 5a8ab061..b7b92858 100644 --- a/distutils/tests/__init__.py +++ b/distutils/tests/__init__.py @@ -24,12 +24,12 @@ def missing_compiler_executable(cmd_names: Sequence[str] = []): # pragma: no co compiler = ccompiler.new_compiler() sysconfig.customize_compiler(compiler) - if compiler.compiler_type == "msvc": + if compiler.compiler_type == "msvc" or compiler.compiler_type == "clangcl": # MSVC has no executables, so check whether initialization succeeds try: compiler.initialize() except errors.DistutilsPlatformError: - return "msvc" + return compiler.compiler_type for name in compiler.executables: if cmd_names and name not in cmd_names: continue diff --git a/newsfragments/376.feature.rst b/newsfragments/376.feature.rst new file mode 100644 index 00000000..fdb57852 --- /dev/null +++ b/newsfragments/376.feature.rst @@ -0,0 +1 @@ +Added ClangCL compiler support. \ No newline at end of file