clang-tool-chain 1.0.42__py3-none-any.whl → 1.0.44__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.
@@ -1,3 +1,3 @@
1
1
  """Version information for clang-tool-chain."""
2
2
 
3
- __version__ = "1.0.42"
3
+ __version__ = "1.0.44"
@@ -5,10 +5,12 @@ This package provides:
5
5
  - Core tool execution: execute_tool, run_tool
6
6
  - Build utilities: build_main, build_run_main
7
7
  - sccache wrappers for compilation caching
8
+ - Sanitizer environment configuration
8
9
  """
9
10
 
10
11
  from clang_tool_chain.execution.build import build_main, build_run_main
11
12
  from clang_tool_chain.execution.core import execute_tool, run_tool, sccache_clang_cpp_main, sccache_clang_main
13
+ from clang_tool_chain.execution.sanitizer_env import detect_sanitizers_from_flags, prepare_sanitizer_environment
12
14
 
13
15
  __all__ = [
14
16
  # Core execution
@@ -20,4 +22,7 @@ __all__ = [
20
22
  # sccache wrappers
21
23
  "sccache_clang_main",
22
24
  "sccache_clang_cpp_main",
25
+ # Sanitizer utilities
26
+ "prepare_sanitizer_environment",
27
+ "detect_sanitizers_from_flags",
23
28
  ]
@@ -209,9 +209,10 @@ class LLDLinkerTransformer(ArgumentTransformer):
209
209
 
210
210
  Priority: 200 (runs after SDK but before ABI)
211
211
 
212
- This transformer adds platform-specific linker flags:
213
- - macOS: -fuse-ld=ld64.lld (explicit Mach-O linker, LLVM 21.x+)
214
- - Linux: -fuse-ld=lld (standard ELF linker)
212
+ This transformer adds -fuse-ld=lld linker flag. The clang driver
213
+ automatically dispatches to the correct linker binary:
214
+ - macOS: ld64.lld (Mach-O linker)
215
+ - Linux: ld.lld (ELF linker)
215
216
 
216
217
  It also translates GNU ld flags to ld64.lld equivalents on macOS:
217
218
  - --no-undefined -> -undefined error
@@ -293,11 +294,16 @@ class ASANRuntimeTransformer(ArgumentTransformer):
293
294
  This transformer ensures proper ASAN runtime linking on Linux:
294
295
  - Detects -fsanitize=address flag
295
296
  - Adds -shared-libasan to use shared runtime library
297
+ - Adds -Wl,--allow-shlib-undefined when building shared libraries with ASAN
296
298
  - Prevents undefined symbol errors during linking
297
299
 
298
300
  The shared runtime library (libclang_rt.asan.so) contains the full
299
301
  ASAN implementation, while the static wrapper library only contains stubs.
300
302
 
303
+ When building shared libraries with sanitizers, the library may have undefined
304
+ symbols that will be provided by the sanitizer runtime when loaded. LLD by
305
+ default enforces no undefined symbols, so we need to allow them explicitly.
306
+
301
307
  Environment Variables:
302
308
  CLANG_TOOL_CHAIN_NO_SHARED_ASAN: Set to '1' to disable shared ASAN
303
309
  """
@@ -306,7 +312,9 @@ class ASANRuntimeTransformer(ArgumentTransformer):
306
312
  return 250
307
313
 
308
314
  def transform(self, args: list[str], context: ToolContext) -> list[str]:
309
- """Add -shared-libasan when using ASAN on Linux."""
315
+ """Add -shared-libasan and --allow-shlib-undefined when using ASAN on Linux."""
316
+ import sys
317
+
310
318
  # Only applies to Linux clang/clang++
311
319
  if context.platform_name != "linux" or context.tool_name not in ("clang", "clang++"):
312
320
  return args
@@ -316,19 +324,41 @@ class ASANRuntimeTransformer(ArgumentTransformer):
316
324
  if not has_asan:
317
325
  return args
318
326
 
327
+ result = list(args)
328
+ injected_flags = []
329
+
319
330
  # Check if user disabled shared ASAN
320
- if os.environ.get("CLANG_TOOL_CHAIN_NO_SHARED_ASAN") == "1":
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:
321
340
  logger.debug("Shared ASAN disabled via CLANG_TOOL_CHAIN_NO_SHARED_ASAN=1")
322
- return args
323
341
 
324
- # Check if -shared-libasan already present
325
- if "-shared-libasan" in args:
326
- return args
342
+ # Check if building a shared library with ASAN
343
+ # Shared libraries need to allow undefined symbols that will be provided
344
+ # by the sanitizer runtime when the runner loads them
345
+ is_shared_lib = "-shared" in args
346
+ if is_shared_lib:
347
+ # Check if --allow-shlib-undefined already present
348
+ has_allow_shlib_undefined = any("--allow-shlib-undefined" in arg for arg in args)
349
+ if not has_allow_shlib_undefined:
350
+ logger.info("Adding -Wl,--allow-shlib-undefined for shared library with ASAN")
351
+ result = ["-Wl,--allow-shlib-undefined"] + result
352
+ injected_flags.append("-Wl,--allow-shlib-undefined")
353
+
354
+ # Warn on stderr if we injected flags
355
+ if injected_flags:
356
+ print(
357
+ f"clang-tool-chain: note: automatically injected sanitizer flags: {' '.join(injected_flags)}",
358
+ file=sys.stderr,
359
+ )
327
360
 
328
- # Add -shared-libasan to use shared runtime library
329
- # This prevents undefined symbol errors during linking
330
- logger.info("Adding -shared-libasan for ASAN runtime linking on Linux")
331
- return ["-shared-libasan"] + args
361
+ return result
332
362
 
333
363
 
334
364
  class RPathTransformer(ArgumentTransformer):
@@ -137,7 +137,7 @@ def _get_directive_args(source_path: Path) -> list[str]:
137
137
  print(f" Effective args: {' '.join(all_args)}", file=sys.stderr)
138
138
 
139
139
  return all_args
140
- except KeyboardInterrupt: # noqa: KBI002
140
+ except KeyboardInterrupt:
141
141
  # Re-raise KeyboardInterrupt to allow clean exit
142
142
  raise
143
143
  except Exception as e:
@@ -249,6 +249,8 @@ class BuildPipeline(ABC):
249
249
  Raises:
250
250
  SystemExit: Always exits with the executable's return code
251
251
  """
252
+ from clang_tool_chain.execution.sanitizer_env import prepare_sanitizer_environment
253
+
252
254
  program_args = self.config.program_args or []
253
255
 
254
256
  print(f"\nRunning: {self.config.output_file}", file=sys.stderr)
@@ -256,11 +258,15 @@ class BuildPipeline(ABC):
256
258
  print(f"Program arguments: {' '.join(program_args)}", file=sys.stderr)
257
259
  print("=" * 60, file=sys.stderr)
258
260
 
261
+ # Prepare environment with sanitizer options for better stack traces
262
+ # Only inject options if the corresponding sanitizer was used during compilation
263
+ env = prepare_sanitizer_environment(compiler_flags=self.config.compiler_flags)
264
+
259
265
  # Run the compiled executable
260
266
  try:
261
267
  # Use absolute path for Windows compatibility
262
268
  abs_output = self.output_path.absolute()
263
- result = subprocess.run([str(abs_output)] + program_args)
269
+ result = subprocess.run([str(abs_output)] + program_args, env=env)
264
270
  sys.exit(result.returncode)
265
271
  except FileNotFoundError:
266
272
  print(f"\n{'=' * 60}", file=sys.stderr)
@@ -0,0 +1,120 @@
1
+ """
2
+ Sanitizer runtime environment configuration.
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.
7
+
8
+ The default options fix <unknown module> entries in stack traces from
9
+ dlopen()'d shared libraries by enabling slow unwinding and symbolization.
10
+ """
11
+
12
+ import logging
13
+ import os
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Default options to inject for optimal stack traces
18
+ # fast_unwind_on_malloc=0: Use slow but accurate unwinding (fixes <unknown module>)
19
+ # symbolize=1: Enable symbolization for readable stack traces
20
+ # detect_leaks=1: Enable leak detection (ASAN only)
21
+ DEFAULT_ASAN_OPTIONS = "fast_unwind_on_malloc=0:symbolize=1:detect_leaks=1"
22
+ DEFAULT_LSAN_OPTIONS = "fast_unwind_on_malloc=0:symbolize=1"
23
+
24
+
25
+ def detect_sanitizers_from_flags(compiler_flags: list[str]) -> tuple[bool, bool]:
26
+ """
27
+ Detect which sanitizers are enabled from compiler flags.
28
+
29
+ Args:
30
+ compiler_flags: List of compiler flags passed to clang.
31
+
32
+ Returns:
33
+ Tuple of (asan_enabled, lsan_enabled).
34
+
35
+ Example:
36
+ >>> detect_sanitizers_from_flags(["-fsanitize=address", "-O2"])
37
+ (True, True) # ASAN implies LSAN by default
38
+ >>> detect_sanitizers_from_flags(["-fsanitize=leak"])
39
+ (False, True)
40
+ >>> detect_sanitizers_from_flags(["-O2", "-Wall"])
41
+ (False, False)
42
+ """
43
+ asan_enabled = False
44
+ lsan_enabled = False
45
+
46
+ for flag in compiler_flags:
47
+ if flag.startswith("-fsanitize="):
48
+ # Extract sanitizer list (e.g., "-fsanitize=address,undefined" -> "address,undefined")
49
+ sanitizers = flag.split("=", 1)[1].split(",")
50
+ for sanitizer in sanitizers:
51
+ sanitizer = sanitizer.strip()
52
+ if sanitizer == "address":
53
+ asan_enabled = True
54
+ # ASAN includes LSAN by default (unless detect_leaks=0)
55
+ lsan_enabled = True
56
+ elif sanitizer == "leak":
57
+ lsan_enabled = True
58
+
59
+ return asan_enabled, lsan_enabled
60
+
61
+
62
+ def prepare_sanitizer_environment(
63
+ base_env: dict[str, str] | None = None,
64
+ compiler_flags: list[str] | None = None,
65
+ ) -> dict[str, str]:
66
+ """
67
+ Prepare environment with optimal sanitizer options.
68
+
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.
73
+
74
+ Args:
75
+ base_env: Base environment dictionary to modify. If None, uses os.environ.
76
+ compiler_flags: List of compiler flags used to build the executable.
77
+ Used to detect which sanitizers are enabled. If None, no options
78
+ are injected (safe default).
79
+
80
+ Returns:
81
+ Environment dictionary with sanitizer options injected as appropriate.
82
+
83
+ Environment Variables:
84
+ CLANG_TOOL_CHAIN_NO_SANITIZER_ENV: Set to "1", "true", or "yes" to
85
+ disable automatic injection of sanitizer options.
86
+ ASAN_OPTIONS: If already set, preserved as-is (user config takes priority).
87
+ LSAN_OPTIONS: If already set, preserved as-is (user config takes priority).
88
+
89
+ Example:
90
+ >>> env = prepare_sanitizer_environment(compiler_flags=["-fsanitize=address"])
91
+ >>> # env now contains ASAN_OPTIONS and LSAN_OPTIONS
92
+ >>> env = prepare_sanitizer_environment(compiler_flags=["-O2"])
93
+ >>> # env unchanged - no sanitizers enabled
94
+ """
95
+ env = base_env.copy() if base_env is not None else os.environ.copy()
96
+
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")
100
+ return env
101
+
102
+ # If no compiler flags provided, don't inject anything (safe default)
103
+ if compiler_flags is None:
104
+ logger.debug("No compiler flags provided, skipping sanitizer environment injection")
105
+ return env
106
+
107
+ # Detect which sanitizers are enabled
108
+ asan_enabled, lsan_enabled = detect_sanitizers_from_flags(compiler_flags)
109
+
110
+ # Inject ASAN_OPTIONS if ASAN is enabled and not already set by user
111
+ if asan_enabled and "ASAN_OPTIONS" not in env:
112
+ env["ASAN_OPTIONS"] = DEFAULT_ASAN_OPTIONS
113
+ logger.info(f"Injecting ASAN_OPTIONS={DEFAULT_ASAN_OPTIONS}")
114
+
115
+ # Inject LSAN_OPTIONS if LSAN is enabled and not already set by user
116
+ if lsan_enabled and "LSAN_OPTIONS" not in env:
117
+ env["LSAN_OPTIONS"] = DEFAULT_LSAN_OPTIONS
118
+ logger.info(f"Injecting LSAN_OPTIONS={DEFAULT_LSAN_OPTIONS}")
119
+
120
+ return env
@@ -6,129 +6,48 @@ This module provides functions for:
6
6
  - Translating GNU ld flags to ld64.lld equivalents on macOS
7
7
  - Managing linker selection based on platform and user preferences
8
8
  - Ensuring ld64.lld symlink exists on macOS (runtime fallback)
9
- - LLVM version detection with caching for compatibility decisions
10
9
  """
11
10
 
12
11
  import logging
13
12
  import os
14
- import re
15
- import subprocess
16
13
  import sys
17
14
 
18
15
  from clang_tool_chain.interrupt_utils import handle_keyboard_interrupt_properly
16
+ from clang_tool_chain.llvm_versions import get_llvm_version_tuple, supports_ld64_lld_flag
19
17
 
20
18
  logger = logging.getLogger(__name__)
21
19
 
22
- # Module-level cache for LLVM version (avoids repeated subprocess calls)
23
- _cached_llvm_version: tuple[int, int, int] | None = None
24
20
 
25
-
26
- def _get_llvm_version() -> tuple[int, int, int] | None:
21
+ def _get_llvm_version_for_platform() -> tuple[int, int, int]:
27
22
  """
28
- Get the LLVM version of the installed clang, with caching.
29
-
30
- This runs `clang --version` and parses the version number. The result
31
- is cached in memory for the duration of the process, and also cached
32
- to a file in the install directory for persistence across invocations.
23
+ Get the LLVM version for the current platform from centralized configuration.
33
24
 
34
25
  Returns:
35
- Tuple of (major, minor, patch) version numbers, or None if detection fails.
26
+ Tuple of (major, minor, patch) version numbers.
36
27
  """
37
- global _cached_llvm_version
38
-
39
- # Return memory-cached version if available
40
- if _cached_llvm_version is not None:
41
- return _cached_llvm_version
42
-
43
- # Try to read from file cache
44
- try:
45
- from ..platform.detection import get_platform_binary_dir
46
-
47
- bin_dir = get_platform_binary_dir()
48
- cache_file = bin_dir.parent / ".llvm_version_cache"
49
-
50
- if cache_file.exists():
51
- cached = cache_file.read_text().strip()
52
- parts = cached.split(".")
53
- if len(parts) >= 3:
54
- _cached_llvm_version = (int(parts[0]), int(parts[1]), int(parts[2]))
55
- logger.debug(f"Loaded LLVM version from cache: {_cached_llvm_version}")
56
- return _cached_llvm_version
57
- except KeyboardInterrupt as ke:
58
- handle_keyboard_interrupt_properly(ke)
59
- except Exception as e:
60
- logger.debug(f"Could not read LLVM version cache: {e}")
28
+ from ..platform.detection import get_platform_info
61
29
 
62
- # Detect version by running clang --version
63
- try:
64
- from ..platform.detection import get_platform_binary_dir
65
-
66
- bin_dir = get_platform_binary_dir()
67
- clang_path = bin_dir / "clang"
68
-
69
- if not clang_path.exists():
70
- logger.debug(f"Clang binary not found at {clang_path}")
71
- return None
72
-
73
- result = subprocess.run(
74
- [str(clang_path), "--version"],
75
- capture_output=True,
76
- text=True,
77
- timeout=10,
78
- )
79
-
80
- if result.returncode != 0:
81
- logger.debug(f"clang --version failed: {result.stderr}")
82
- return None
83
-
84
- # Parse version from output like "clang version 19.1.7 (...)"
85
- version_match = re.search(r"clang version (\d+)\.(\d+)\.(\d+)", result.stdout)
86
- if version_match:
87
- major, minor, patch = int(version_match.group(1)), int(version_match.group(2)), int(version_match.group(3))
88
- _cached_llvm_version = (major, minor, patch)
89
- logger.debug(f"Detected LLVM version: {_cached_llvm_version}")
90
-
91
- # Save to file cache for persistence
92
- try:
93
- cache_file = bin_dir.parent / ".llvm_version_cache"
94
- cache_file.write_text(f"{major}.{minor}.{patch}")
95
- logger.debug(f"Saved LLVM version to cache: {cache_file}")
96
- except KeyboardInterrupt as ke:
97
- handle_keyboard_interrupt_properly(ke)
98
- except Exception as e:
99
- logger.debug(f"Could not write LLVM version cache: {e}")
100
-
101
- return _cached_llvm_version
102
-
103
- logger.debug(f"Could not parse LLVM version from: {result.stdout[:200]}")
104
- return None
105
-
106
- except KeyboardInterrupt as ke:
107
- handle_keyboard_interrupt_properly(ke)
108
- except Exception as e:
109
- logger.debug(f"LLVM version detection failed: {e}")
110
- return None
30
+ platform_name, _ = get_platform_info()
31
+ return get_llvm_version_tuple(platform_name)
111
32
 
112
33
 
113
34
  def _llvm_supports_ld64_lld_flag() -> bool:
114
35
  """
115
- Check if the installed LLVM version supports -fuse-ld=ld64.lld.
36
+ Check if the current platform's LLVM version supports -fuse-ld=ld64.lld.
116
37
 
117
- The -fuse-ld=ld64.lld flag is only recognized by LLVM 21.x and later.
118
- Earlier versions require -fuse-ld=lld (which auto-detects Mach-O from target).
38
+ Note: The clang driver does NOT recognize -fuse-ld=ld64.lld as a valid option.
39
+ This function exists for backward compatibility but always returns False in practice
40
+ because -fuse-ld=lld should be used instead (clang auto-dispatches to ld64.lld on Darwin).
119
41
 
120
42
  Returns:
121
- True if LLVM >= 21.x, False otherwise (or if version detection fails).
43
+ True if LLVM >= 21.x (but we always fall back to -fuse-ld=lld anyway)
122
44
  """
123
- version = _get_llvm_version()
124
- if version is None:
125
- # If we can't detect version, assume older LLVM for safety
126
- logger.debug("Could not detect LLVM version, assuming < 21.x for ld64.lld compatibility")
127
- return False
45
+ from ..platform.detection import get_platform_info
128
46
 
129
- major, _, _ = version
130
- supports = major >= 21
131
- logger.debug(f"LLVM {major}.x {'supports' if supports else 'does not support'} -fuse-ld=ld64.lld")
47
+ platform_name, _ = get_platform_info()
48
+ supports = supports_ld64_lld_flag(platform_name)
49
+ version = get_llvm_version_tuple(platform_name)
50
+ logger.debug(f"LLVM {version[0]}.x {'supports' if supports else 'does not support'} -fuse-ld=ld64.lld")
132
51
  return supports
133
52
 
134
53
 
@@ -354,6 +273,39 @@ def _user_specified_lld_on_macos(args: list[str]) -> bool:
354
273
  return "-fuse-ld=lld" in args_str or "-fuse-ld=ld64.lld" in args_str
355
274
 
356
275
 
276
+ def _user_specified_ld64_lld(args: list[str]) -> bool:
277
+ """
278
+ Check if the user explicitly specified -fuse-ld=ld64.lld.
279
+
280
+ This is used to emit a warning that the flag will be auto-converted
281
+ to -fuse-ld=lld since clang driver doesn't recognize ld64.lld.
282
+
283
+ Args:
284
+ args: Command-line arguments
285
+
286
+ Returns:
287
+ True if user specified -fuse-ld=ld64.lld
288
+ """
289
+ return any("-fuse-ld=ld64.lld" in arg for arg in args)
290
+
291
+
292
+ def _convert_ld64_lld_to_lld(args: list[str]) -> list[str]:
293
+ """
294
+ Convert -fuse-ld=ld64.lld to -fuse-ld=lld in arguments.
295
+
296
+ The clang driver does not recognize -fuse-ld=ld64.lld as a valid option.
297
+ This function converts it to -fuse-ld=lld which the driver recognizes
298
+ and automatically dispatches to ld64.lld on Darwin targets.
299
+
300
+ Args:
301
+ args: Original compiler arguments
302
+
303
+ Returns:
304
+ Modified arguments with -fuse-ld=ld64.lld replaced by -fuse-ld=lld
305
+ """
306
+ return [arg.replace("-fuse-ld=ld64.lld", "-fuse-ld=lld") for arg in args]
307
+
308
+
357
309
  # pyright: reportUnusedFunction=false
358
310
  def _add_lld_linker_if_needed(platform_name: str, args: list[str]) -> list[str]:
359
311
  """
@@ -366,9 +318,10 @@ def _add_lld_linker_if_needed(platform_name: str, args: list[str]) -> list[str]:
366
318
  - Faster linking performance
367
319
  - Uniform toolchain across all platforms
368
320
 
369
- Platform-specific linker flags:
370
- - macOS: Uses -fuse-ld=ld64.lld (explicit Mach-O variant required by LLVM 21.x+)
371
- - Linux: Uses -fuse-ld=lld (standard ELF linker)
321
+ Platform-specific behavior:
322
+ - Uses -fuse-ld=lld on all platforms (clang driver auto-dispatches)
323
+ - macOS: Clang driver finds ld64.lld (Mach-O linker)
324
+ - Linux: Clang driver finds ld.lld (ELF linker)
372
325
 
373
326
  The function is skipped when:
374
327
  - User sets CLANG_TOOL_CHAIN_USE_SYSTEM_LD=1
@@ -384,14 +337,25 @@ def _add_lld_linker_if_needed(platform_name: str, args: list[str]) -> list[str]:
384
337
  args: Original compiler arguments
385
338
 
386
339
  Returns:
387
- Modified arguments with platform-specific -fuse-ld flag prepended if needed
340
+ Modified arguments with -fuse-ld=lld flag prepended if needed
388
341
  """
389
342
  # On macOS, if user explicitly specified LLD, we still need to translate flags
390
343
  # and ensure ld64.lld symlink exists, even though we don't inject the -fuse-ld flag ourselves
391
344
  if platform_name == "darwin" and _user_specified_lld_on_macos(args):
392
345
  logger.debug("User specified LLD on macOS, translating GNU ld flags to ld64.lld equivalents")
393
- # Ensure ld64.lld symlink exists for user-specified -fuse-ld=ld64.lld
346
+ # Ensure ld64.lld symlink exists for lld to dispatch to Mach-O mode
394
347
  _ensure_ld64_lld_symlink()
348
+
349
+ # Check if user specified -fuse-ld=ld64.lld (which is not a valid clang driver option)
350
+ # and emit a warning about the auto-conversion to -fuse-ld=lld
351
+ if _user_specified_ld64_lld(args):
352
+ print(
353
+ "[clang-tool-chain] Warning: -fuse-ld=ld64.lld is not a valid clang driver option. "
354
+ "Auto-converting to -fuse-ld=lld (clang driver auto-dispatches to ld64.lld on Darwin).",
355
+ file=sys.stderr,
356
+ )
357
+ args = _convert_ld64_lld_to_lld(args)
358
+
395
359
  return _translate_linker_flags_for_macos_lld(args)
396
360
 
397
361
  if not _should_force_lld(platform_name, args):
@@ -402,22 +366,13 @@ def _add_lld_linker_if_needed(platform_name: str, args: list[str]) -> list[str]:
402
366
  # On macOS, translate GNU ld flags to ld64.lld equivalents
403
367
  if platform_name == "darwin":
404
368
  args = _translate_linker_flags_for_macos_lld(args)
369
+ # Ensure ld64.lld symlink exists for lld to dispatch to Mach-O mode
370
+ _ensure_ld64_lld_symlink()
405
371
 
406
- # Check LLVM version to determine the correct linker flag
407
- if _llvm_supports_ld64_lld_flag():
408
- # LLVM 21.x+ supports explicit -fuse-ld=ld64.lld
409
- _ensure_ld64_lld_symlink()
410
- return ["-fuse-ld=ld64.lld"] + args
411
- else:
412
- # LLVM < 21.x: rewrite to -fuse-ld=lld with compatibility notice
413
- version = _get_llvm_version()
414
- version_str = f"{version[0]}.{version[1]}.{version[2]}" if version else "unknown"
415
- print(
416
- f"[clang-tool-chain] LLVM {version_str} does not support -fuse-ld=ld64.lld, "
417
- f"using -fuse-ld=lld instead (lld auto-detects Mach-O from target)",
418
- file=sys.stderr,
419
- )
420
- return ["-fuse-ld=lld"] + args
421
- else:
422
- # Linux uses standard lld
423
- return ["-fuse-ld=lld"] + args
372
+ # Always use -fuse-ld=lld on all platforms.
373
+ # Note: -fuse-ld=ld64.lld is NOT a valid clang driver option.
374
+ # The clang driver only recognizes generic names like "lld", "gold", "bfd".
375
+ # When -fuse-ld=lld is used, clang automatically dispatches to:
376
+ # - Darwin: ld64.lld (Mach-O linker)
377
+ # - Linux: ld.lld (ELF linker)
378
+ return ["-fuse-ld=lld"] + args
@@ -0,0 +1,101 @@
1
+ """
2
+ Centralized LLVM version configuration for clang-tool-chain.
3
+
4
+ This module provides a single source of truth for LLVM versions used by the toolchain.
5
+ When updating LLVM versions, only this file needs to be modified.
6
+
7
+ Version history:
8
+ - 21.1.6: macOS (darwin) x86_64 and arm64
9
+ - 21.1.5: Windows x86_64, Linux x86_64 and arm64
10
+ """
11
+
12
+ from typing import NamedTuple
13
+
14
+
15
+ class LLVMVersion(NamedTuple):
16
+ """LLVM version as a tuple of (major, minor, patch)."""
17
+
18
+ major: int
19
+ minor: int
20
+ patch: int
21
+
22
+ def __str__(self) -> str:
23
+ return f"{self.major}.{self.minor}.{self.patch}"
24
+
25
+ @classmethod
26
+ def from_string(cls, version_str: str) -> "LLVMVersion":
27
+ """Parse a version string like '21.1.6' into an LLVMVersion."""
28
+ parts = version_str.split(".")
29
+ if len(parts) != 3:
30
+ raise ValueError(f"Invalid version string: {version_str}")
31
+ return cls(int(parts[0]), int(parts[1]), int(parts[2]))
32
+
33
+
34
+ # Centralized LLVM version configuration by platform
35
+ # Format: {platform_name: LLVMVersion}
36
+ # Platform names match detection.py: "darwin", "linux", "win"
37
+ LLVM_VERSIONS: dict[str, LLVMVersion] = {
38
+ "darwin": LLVMVersion(21, 1, 6), # macOS x86_64 and arm64
39
+ "linux": LLVMVersion(21, 1, 5), # Linux x86_64 and arm64
40
+ "win": LLVMVersion(21, 1, 5), # Windows x86_64
41
+ }
42
+
43
+ # Default version when platform is unknown (should not happen in practice)
44
+ DEFAULT_LLVM_VERSION = LLVMVersion(21, 1, 5)
45
+
46
+
47
+ def get_llvm_version(platform_name: str) -> LLVMVersion:
48
+ """
49
+ Get the LLVM version for a specific platform.
50
+
51
+ Args:
52
+ platform_name: Platform name ("darwin", "linux", "win")
53
+
54
+ Returns:
55
+ LLVMVersion tuple for the platform
56
+ """
57
+ return LLVM_VERSIONS.get(platform_name, DEFAULT_LLVM_VERSION)
58
+
59
+
60
+ def get_llvm_version_string(platform_name: str) -> str:
61
+ """
62
+ Get the LLVM version string for a specific platform.
63
+
64
+ Args:
65
+ platform_name: Platform name ("darwin", "linux", "win")
66
+
67
+ Returns:
68
+ Version string like "21.1.6"
69
+ """
70
+ return str(get_llvm_version(platform_name))
71
+
72
+
73
+ def get_llvm_version_tuple(platform_name: str) -> tuple[int, int, int]:
74
+ """
75
+ Get the LLVM version as a tuple for a specific platform.
76
+
77
+ Args:
78
+ platform_name: Platform name ("darwin", "linux", "win")
79
+
80
+ Returns:
81
+ Version tuple like (21, 1, 6)
82
+ """
83
+ version = get_llvm_version(platform_name)
84
+ return (version.major, version.minor, version.patch)
85
+
86
+
87
+ def supports_ld64_lld_flag(platform_name: str) -> bool:
88
+ """
89
+ Check if the platform's LLVM version supports -fuse-ld=ld64.lld.
90
+
91
+ The -fuse-ld=ld64.lld flag is only recognized by LLVM 21.x and later.
92
+ Earlier versions require -fuse-ld=lld (which auto-detects Mach-O from target).
93
+
94
+ Args:
95
+ platform_name: Platform name ("darwin", "linux", "win")
96
+
97
+ Returns:
98
+ True if LLVM >= 21.x, False otherwise
99
+ """
100
+ version = get_llvm_version(platform_name)
101
+ return version.major >= 21
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clang-tool-chain
3
- Version: 1.0.42
3
+ Version: 1.0.44
4
4
  Summary: Clang Tool Chain - C/C++ compilation toolchain utilities
5
5
  Project-URL: Homepage, https://github.com/zackees/clang-tool-chain
6
6
  Project-URL: Repository, https://github.com/zackees/clang-tool-chain
@@ -240,6 +240,7 @@ Comprehensive reference of all available commands organized by category.
240
240
  - [Inlined Build Directives](#-inlined-build-directives)
241
241
  - [Executable C++ Scripts](#-executable-c-scripts-shebang-support)
242
242
  - [Windows DLL Deployment](#-windows-dll-deployment)
243
+ - [Address Sanitizer (ASAN)](#️-address-sanitizer-asan-support)
243
244
  - [sccache Integration](#-sccache-integration)
244
245
 
245
246
  ### Platform & Configuration
@@ -794,6 +795,7 @@ jobs:
794
795
  - `CLANG_TOOL_CHAIN_LIB_DEPLOY_VERBOSE` - Enable verbose library deployment logging
795
796
  - `CLANG_TOOL_CHAIN_USE_SYSTEM_LD` - Use system linker instead of LLD
796
797
  - `CLANG_TOOL_CHAIN_NO_DIRECTIVES` - Disable inlined build directives
798
+ - `CLANG_TOOL_CHAIN_NO_SANITIZER_ENV` - Disable automatic ASAN/LSAN options injection at runtime
797
799
  - `SDKROOT` - Custom macOS SDK path (auto-detected by default)
798
800
 
799
801
  **📖 [Complete Documentation](docs/CONFIGURATION.md)** - All environment variables, macOS SDK, Windows DLL settings, sccache backends.
@@ -838,6 +840,56 @@ Automatic MinGW runtime DLL deployment for Windows executables (GNU ABI). Progra
838
840
 
839
841
  ---
840
842
 
843
+ ## 🛡️ Address Sanitizer (ASAN) Support
844
+
845
+ Full ASAN support with automatic runtime configuration for better stack traces.
846
+
847
+ ### Compilation
848
+
849
+ ```bash
850
+ # Compile with ASAN
851
+ clang-tool-chain-cpp -fsanitize=address test.cpp -o test
852
+
853
+ # With automatic library deployment (recommended)
854
+ clang-tool-chain-cpp -fsanitize=address test.cpp -o test --deploy-dependencies
855
+
856
+ # Run - ASAN errors will be detected
857
+ ./test
858
+ ```
859
+
860
+ ### Runtime Environment (Automatic)
861
+
862
+ When running executables via `clang-tool-chain-build-run`, optimal sanitizer options are **automatically injected** to improve stack trace quality - but **only when the corresponding sanitizer was used during compilation**:
863
+
864
+ - `ASAN_OPTIONS=fast_unwind_on_malloc=0:symbolize=1:detect_leaks=1` (when `-fsanitize=address` is used)
865
+ - `LSAN_OPTIONS=fast_unwind_on_malloc=0:symbolize=1` (when `-fsanitize=address` or `-fsanitize=leak` is used)
866
+
867
+ **What these options fix:**
868
+ - `<unknown module>` entries in stack traces from `dlopen()`'d shared libraries
869
+ - Missing function names in crash reports
870
+ - Incomplete leak detection
871
+
872
+ **Your options are always preserved** - if you set `ASAN_OPTIONS` or `LSAN_OPTIONS` yourself, clang-tool-chain won't override them.
873
+
874
+ **Regular builds are unaffected** - sanitizer options are only injected when the compiler flags indicate sanitizers are being used.
875
+
876
+ ### Configuration
877
+
878
+ ```bash
879
+ # Disable automatic sanitizer environment injection
880
+ export CLANG_TOOL_CHAIN_NO_SANITIZER_ENV=1
881
+
882
+ # Disable automatic -shared-libasan on Linux (use static ASAN)
883
+ export CLANG_TOOL_CHAIN_NO_SHARED_ASAN=1
884
+ ```
885
+
886
+ **Platform Notes:**
887
+ - **Linux**: Automatically uses `-shared-libasan` for proper runtime linking
888
+ - **Windows**: Works with both GNU and MSVC ABIs
889
+ - **macOS**: Uses bundled LLVM ASAN runtime
890
+
891
+ ---
892
+
841
893
  ## 🔧 How It Works
842
894
 
843
895
  Auto-downloads on first use (~71-91 MB, 10-60 seconds). Subsequent uses are instant.
@@ -1,5 +1,5 @@
1
1
  clang_tool_chain/__init__.py,sha256=w3f1hdA72SH-WsfwesNTQbTeiXIlhV8ignySXrV3SfU,23
2
- clang_tool_chain/__version__.py,sha256=_irBDZ3WPOleeFZ3Cc2qgO4-6MZvkq-a-BKfGhurz7Q,72
2
+ clang_tool_chain/__version__.py,sha256=VyBCcvXR6mMibUFhauoR0iyFq2jth9ReImF-qE_frUY,72
3
3
  clang_tool_chain/archive.py,sha256=t3reh7cm5XP2rhTqfRIDAQZv5XQq7SsstyiROYg8wFA,27697
4
4
  clang_tool_chain/archive_cache.py,sha256=5ZmlwXIJZDcrDFkTbdgBQYN9sulGn0WyI6qwWqC4HEU,6806
5
5
  clang_tool_chain/checksums.py,sha256=KFXeAeDz5ZlcZVOxsHDpNDCrm9UDoJ8bMA4PeNhuzdA,9868
@@ -12,6 +12,7 @@ clang_tool_chain/env_breadcrumbs.py,sha256=bvPTz8xABILhzrXTEBzdGrSbpEXLf2YVgcYEe
12
12
  clang_tool_chain/fetch.py,sha256=DwsNl5DZkNqEYXL-FbCTnp6IA2iCAa9pMl5oPjyuOS4,4696
13
13
  clang_tool_chain/installer.py,sha256=GuGeUvVcAw4HMj9jrub-I11ixmksw-vgtSOFrlupmPA,3323
14
14
  clang_tool_chain/interrupt_utils.py,sha256=7YvazvGzyItRVDZ_pzUSK6at8PCw-Dgih69HLSz0tT4,1153
15
+ clang_tool_chain/llvm_versions.py,sha256=lJuMmrRwOpXJxkrNL6HPbZOCemhZA88t0O0SET3VASo,2963
15
16
  clang_tool_chain/logging_config.py,sha256=HS0AMgIiyx2fwdl24mZ1oJDCW_MD5VeLxkTwT3d3v9o,2059
16
17
  clang_tool_chain/manifest.py,sha256=kYtUDQLY8RHpKeKRoIlzzA52bDnTG0Thx1rp19LvdNg,22138
17
18
  clang_tool_chain/parallel_download.py,sha256=nIyiTzSO8c3NA787TQztHRKko2GtQrZbiNbQiepjbDk,19144
@@ -35,10 +36,10 @@ clang_tool_chain/deployment/factory.py,sha256=Mq6cMMUctoYBZ81I16_hXUmDLQGSAngCmB
35
36
  clang_tool_chain/deployment/so_deployer.py,sha256=9mRKcJUXKV-8vXWJGdsUkB5cTPWjmToFUcHTVTCHdVk,11527
36
37
  clang_tool_chain/directives/__init__.py,sha256=MJDNYL_MD2MF0HFsrTsSTX645bYo6vtjq2pOTtfykaU,198
37
38
  clang_tool_chain/directives/parser.py,sha256=6J7mO1JtvuHkkKS0Xges5b_jT9b3uTF6ULI0ZiwGAdw,11179
38
- clang_tool_chain/execution/__init__.py,sha256=EeKG_0DGtDr1uqMq2rX_z9bpjHpLD9kgH-PY1_IeCJA,625
39
- clang_tool_chain/execution/arg_transformers.py,sha256=YkGqz_vbfNE-yFXHnzZREbS1T545AfQcV0DJTkU59YM,18254
39
+ clang_tool_chain/execution/__init__.py,sha256=wSYnlGmfTlaUPEKwW6PgwlxKTti6Wa5yyVjXVipdpcY,875
40
+ clang_tool_chain/execution/arg_transformers.py,sha256=vN-1T1mfD61Mg-P2B6hOQeLdD1RZA3l1pmNBbJSzo-g,19750
40
41
  clang_tool_chain/execution/build.py,sha256=PCtHw31WXbjCp2K_qaf1liaCiIjD49036cknqdQyhJM,13040
41
- clang_tool_chain/execution/build_pipeline.py,sha256=u13iMjsJj6lcguTGNWdJWgPi9QQv1Z_2VrR_l-sF3tg,17283
42
+ clang_tool_chain/execution/build_pipeline.py,sha256=piqh7P2-GR0mVOik3OUGRFqWFIRr8LQM3PU7TcBsujk,17622
42
43
  clang_tool_chain/execution/core.py,sha256=7CJ0azznC5lq5bw8amk2kwCIN2I_OnDiKytpapkvrdY,25273
43
44
  clang_tool_chain/execution/cosmocc.py,sha256=oGlaPK6Jpz3FaohEkWpRz8sRNu2sT-HYCoIf9xBxJHk,13319
44
45
  clang_tool_chain/execution/emscripten.py,sha256=lgxPQpeB1_wWxNILgeyyrW5lEn117dHS9dQ3ikHRn1w,44235
@@ -46,6 +47,7 @@ clang_tool_chain/execution/iwyu.py,sha256=bmP0d_PZObA1JfmFYp3qIOKCb7y32AWPm2_ReF
46
47
  clang_tool_chain/execution/lldb.py,sha256=VpxkWTPS6PsyskaKTALeziR5Z5NLwarW174Fm1SMX9k,20718
47
48
  clang_tool_chain/execution/nodejs_resolver.py,sha256=8QsJWvIfmt5mBDV7n0ypSjsPyXS-eZTizhBli937I-g,11172
48
49
  clang_tool_chain/execution/platform_executor.py,sha256=sF4GyW0ujy2EewG8y2Eo1gUWGzss5G5iIkv02w7-2_o,14362
50
+ clang_tool_chain/execution/sanitizer_env.py,sha256=lrGX17BriqYPsHCCrQnwjw0ttnp9mezsq0eYAOLfBag,4853
49
51
  clang_tool_chain/installers/__init__.py,sha256=NAV5woPCEDKSbFr1UmfQsrg4Ua5UdghN4q7H3ymvRsg,279
50
52
  clang_tool_chain/installers/base.py,sha256=OS78bau9zoYPitmhla7pKsfCPEj-zLY0DkvVzjE31Tw,15437
51
53
  clang_tool_chain/installers/clang.py,sha256=rUtheVRF7mq_1YdmQ3XzIybrJqsHbm2Xf0cbhRbH7RQ,16994
@@ -55,7 +57,7 @@ clang_tool_chain/installers/iwyu.py,sha256=9aAhdGtOTY6BrLuPtladY8Y2mz1i7FjgbMxZf
55
57
  clang_tool_chain/installers/lldb.py,sha256=FpG8NMNQk8PoNfg6aeU_plmSQrVET7zo-pTvoK8z838,2261
56
58
  clang_tool_chain/installers/nodejs.py,sha256=5N07rotgmCfUaDm1uJfBlIAFKC1iTpgZT0HBRuoYwKI,9343
57
59
  clang_tool_chain/linker/__init__.py,sha256=ghzDFpZ2-gPmdDO6K05C7yNbY6pZLANPuUks9TaQwVY,537
58
- clang_tool_chain/linker/lld.py,sha256=u7UR1GtJbl5SCmuXVxFzBZy2DdtiDGmdJ-KZbwpzgrc,15611
60
+ clang_tool_chain/linker/lld.py,sha256=08qkvoK4-8yACm34GfZzp3vM8rZYHCv_M9fjUeSetd0,13826
59
61
  clang_tool_chain/platform/__init__.py,sha256=WkV9Y25ua0mtzEGcsIxF-qExtroSTAMKkcElWuQF2BE,342
60
62
  clang_tool_chain/platform/detection.py,sha256=PLHyUfmQ5xuohhpz0KSXJWK3d0u0fCsjx1DbM8f1CxQ,5470
61
63
  clang_tool_chain/platform/paths.py,sha256=K0IjeVwbmgPlAWQO8mS3r1WS4C2dN6IYrSqPpckeT5c,6088
@@ -65,8 +67,8 @@ clang_tool_chain/sdk/windows.py,sha256=8zMLAoFz2OKMz-w6Kqxr3A-6Cofto2VWZvCvRi7kn
65
67
  clang_tool_chain/testing/__init__.py,sha256=-sYqOOCuTV_u-MkmExrD4uKdTHG4RmMwR3D1kIG281Q,208
66
68
  clang_tool_chain/testing/diagnostic_runner.py,sha256=mnmFUEOQulY3-Ggu6hKVGZwjrKQNmV6kY80PRTUu2qU,5293
67
69
  clang_tool_chain/testing/diagnostic_tests.py,sha256=GmtKWrDcddZTpx9_yIKfhRAy6YOde8dj7SksCWVEME4,6019
68
- clang_tool_chain-1.0.42.dist-info/METADATA,sha256=YhVwCSP3VcNbA5LQ-On1o1exq0o5I9vtUqUJzkoBasY,48504
69
- clang_tool_chain-1.0.42.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
70
- clang_tool_chain-1.0.42.dist-info/entry_points.txt,sha256=DBfnqZJzHFoO6rldGY3ESaX-z2ydAvIueUPM9ss9M-w,2728
71
- clang_tool_chain-1.0.42.dist-info/licenses/LICENSE,sha256=51FO1oc2pZbQNI0v0_THnznnZIF4iFgawG1xnQ58kKo,10997
72
- clang_tool_chain-1.0.42.dist-info/RECORD,,
70
+ clang_tool_chain-1.0.44.dist-info/METADATA,sha256=SjvdkUZlqBEHGRSpfaWoAys6sGSSOkH3_b9BdvKFOd0,50425
71
+ clang_tool_chain-1.0.44.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
72
+ clang_tool_chain-1.0.44.dist-info/entry_points.txt,sha256=DBfnqZJzHFoO6rldGY3ESaX-z2ydAvIueUPM9ss9M-w,2728
73
+ clang_tool_chain-1.0.44.dist-info/licenses/LICENSE,sha256=51FO1oc2pZbQNI0v0_THnznnZIF4iFgawG1xnQ58kKo,10997
74
+ clang_tool_chain-1.0.44.dist-info/RECORD,,