1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
|
# @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 ``<linker> --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
|