# @file ClangToolChain.py # Plugin to configures paths for the Clang tool chain ## # This plugin works in conjunction with the tools_def # # Copyright (c) Microsoft Corporation # SPDX-License-Identifier: BSD-2-Clause-Patent ## import logging import shutil from io import StringIO from pathlib import Path from edk2toolext.environment import shell_environment, version_aggregator from edk2toolext.environment.plugintypes.uefi_build_plugin import ( IUefiBuildPlugin, ) from edk2toollib.utility_functions import GetHostInfo, RunCmd from edk2toollib.windows import locate_tools from edk2toollib.windows.locate_tools import FindWithVsWhere SUPPORTED_TOOL_CHAINS = ("CLANGPDB", "CLANGDWARF") # VS environment variables needed to configure SDK/include/lib paths for # both CLANGPDB (nmake host) and VS-bundled clang resolution. _VS_INTERESTING_KEYS = [ "ExtensionSdkDir", "INCLUDE", "LIB", "LIBPATH", "UniversalCRTSdkDir", "UCRTVersion", "WindowsLibPath", "WindowsSdkBinPath", "WindowsSdkDir", "WindowsSdkVerBinPath", "WindowsSDKVersion", "WindowsSDKLibVersion", "VCToolsInstallDir", "Path", ] class ClangToolChain(IUefiBuildPlugin): def do_post_build(self, thebuilder): """No-op post-build hook required by IUefiBuildPlugin.""" return 0 def do_pre_build(self, thebuilder): """Configure CLANG_BIN and report tool versions for CLANGPDB/CLANGDWARF. Resolves the clang binary in this order: 1. CLANG_BIN environment variable 2. System PATH 3. Default LLVM install path (Windows) 4. VS-installed clang (Windows only) Returns 0 on success, 1 if clang cannot be found. """ self.Logger = logging.getLogger("ClangPdbToolChain") ## # CLANGPDB/CLANGDWARF # - Need to find the clang path. # - Report path and version for logging # # if CLANG_BIN already set the plugin will confirm it exists and # get the version of clang # If not set it will look for clang on the path. If found it will # configure for that. # if still not found it will try the default install directory. # finally an error will be reported if not found ## tool_chain_tag = thebuilder.env.GetValue("TOOL_CHAIN_TAG") if tool_chain_tag.upper() not in SUPPORTED_TOOL_CHAINS: return 0 HostInfo = GetHostInfo() # # Windows-specific host build tool setup for CLANGPDB and CLANGDWARF. # CLANGPDB on Windows uses nmake from the VS compiler toolchain or # mingw32-make. CLANGDWARF always uses mingw32-make. # if HostInfo.os == "Windows": env = shell_environment.GetEnvironment() if tool_chain_tag == "CLANGPDB": # Look for $(CLANG_BIN)/mingw32-make.exe to determine if in a mingw32 environment clang_bin_str = env.get_shell_var("CLANG_BIN") clang_bin_path = Path(clang_bin_str) if clang_bin_str else None if clang_bin_path and (clang_bin_path / "mingw32-make.exe").exists(): self.Logger.debug("CLANG_BIN is set to a mingw32 toolchain.") # CLANGPDB requires lld-link.exe to generate PE/COFF images. if not (clang_bin_path / "lld-link.exe").exists(): self.Logger.error("CLANG_BIN is set to a mingw32 toolchain but lld-link.exe not found.") return -1 if self._configure_mingw32_host("CLANGPDB") is None: return -1 else: # VS nmake path: resolve host type and query VS environment variables. HostType = self._apply_vs_vars(HostInfo) if not HostType: self.Logger.error(f"CLANGPDB not supported for detected host [{HostInfo.arch}-{HostInfo.bit}]") return -1 # Add path to IA32 toolchain DLLs for unit test execution if vc_tools_install_dir := env.get_shell_var("VCToolsInstallDir"): ia32_dll_path = Path(vc_tools_install_dir) / 'bin' / 'Hostx64' / 'x86' if ia32_dll_path.exists(): env.append_path(str(ia32_dll_path)) else: self.Logger.warning("IA32 toolchain DLLs not found in expected path %s. IA32 unit tests may fail to run." % ia32_dll_path) if env.get_shell_var("CLANG_HOST_BIN") is not None: self.Logger.debug("CLANG_HOST_BIN is already set.") else: install_path = self._get_vs_install_path(None, None) vc_ver = self._get_vc_version(install_path, None) if install_path is None or vc_ver is None: self.Logger.error("Failed to configure environment for VS") return -1 version_aggregator.GetVersionAggregator().ReportVersion( "Visual Studio Install Path", install_path, version_aggregator.VersionTypes.INFO) version_aggregator.GetVersionAggregator().ReportVersion( "VC Version", vc_ver, version_aggregator.VersionTypes.TOOL) clang_host_bin_prefix = Path(install_path, "VC", "Tools", "MSVC", vc_ver, "bin", f"Host{HostType}", HostType) if not clang_host_bin_prefix.exists(): self.Logger.error("Path for VS toolchain is invalid") return -2 # The environment is using nmake (not make) so add "n" to the end of the path. # The rest of the command is derived from definitions in tools.def. env.set_shell_var("CLANG_HOST_BIN", str(clang_host_bin_prefix / "n")) elif tool_chain_tag == "CLANGDWARF": clang_host_bin = self._configure_mingw32_host("CLANGDWARF") if clang_host_bin is None: return -1 clang_bin_str = env.get_shell_var("CLANG_BIN") if clang_bin_str: self.Logger.debug("CLANG_BIN is already set.") else: clang_bin_str = "c:\\edk2-clang\\bin\\" if not (Path(clang_bin_str) / (clang_host_bin + "make.exe")).exists(): self.Logger.error(f"mingw32 toolchain not found in CLANG_BIN path {clang_bin_str}") return -2 env.set_shell_var("CLANG_BIN", clang_bin_str) ClangBin = self._resolve_clang_bin(HostInfo) if ClangBin is None: return 1 clang_bin_parent = ClangBin.parent version_aggregator.GetVersionAggregator().ReportVersion( "CLANG_BIN", str(ClangBin), version_aggregator.VersionTypes.INFO, ) clang_version = self._get_clang_version(clang_bin_parent) if clang_version: version_aggregator.GetVersionAggregator().ReportVersion( "CLANG", clang_version, version_aggregator.VersionTypes.TOOL, ) else: self.Logger.warning("Could not determine clang version") # Report the linker version based on toolchain linker_name = {"CLANGPDB": "lld-link", "CLANGDWARF": "lld"}.get(tool_chain_tag) if linker_name: lld_version = self._get_linker_version(clang_bin_parent, linker_name) if lld_version: version_aggregator.GetVersionAggregator().ReportVersion( linker_name, lld_version, version_aggregator.VersionTypes.TOOL, ) else: self.Logger.warning(f"Could not determine {linker_name} version") return 0 def _resolve_clang_bin(self, host_info): """Locate the clang executable, trying in order: 1. CLANG_BIN environment variable (validated if set) 2. System PATH 3. Default LLVM install path (Windows) 4. VS-installed clang (Windows only) Sets CLANG_BIN in the environment when resolved via steps 2-4. Returns a Path to the clang binary, or None if not found (errors are logged). """ env = shell_environment.GetEnvironment() clang_exe = "clang.exe" if host_info.os == "Windows" else "clang" clang_bin_env = env.get_shell_var("CLANG_BIN") # 1. Check CLANG_BIN from the environment ClangBin = Path(clang_bin_env, clang_exe) if clang_bin_env else None if ClangBin and not ClangBin.is_file(): self.Logger.error( "CLANG_BIN is set to '%s' but '%s' was not found.", clang_bin_env, ClangBin, ) return None # 2. CLANG_BIN was not set; check system path. if not ClangBin: ClangBin = Path(w).resolve() if (w := shutil.which("clang")) else None # 3. Check Windows LLVM install path. if not ClangBin and host_info.os == "Windows": ClangBin = Path("C:/Program Files/LLVM/bin/clang.exe") # 4. Finally try MSVC LLVM install. if (not ClangBin or not ClangBin.is_file()) and host_info.os == "Windows": vs_clang_path = self._configure_windows_vs_host_for_clang(host_info) ClangBin = vs_clang_path if vs_clang_path and vs_clang_path.is_file() else None if not (ClangBin and ClangBin.is_file()): self.Logger.error("Could not find clang executable.") return None # If CLANG_BIN was not originally set, write it back from the discovered path. if not clang_bin_env: env.set_shell_var("CLANG_BIN", ClangBin.parent.as_posix() + "/") return ClangBin def _configure_mingw32_host(self, tag): """Validate BASETOOLS_MINGW_BUILD and resolve CLANG_HOST_BIN for a mingw32 environment. Returns the CLANG_HOST_BIN prefix string, or None on validation error. """ env = shell_environment.GetEnvironment() basetools_mingw_build = env.get_shell_var("BASETOOLS_MINGW_BUILD") or 'TRUE' if basetools_mingw_build.upper() != 'TRUE': self.Logger.error(f"BASETOOLS_MINGW_BUILD must be set to TRUE for {tag} toolchain.") return None env.set_shell_var("BASETOOLS_MINGW_BUILD", basetools_mingw_build) clang_host_bin = env.get_shell_var("CLANG_HOST_BIN") if clang_host_bin: self.Logger.debug("CLANG_HOST_BIN is already set.") else: # The environment is mingw32 make; add "mingw32-" prefix. # The rest of the command is derived from definitions in tools.def. clang_host_bin = 'mingw32-' env.set_shell_var("CLANG_HOST_BIN", clang_host_bin) return clang_host_bin def _apply_vs_vars(self, host_info): """Resolve the VS host type, query VS env variables, and apply them to the shell environment. Returns the resolved host type string (e.g. 'x86', 'x64'), or None if unsupported. """ env = shell_environment.GetEnvironment() host_type = env.get_shell_var("CLANG_VS_HOST") if host_type: host_type = host_type.lower() self.Logger.info(f"CLANG_VS_HOST defined by environment. Value is {host_type}") elif host_info.arch == "x86": host_type = "x86" if host_info.bit == "32" else "x64" if host_info.bit == "64" else None if not host_type: return None # CLANG_VS_HOST options are not exactly the same as QueryVcVariables. This translates. vc_host_arch_translator = {"x86": "x86", "x64": "AMD64", "arm64": "not supported"} vs_vars = locate_tools.QueryVcVariables(_VS_INTERESTING_KEYS, vc_host_arch_translator[host_type]) for k, v in vs_vars.items(): env.set_shell_var(k, v) return host_type def _configure_windows_vs_host_for_clang(self, host_info): """Configure the VS host environment and locate the VS-bundled clang. Sets CLANG_HOST_BIN and relevant VS shell variables, then searches the VS LLVM install tree for clang.exe. Args: host_info: HostInfo object from GetHostInfo(). Returns: Path to clang.exe if found, otherwise None. """ host_type = self._apply_vs_vars(host_info) if not host_type: self.Logger.error("VS host type could not be determined; unsupported host architecture.") return None env = shell_environment.GetEnvironment() # If environment already has CLANG_HOST_BIN set then user has already # set the path to the VS tools like nmake.exe. if env.get_shell_var("CLANG_HOST_BIN") is not None: self.Logger.debug("CLANG_HOST_BIN is already set.") clang_bin = env.get_shell_var("CLANG_BIN") if clang_bin: clang_path = Path(clang_bin) / "clang.exe" return clang_path if clang_path.is_file() else None return None install_path = self._get_vs_install_path(None, None) vc_ver = self._get_vc_version(install_path, None) if install_path is None or vc_ver is None: self.Logger.error("Failed to configure environment for VS") return None version_aggregator.GetVersionAggregator().ReportVersion( "Visual Studio Install Path", install_path, version_aggregator.VersionTypes.INFO, ) version_aggregator.GetVersionAggregator().ReportVersion( "VC Version", vc_ver, version_aggregator.VersionTypes.TOOL ) clang_host_bin_prefix = Path(install_path, "VC", "Tools", "MSVC", vc_ver, "bin", f"Host{host_type}", host_type) self.Logger.info("VS host tools path: %s", clang_host_bin_prefix) # now confirm it exists if not clang_host_bin_prefix.exists(): self.Logger.error("Path for VS toolchain is invalid") return None # The environment is using nmake (not make) so add "n" to the end # of the path. The rest of the command is derived from definitions # in tools.def. shell_environment.GetEnvironment().set_shell_var( "CLANG_HOST_BIN", str(clang_host_bin_prefix / "n") ) # VS 2019+ installs 64-bit clang under Llvm\x64\bin; older/32-bit # installs use Llvm\bin. Try the arch-specific path first. llvm_base = Path(install_path, "VC", "Tools", "Llvm") self.Logger.info("Searching for clang under: %s", llvm_base) for candidate in ( Path(llvm_base, host_type, "bin", "clang.exe"), Path(llvm_base, "bin", "clang.exe"), ): self.Logger.debug("Checking candidate: %s", candidate) if candidate.is_file(): self.Logger.info("Selected VS clang path: %s", candidate) return candidate self.Logger.error( "Could not find clang.exe under %s", llvm_base ) return None def _get_vs_install_path(self, vs_version, varname): """Return the Visual Studio install path. Checks the shell variable *varname* first; falls back to vswhere. Args: vs_version: VS version string passed to FindWithVsWhere (e.g. "vs2022"). varname: Shell variable name to check first, or None. Returns: Install path string, or None if not found. """ # check if already specified path = None if varname is not None: path = shell_environment.GetEnvironment().get_shell_var(varname) if path is None: # Not specified...find latest try: path = FindWithVsWhere(vs_version=vs_version) except (OSError, ValueError, RuntimeError) as e: self.Logger.error(str(e)) return None if path is not None and Path(path).exists(): self.Logger.debug("Found VS instance for %s", vs_version) else: self.Logger.error( "VsWhere successfully executed, but could not find " f"VS instance for {vs_version}." ) return path def _get_vc_version(self, path, varname): """Return the VC tools version string. Checks the shell variable *varname* first; falls back to scanning the MSVC tools directory under *path*. Args: path: VS install path string, or None. varname: Shell variable name to check first, or None. Returns: VC tools version string (e.g. "14.38.33130"), or None if not found. """ # check if already specified vc_ver = None if varname is not None: vc_ver = shell_environment.GetEnvironment().get_shell_var(varname) if path is None: self.Logger.critical( "Failed to find Visual Studio tools. " " Might need to check for VS install" ) return vc_ver if vc_ver is None: # Not specified...find latest vc_tools_path = Path(path) / "VC" / "Tools" / "MSVC" if not vc_tools_path.is_dir(): self.Logger.critical( "Failed to find VC tools. " " Might need to check for VS install" ) return vc_ver dirs = [ entry.name for entry in vc_tools_path.iterdir() if entry.is_dir() ] if not dirs: self.Logger.critical( "Failed to find VC tools version directories. " " Might need to check for VS install" ) return vc_ver if len(dirs) > 1: logging.warning( f"Multiple VC versions found: [{', '.join(dirs)}]." f" Using {dirs[-1]}" ) vc_ver = dirs[-1].strip() # get last in list self.Logger.debug("Found VC Tool version is %s", vc_ver) return vc_ver def _get_linker_version(self, clang_bin_path, linker_name): """Return the version string for an LLD-based linker. Runs `` --version`` and parses the ``LLD X.Y.Z`` prefix. Handles lld-link returning a non-zero exit code for ``--version``. Args: clang_bin_path: Directory containing the linker binary. linker_name: Binary name without extension (``"lld"`` or ``"lld-link"``). Returns: Version string (e.g. ``"18.1.8"``), or None if parsing fails. """ return_buffer = StringIO() linker_path = Path(clang_bin_path) / linker_name if not linker_path.exists(): linker_path = linker_path.with_suffix(".exe") ret = RunCmd(str(linker_path), "--version", outstream=return_buffer) # lld-link --version may return non-zero; treat output as valid if present output = return_buffer.getvalue() lines = output.splitlines() if not lines: self.Logger.warning("%s --version produced no output (exit %d)", linker_name, ret) return None line = lines[0].strip() # Output is typically "LLD X.Y.Z (compatible with GNU linkers)" etc. marker = "LLD " idx = line.find(marker) if idx >= 0: # Take everything up to the first space or paren after the version version_str = line[idx + len(marker):].split()[0].rstrip(",)") return version_str self.Logger.warning("Unexpected %s --version format: %r", linker_name, line) return line def _get_clang_version(self, clang_bin_path): """Return the clang version string. Runs ``clang --version`` and parses the ``clang version X.Y.Z`` line. Handles both upstream (``clang version X.Y.Z``) and distro-prefixed (``Ubuntu clang version X.Y.Z``) output formats. Args: clang_bin_path: Directory containing the clang binary. Returns: Version string (e.g. ``"18.1.8"``), or None if parsing fails. """ return_buffer = StringIO() clang_path = Path(clang_bin_path) / "clang" if not clang_path.exists(): clang_path = clang_path.with_suffix(".exe") ret = RunCmd(str(clang_path), "--version", outstream=return_buffer) if ret != 0: self.Logger.warning("clang --version exited with code %d", ret) return None output = return_buffer.getvalue() lines = output.splitlines() if not lines: self.Logger.warning("clang --version produced no output") return None line = lines[0].strip() # Output is typically "clang version X.Y.Z" or "Ubuntu clang version X.Y.Z" marker = "clang version " idx = line.find(marker) if idx >= 0: return line[idx + len(marker):].strip() self.Logger.warning("Unexpected clang --version format: %r", line) return line