clang-tool-chain 1.0.44__py3-none-any.whl → 1.0.47__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,9 +14,12 @@ The Chain of Responsibility pattern allows:
14
14
 
15
15
  Architecture:
16
16
  ArgumentTransformer (ABC)
17
- ├── MacOSSDKTransformer (priority=100)
18
17
  ├── DirectivesTransformer (priority=50)
18
+ ├── MacOSSDKTransformer (priority=100)
19
+ ├── LinuxUnwindTransformer (priority=150)
19
20
  ├── LLDLinkerTransformer (priority=200)
21
+ ├── ASANRuntimeTransformer (priority=250)
22
+ ├── RPathTransformer (priority=275)
20
23
  ├── GNUABITransformer (priority=300)
21
24
  └── MSVCABITransformer (priority=300)
22
25
 
@@ -35,6 +38,8 @@ from dataclasses import dataclass
35
38
  from pathlib import Path
36
39
  from typing import TYPE_CHECKING
37
40
 
41
+ from clang_tool_chain.env_utils import is_feature_disabled
42
+
38
43
  if TYPE_CHECKING:
39
44
  from clang_tool_chain.directives.parser import ParsedDirectives
40
45
 
@@ -111,6 +116,7 @@ class DirectivesTransformer(ArgumentTransformer):
111
116
 
112
117
  Environment Variables:
113
118
  CLANG_TOOL_CHAIN_NO_DIRECTIVES: Set to '1' to disable directive parsing
119
+ CLANG_TOOL_CHAIN_NO_AUTO: Set to '1' to disable all automatic features
114
120
  CLANG_TOOL_CHAIN_DIRECTIVE_VERBOSE: Set to '1' to enable debug logging
115
121
  """
116
122
 
@@ -119,9 +125,8 @@ class DirectivesTransformer(ArgumentTransformer):
119
125
 
120
126
  def transform(self, args: list[str], context: ToolContext) -> list[str]:
121
127
  """Add arguments from inlined build directives in source files."""
122
- # Check if directives are disabled
123
- if os.environ.get("CLANG_TOOL_CHAIN_NO_DIRECTIVES") == "1":
124
- logger.debug("Directives disabled via CLANG_TOOL_CHAIN_NO_DIRECTIVES=1")
128
+ # Check if directives are disabled (via NO_DIRECTIVES or NO_AUTO)
129
+ if is_feature_disabled("DIRECTIVES"):
125
130
  return args
126
131
 
127
132
  # Only apply to clang/clang++ compilation commands
@@ -203,6 +208,86 @@ class MacOSSDKTransformer(ArgumentTransformer):
203
208
  return _add_macos_sysroot_if_needed(args)
204
209
 
205
210
 
211
+ class LinuxUnwindTransformer(ArgumentTransformer):
212
+ """
213
+ Transformer for adding bundled libunwind include/library paths on Linux.
214
+
215
+ Priority: 150 (runs after SDK but before linker)
216
+
217
+ This transformer adds include and library paths for the bundled libunwind
218
+ headers and libraries on Linux. This allows compilation of code that uses
219
+ libunwind without requiring system libunwind-dev to be installed.
220
+
221
+ When libunwind.h exists in the clang toolchain's include directory:
222
+ - Adds -I<clang_root>/include for header discovery
223
+ - Adds -L<clang_root>/lib for library discovery
224
+ - Adds -Wl,-rpath,<clang_root>/lib for runtime library discovery
225
+
226
+ Environment Variables:
227
+ CLANG_TOOL_CHAIN_NO_BUNDLED_UNWIND: Set to '1' to disable bundled libunwind
228
+ CLANG_TOOL_CHAIN_NO_AUTO: Set to '1' to disable all automatic features
229
+ """
230
+
231
+ def priority(self) -> int:
232
+ return 150
233
+
234
+ def transform(self, args: list[str], context: ToolContext) -> list[str]:
235
+ """Add bundled libunwind paths if available on Linux."""
236
+ # Only applies to Linux clang/clang++
237
+ if context.platform_name != "linux" or context.tool_name not in ("clang", "clang++"):
238
+ return args
239
+
240
+ # Check if disabled (via NO_BUNDLED_UNWIND or NO_AUTO)
241
+ if is_feature_disabled("BUNDLED_UNWIND"):
242
+ return args
243
+
244
+ # Check if compile-only (no linking)
245
+ is_compile_only = "-c" in args
246
+
247
+ try:
248
+ from clang_tool_chain.platform.detection import get_platform_binary_dir
249
+
250
+ clang_bin = get_platform_binary_dir()
251
+ clang_root = clang_bin.parent
252
+
253
+ # Check if bundled libunwind.h exists
254
+ libunwind_header = clang_root / "include" / "libunwind.h"
255
+ if not libunwind_header.exists():
256
+ logger.debug("Bundled libunwind.h not found, skipping LinuxUnwindTransformer")
257
+ return args
258
+
259
+ result = list(args)
260
+ include_dir = clang_root / "include"
261
+ lib_dir = clang_root / "lib"
262
+
263
+ # Add include path (always needed for compilation)
264
+ include_flag = f"-I{include_dir}"
265
+ if include_flag not in args:
266
+ result = [include_flag] + result
267
+ logger.debug(f"Adding bundled libunwind include path: {include_flag}")
268
+
269
+ # Add library path and rpath (only for linking)
270
+ if not is_compile_only:
271
+ lib_flag = f"-L{lib_dir}"
272
+ if lib_flag not in args:
273
+ result = [lib_flag] + result
274
+ logger.debug(f"Adding bundled libunwind library path: {lib_flag}")
275
+
276
+ # Add rpath so runtime can find libunwind.so
277
+ rpath_flag = f"-Wl,-rpath,{lib_dir}"
278
+ # Check if any rpath to our lib dir already exists
279
+ has_our_rpath = any(str(lib_dir) in arg and "-rpath" in arg for arg in args)
280
+ if not has_our_rpath:
281
+ result = [rpath_flag] + result
282
+ logger.debug(f"Adding bundled libunwind rpath: {rpath_flag}")
283
+
284
+ return result
285
+
286
+ except Exception as e:
287
+ logger.debug(f"LinuxUnwindTransformer error: {e}")
288
+ return args
289
+
290
+
206
291
  class LLDLinkerTransformer(ArgumentTransformer):
207
292
  """
208
293
  Transformer for forcing LLVM's lld linker on macOS and Linux.
@@ -291,32 +376,38 @@ class ASANRuntimeTransformer(ArgumentTransformer):
291
376
 
292
377
  Priority: 250 (runs after linker but before ABI)
293
378
 
294
- This transformer ensures proper ASAN runtime linking on Linux:
379
+ This transformer ensures proper ASAN runtime linking on Linux and Windows:
295
380
  - Detects -fsanitize=address flag
296
381
  - Adds -shared-libasan to use shared runtime library
297
- - Adds -Wl,--allow-shlib-undefined when building shared libraries with ASAN
382
+ - Adds -Wl,--allow-shlib-undefined when building shared libraries with ASAN (Linux only)
298
383
  - Prevents undefined symbol errors during linking
299
384
 
300
- The shared runtime library (libclang_rt.asan.so) contains the full
301
- ASAN implementation, while the static wrapper library only contains stubs.
385
+ The shared runtime library (libclang_rt.asan.so on Linux, libclang_rt.asan_dynamic.dll
386
+ on Windows) contains the full ASAN implementation, while the static wrapper library
387
+ only contains stubs.
302
388
 
303
389
  When building shared libraries with sanitizers, the library may have undefined
304
390
  symbols that will be provided by the sanitizer runtime when loaded. LLD by
305
391
  default enforces no undefined symbols, so we need to allow them explicitly.
306
392
 
393
+ Note: macOS uses a different ASAN runtime mechanism and is not affected.
394
+
307
395
  Environment Variables:
308
396
  CLANG_TOOL_CHAIN_NO_SHARED_ASAN: Set to '1' to disable shared ASAN
397
+ CLANG_TOOL_CHAIN_NO_SANITIZER_NOTE: Set to '1' to suppress the injection note
398
+ CLANG_TOOL_CHAIN_NO_AUTO: Set to '1' to disable all automatic features
309
399
  """
310
400
 
311
401
  def priority(self) -> int:
312
402
  return 250
313
403
 
314
404
  def transform(self, args: list[str], context: ToolContext) -> list[str]:
315
- """Add -shared-libasan and --allow-shlib-undefined when using ASAN on Linux."""
405
+ """Add -shared-libasan and --allow-shlib-undefined when using ASAN on Linux/Windows."""
316
406
  import sys
317
407
 
318
- # Only applies to Linux clang/clang++
319
- if context.platform_name != "linux" or context.tool_name not in ("clang", "clang++"):
408
+ # Only applies to Linux and Windows (GNU ABI) clang/clang++
409
+ # macOS uses a different ASAN runtime mechanism
410
+ if context.platform_name not in ("linux", "win") or context.tool_name not in ("clang", "clang++"):
320
411
  return args
321
412
 
322
413
  # Check if ASAN is enabled
@@ -327,23 +418,21 @@ class ASANRuntimeTransformer(ArgumentTransformer):
327
418
  result = list(args)
328
419
  injected_flags = []
329
420
 
330
- # Check if user disabled shared ASAN
331
- if os.environ.get("CLANG_TOOL_CHAIN_NO_SHARED_ASAN") != "1":
332
- # Check if -shared-libasan already present
333
- if "-shared-libasan" not in args:
334
- # Add -shared-libasan to use shared runtime library
335
- # This prevents undefined symbol errors during linking
336
- logger.info("Adding -shared-libasan for ASAN runtime linking on Linux")
337
- result = ["-shared-libasan"] + result
338
- injected_flags.append("-shared-libasan")
339
- else:
340
- logger.debug("Shared ASAN disabled via CLANG_TOOL_CHAIN_NO_SHARED_ASAN=1")
341
-
342
- # Check if building a shared library with ASAN
421
+ # Check if user disabled shared ASAN (via NO_SHARED_ASAN or NO_AUTO)
422
+ # Also check if -shared-libasan already present
423
+ if not is_feature_disabled("SHARED_ASAN") and "-shared-libasan" not in args:
424
+ # Add -shared-libasan to use shared runtime library
425
+ # This prevents undefined symbol errors during linking
426
+ logger.info("Adding -shared-libasan for ASAN runtime linking on Linux")
427
+ result = ["-shared-libasan"] + result
428
+ injected_flags.append("-shared-libasan")
429
+
430
+ # Check if building a shared library with ASAN on Linux
343
431
  # Shared libraries need to allow undefined symbols that will be provided
344
432
  # by the sanitizer runtime when the runner loads them
433
+ # Note: --allow-shlib-undefined is a Linux ELF linker flag, not supported on Windows
345
434
  is_shared_lib = "-shared" in args
346
- if is_shared_lib:
435
+ if is_shared_lib and context.platform_name == "linux":
347
436
  # Check if --allow-shlib-undefined already present
348
437
  has_allow_shlib_undefined = any("--allow-shlib-undefined" in arg for arg in args)
349
438
  if not has_allow_shlib_undefined:
@@ -351,10 +440,11 @@ class ASANRuntimeTransformer(ArgumentTransformer):
351
440
  result = ["-Wl,--allow-shlib-undefined"] + result
352
441
  injected_flags.append("-Wl,--allow-shlib-undefined")
353
442
 
354
- # Warn on stderr if we injected flags
355
- if injected_flags:
443
+ # Warn on stderr if we injected flags (unless disabled)
444
+ if injected_flags and not is_feature_disabled("SANITIZER_NOTE"):
356
445
  print(
357
- f"clang-tool-chain: note: automatically injected sanitizer flags: {' '.join(injected_flags)}",
446
+ f"clang-tool-chain: note: automatically injected sanitizer flags: {' '.join(injected_flags)} "
447
+ "(disable with CLANG_TOOL_CHAIN_NO_SANITIZER_NOTE=1)",
358
448
  file=sys.stderr,
359
449
  )
360
450
 
@@ -378,6 +468,7 @@ class RPathTransformer(ArgumentTransformer):
378
468
 
379
469
  Environment Variables:
380
470
  CLANG_TOOL_CHAIN_NO_RPATH: Set to '1' to disable automatic rpath injection
471
+ CLANG_TOOL_CHAIN_NO_AUTO: Set to '1' to disable all automatic features
381
472
  """
382
473
 
383
474
  def priority(self) -> int:
@@ -401,9 +492,8 @@ class RPathTransformer(ArgumentTransformer):
401
492
  if not has_deploy_flag and not deploy_from_env:
402
493
  return args
403
494
 
404
- # Check if user disabled rpath
405
- if os.environ.get("CLANG_TOOL_CHAIN_NO_RPATH") == "1":
406
- logger.debug("Rpath injection disabled via CLANG_TOOL_CHAIN_NO_RPATH=1")
495
+ # Check if user disabled rpath (via NO_RPATH or NO_AUTO)
496
+ if is_feature_disabled("RPATH"):
407
497
  return args
408
498
 
409
499
  # Check if rpath already present
@@ -528,11 +618,12 @@ def create_default_pipeline() -> ArgumentPipeline:
528
618
  This includes all standard transformers in their default priority order:
529
619
  1. DirectivesTransformer (priority=50)
530
620
  2. MacOSSDKTransformer (priority=100)
531
- 3. LLDLinkerTransformer (priority=200)
532
- 4. ASANRuntimeTransformer (priority=250)
533
- 5. RPathTransformer (priority=275)
534
- 6. GNUABITransformer (priority=300)
535
- 7. MSVCABITransformer (priority=300)
621
+ 3. LinuxUnwindTransformer (priority=150)
622
+ 4. LLDLinkerTransformer (priority=200)
623
+ 5. ASANRuntimeTransformer (priority=250)
624
+ 6. RPathTransformer (priority=275)
625
+ 7. GNUABITransformer (priority=300)
626
+ 8. MSVCABITransformer (priority=300)
536
627
 
537
628
  Returns:
538
629
  Configured ArgumentPipeline ready for use
@@ -541,6 +632,7 @@ def create_default_pipeline() -> ArgumentPipeline:
541
632
  [
542
633
  DirectivesTransformer(),
543
634
  MacOSSDKTransformer(),
635
+ LinuxUnwindTransformer(),
544
636
  LLDLinkerTransformer(),
545
637
  ASANRuntimeTransformer(),
546
638
  RPathTransformer(),
@@ -26,6 +26,7 @@ from typing import NoReturn
26
26
 
27
27
  from clang_tool_chain.cli_parsers import parse_build_args, parse_build_run_args
28
28
  from clang_tool_chain.directives import DirectiveParser
29
+ from clang_tool_chain.env_utils import is_feature_disabled
29
30
  from clang_tool_chain.execution.core import execute_tool
30
31
  from clang_tool_chain.interrupt_utils import handle_keyboard_interrupt_properly
31
32
  from clang_tool_chain.platform import get_platform_info
@@ -47,9 +48,13 @@ def _get_directive_args(source_path: Path) -> list[str]:
47
48
 
48
49
  Returns:
49
50
  List of compiler/linker arguments derived from directives
51
+
52
+ Environment Variables:
53
+ CLANG_TOOL_CHAIN_NO_DIRECTIVES: Set to '1' to disable directive parsing
54
+ CLANG_TOOL_CHAIN_NO_AUTO: Set to '1' to disable all automatic features
50
55
  """
51
- # Check if directives parsing is disabled via environment variable
52
- if os.environ.get("CLANG_TOOL_CHAIN_NO_DIRECTIVES", "").lower() in ("1", "true", "yes"):
56
+ # Check if directives parsing is disabled (via NO_DIRECTIVES or NO_AUTO)
57
+ if is_feature_disabled("DIRECTIVES"):
53
58
  return []
54
59
 
55
60
  try:
@@ -106,10 +111,11 @@ def get_directive_args_from_compiler_args(args: list[str]) -> list[str]:
106
111
 
107
112
  Environment Variables:
108
113
  CLANG_TOOL_CHAIN_NO_DIRECTIVES: Set to '1' to disable directive parsing
114
+ CLANG_TOOL_CHAIN_NO_AUTO: Set to '1' to disable all automatic features
109
115
  CLANG_TOOL_CHAIN_DIRECTIVE_VERBOSE: Set to '1' to show parsed directives
110
116
  """
111
- # Check if directives parsing is disabled via environment variable
112
- if os.environ.get("CLANG_TOOL_CHAIN_NO_DIRECTIVES", "").lower() in ("1", "true", "yes"):
117
+ # Check if directives parsing is disabled (via NO_DIRECTIVES or NO_AUTO)
118
+ if is_feature_disabled("DIRECTIVES"):
113
119
  return []
114
120
 
115
121
  directive_args: list[str] = []
@@ -24,6 +24,7 @@ from pathlib import Path
24
24
  from typing import NoReturn
25
25
 
26
26
  from clang_tool_chain.directives import DirectiveParser
27
+ from clang_tool_chain.env_utils import is_feature_disabled
27
28
  from clang_tool_chain.execution.core import run_tool
28
29
  from clang_tool_chain.interrupt_utils import handle_keyboard_interrupt_properly
29
30
 
@@ -109,9 +110,13 @@ def _get_directive_args(source_path: Path) -> list[str]:
109
110
 
110
111
  Returns:
111
112
  List of compiler/linker arguments derived from directives
113
+
114
+ Environment Variables:
115
+ CLANG_TOOL_CHAIN_NO_DIRECTIVES: Set to '1' to disable directive parsing
116
+ CLANG_TOOL_CHAIN_NO_AUTO: Set to '1' to disable all automatic features
112
117
  """
113
- # Check if directives parsing is disabled via environment variable
114
- if os.environ.get("CLANG_TOOL_CHAIN_NO_DIRECTIVES", "").lower() in ("1", "true", "yes"):
118
+ # Check if directives parsing is disabled (via NO_DIRECTIVES or NO_AUTO)
119
+ if is_feature_disabled("DIRECTIVES"):
115
120
  return []
116
121
 
117
122
  try:
@@ -1,16 +1,23 @@
1
1
  """
2
2
  Sanitizer runtime environment configuration.
3
3
 
4
- This module provides automatic injection of ASAN_OPTIONS and LSAN_OPTIONS
5
- environment variables to improve stack trace quality when running executables
6
- compiled with Address Sanitizer or Leak Sanitizer.
4
+ This module provides automatic injection of ASAN_OPTIONS, LSAN_OPTIONS, and
5
+ ASAN_SYMBOLIZER_PATH environment variables to improve stack trace quality
6
+ when running executables compiled with Address Sanitizer or Leak Sanitizer.
7
7
 
8
8
  The default options fix <unknown module> entries in stack traces from
9
9
  dlopen()'d shared libraries by enabling slow unwinding and symbolization.
10
+
11
+ The symbolizer path is automatically detected from the clang-tool-chain
12
+ installation, enabling proper address-to-symbol resolution without manual
13
+ configuration.
10
14
  """
11
15
 
12
16
  import logging
13
17
  import os
18
+ import shutil
19
+
20
+ from clang_tool_chain.env_utils import is_feature_disabled
14
21
 
15
22
  logger = logging.getLogger(__name__)
16
23
 
@@ -22,6 +29,46 @@ DEFAULT_ASAN_OPTIONS = "fast_unwind_on_malloc=0:symbolize=1:detect_leaks=1"
22
29
  DEFAULT_LSAN_OPTIONS = "fast_unwind_on_malloc=0:symbolize=1"
23
30
 
24
31
 
32
+ def get_symbolizer_path() -> str | None:
33
+ """
34
+ Get the path to llvm-symbolizer from the clang-tool-chain installation.
35
+
36
+ This function finds the llvm-symbolizer binary bundled with clang-tool-chain,
37
+ which is required by ASAN/LSAN to convert memory addresses into function names
38
+ and source locations in stack traces.
39
+
40
+ Returns:
41
+ Absolute path to llvm-symbolizer, or None if not found.
42
+
43
+ Example:
44
+ >>> path = get_symbolizer_path()
45
+ >>> if path:
46
+ ... os.environ["ASAN_SYMBOLIZER_PATH"] = path
47
+
48
+ Note:
49
+ Falls back to system PATH if the clang-tool-chain binary is not available.
50
+ This allows the function to work even when clang-tool-chain is not fully
51
+ installed (e.g., during development or in CI environments).
52
+ """
53
+ # Try to find llvm-symbolizer from clang-tool-chain installation
54
+ try:
55
+ from clang_tool_chain.platform.paths import find_tool_binary
56
+
57
+ symbolizer = find_tool_binary("llvm-symbolizer")
58
+ return str(symbolizer)
59
+ except (ImportError, RuntimeError) as e:
60
+ logger.debug(f"Could not find llvm-symbolizer in clang-tool-chain: {e}")
61
+
62
+ # Fall back to system PATH
63
+ system_symbolizer = shutil.which("llvm-symbolizer")
64
+ if system_symbolizer:
65
+ logger.debug(f"Using system llvm-symbolizer: {system_symbolizer}")
66
+ return system_symbolizer
67
+
68
+ logger.debug("llvm-symbolizer not found in clang-tool-chain or system PATH")
69
+ return None
70
+
71
+
25
72
  def detect_sanitizers_from_flags(compiler_flags: list[str]) -> tuple[bool, bool]:
26
73
  """
27
74
  Detect which sanitizers are enabled from compiler flags.
@@ -64,12 +111,16 @@ def prepare_sanitizer_environment(
64
111
  compiler_flags: list[str] | None = None,
65
112
  ) -> dict[str, str]:
66
113
  """
67
- Prepare environment with optimal sanitizer options.
114
+ Prepare environment with optimal sanitizer options and symbolizer path.
68
115
 
69
- This function injects ASAN_OPTIONS and/or LSAN_OPTIONS environment variables
70
- if they are not already set by the user AND the corresponding sanitizer was
71
- enabled during compilation. The injected options improve stack trace quality
72
- for executables using dlopen()'d shared libraries.
116
+ This function injects ASAN_OPTIONS, LSAN_OPTIONS, and ASAN_SYMBOLIZER_PATH
117
+ environment variables if they are not already set by the user AND the
118
+ corresponding sanitizer was enabled during compilation. The injected options
119
+ improve stack trace quality for executables using dlopen()'d shared libraries.
120
+
121
+ The ASAN_SYMBOLIZER_PATH is automatically detected from the clang-tool-chain
122
+ installation, enabling proper address-to-symbol resolution (function names,
123
+ file paths, line numbers) without manual configuration.
73
124
 
74
125
  Args:
75
126
  base_env: Base environment dictionary to modify. If None, uses os.environ.
@@ -83,20 +134,21 @@ def prepare_sanitizer_environment(
83
134
  Environment Variables:
84
135
  CLANG_TOOL_CHAIN_NO_SANITIZER_ENV: Set to "1", "true", or "yes" to
85
136
  disable automatic injection of sanitizer options.
137
+ CLANG_TOOL_CHAIN_NO_AUTO: Set to "1" to disable all automatic features.
86
138
  ASAN_OPTIONS: If already set, preserved as-is (user config takes priority).
87
139
  LSAN_OPTIONS: If already set, preserved as-is (user config takes priority).
140
+ ASAN_SYMBOLIZER_PATH: If already set, preserved as-is (user config takes priority).
88
141
 
89
142
  Example:
90
143
  >>> env = prepare_sanitizer_environment(compiler_flags=["-fsanitize=address"])
91
- >>> # env now contains ASAN_OPTIONS and LSAN_OPTIONS
144
+ >>> # env now contains ASAN_OPTIONS, LSAN_OPTIONS, and ASAN_SYMBOLIZER_PATH
92
145
  >>> env = prepare_sanitizer_environment(compiler_flags=["-O2"])
93
146
  >>> # env unchanged - no sanitizers enabled
94
147
  """
95
148
  env = base_env.copy() if base_env is not None else os.environ.copy()
96
149
 
97
- # Check if disabled via environment variable
98
- if os.environ.get("CLANG_TOOL_CHAIN_NO_SANITIZER_ENV", "").lower() in ("1", "true", "yes"):
99
- logger.debug("Sanitizer environment injection disabled via CLANG_TOOL_CHAIN_NO_SANITIZER_ENV")
150
+ # Check if disabled via environment variable (NO_SANITIZER_ENV or NO_AUTO)
151
+ if is_feature_disabled("SANITIZER_ENV"):
100
152
  return env
101
153
 
102
154
  # If no compiler flags provided, don't inject anything (safe default)
@@ -117,4 +169,17 @@ def prepare_sanitizer_environment(
117
169
  env["LSAN_OPTIONS"] = DEFAULT_LSAN_OPTIONS
118
170
  logger.info(f"Injecting LSAN_OPTIONS={DEFAULT_LSAN_OPTIONS}")
119
171
 
172
+ # Inject ASAN_SYMBOLIZER_PATH if any sanitizer is enabled and not already set
173
+ if (asan_enabled or lsan_enabled) and "ASAN_SYMBOLIZER_PATH" not in env:
174
+ symbolizer_path = get_symbolizer_path()
175
+ if symbolizer_path:
176
+ env["ASAN_SYMBOLIZER_PATH"] = symbolizer_path
177
+ logger.info(f"Injecting ASAN_SYMBOLIZER_PATH={symbolizer_path}")
178
+ else:
179
+ logger.warning(
180
+ "llvm-symbolizer not found - ASAN/LSAN stack traces may show "
181
+ "raw addresses instead of function names. Install llvm-symbolizer "
182
+ "or ensure clang-tool-chain is properly installed."
183
+ )
184
+
120
185
  return env
@@ -12,6 +12,7 @@ import logging
12
12
  import os
13
13
  import sys
14
14
 
15
+ from clang_tool_chain.env_utils import is_feature_disabled
15
16
  from clang_tool_chain.interrupt_utils import handle_keyboard_interrupt_properly
16
17
  from clang_tool_chain.llvm_versions import get_llvm_version_tuple, supports_ld64_lld_flag
17
18
 
@@ -187,13 +188,16 @@ def _translate_linker_flags_for_macos_lld(args: list[str]) -> list[str]:
187
188
  Translate GNU ld linker flags to ld64.lld equivalents for macOS.
188
189
 
189
190
  When using lld on macOS (ld64.lld), certain GNU ld flags need to be
190
- translated to their Mach-O equivalents:
191
+ translated to their Mach-O equivalents or removed:
191
192
  - --no-undefined -> -undefined error
192
193
  - --fatal-warnings -> -fatal_warnings
193
- - More translations can be added as needed
194
+ - --allow-shlib-undefined -> (removed, ld64 allows undefined symbols by default)
194
195
 
195
196
  This function processes both direct linker flags and flags passed via -Wl,
196
197
 
198
+ A warning is printed to stderr when flags are removed, unless silenced via
199
+ CLANG_TOOL_CHAIN_NO_LINKER_COMPAT_NOTE=1.
200
+
197
201
  Args:
198
202
  args: Original compiler arguments
199
203
 
@@ -201,12 +205,19 @@ def _translate_linker_flags_for_macos_lld(args: list[str]) -> list[str]:
201
205
  Modified arguments with translated linker flags
202
206
  """
203
207
  # Map of GNU ld flags to ld64.lld equivalents
204
- flag_translations = {
208
+ # None means the flag should be removed (no equivalent needed)
209
+ flag_translations: dict[str, str | None] = {
205
210
  "--no-undefined": "-undefined error",
206
211
  "--fatal-warnings": "-fatal_warnings",
212
+ # --allow-shlib-undefined: ld64 allows undefined symbols in dylibs by default,
213
+ # so this flag is a no-op on macOS. We remove it entirely.
214
+ "--allow-shlib-undefined": None,
207
215
  # Add more translations as needed
208
216
  }
209
217
 
218
+ # Track removed flags for warning
219
+ removed_flags: list[str] = []
220
+
210
221
  result = []
211
222
  i = 0
212
223
  while i < len(args):
@@ -221,14 +232,18 @@ def _translate_linker_flags_for_macos_lld(args: list[str]) -> list[str]:
221
232
  for flag in linker_flags:
222
233
  # Check if this flag needs translation
223
234
  if flag in flag_translations:
224
- # Translate the flag (may result in multiple flags)
225
235
  translated = flag_translations[flag]
226
- if " " in translated:
236
+ if translated is None:
237
+ # Flag should be removed entirely (no equivalent on macOS)
238
+ removed_flags.append(flag)
239
+ logger.debug(f"Removed linker flag (no macOS equivalent): {flag}")
240
+ elif " " in translated:
227
241
  # Multiple flags (e.g., "-undefined error")
228
242
  translated_flags.extend(translated.split())
243
+ logger.debug(f"Translated linker flag: {flag} -> {translated}")
229
244
  else:
230
245
  translated_flags.append(translated)
231
- logger.debug(f"Translated linker flag: {flag} -> {translated}")
246
+ logger.debug(f"Translated linker flag: {flag} -> {translated}")
232
247
  else:
233
248
  translated_flags.append(flag)
234
249
 
@@ -239,19 +254,34 @@ def _translate_linker_flags_for_macos_lld(args: list[str]) -> list[str]:
239
254
  # Handle standalone linker flags passed directly
240
255
  elif arg in flag_translations:
241
256
  translated = flag_translations[arg]
242
- logger.debug(f"Translated linker flag: {arg} -> {translated}")
243
- # Add via -Wl, to pass to linker
244
- if " " in translated:
245
- # Multiple flags
246
- result.append("-Wl," + ",".join(translated.split()))
257
+ if translated is None:
258
+ # Flag should be removed entirely (no equivalent on macOS)
259
+ removed_flags.append(arg)
260
+ logger.debug(f"Removed linker flag (no macOS equivalent): {arg}")
247
261
  else:
248
- result.append("-Wl," + translated)
262
+ logger.debug(f"Translated linker flag: {arg} -> {translated}")
263
+ # Add via -Wl, to pass to linker
264
+ if " " in translated:
265
+ # Multiple flags
266
+ result.append("-Wl," + ",".join(translated.split()))
267
+ else:
268
+ result.append("-Wl," + translated)
249
269
 
250
270
  else:
251
271
  result.append(arg)
252
272
 
253
273
  i += 1
254
274
 
275
+ # Emit warning for removed flags (unless silenced)
276
+ if removed_flags and not is_feature_disabled("LINKER_COMPAT_NOTE"):
277
+ import sys
278
+
279
+ print(
280
+ f"clang-tool-chain: note: removed GNU linker flags not supported by ld64.lld: "
281
+ f"{', '.join(removed_flags)} (disable with CLANG_TOOL_CHAIN_NO_LINKER_COMPAT_NOTE=1)",
282
+ file=sys.stderr,
283
+ )
284
+
255
285
  return result
256
286
 
257
287
 
@@ -151,6 +151,15 @@ from clang_tool_chain.execution.lldb import (
151
151
  get_lldb_binary_dir,
152
152
  )
153
153
 
154
+ # ============================================================================
155
+ # Sanitizer Environment
156
+ # ============================================================================
157
+ from clang_tool_chain.execution.sanitizer_env import (
158
+ detect_sanitizers_from_flags,
159
+ get_symbolizer_path,
160
+ prepare_sanitizer_environment,
161
+ )
162
+
154
163
  # ============================================================================
155
164
  # Linker Configuration
156
165
  # ============================================================================
@@ -218,6 +227,10 @@ __all__ = [
218
227
  "run_tool",
219
228
  "sccache_clang_main",
220
229
  "sccache_clang_cpp_main",
230
+ # Sanitizer Environment
231
+ "prepare_sanitizer_environment",
232
+ "detect_sanitizers_from_flags",
233
+ "get_symbolizer_path",
221
234
  # Emscripten
222
235
  "ensure_nodejs_available",
223
236
  "execute_emscripten_tool",