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.
@@ -1 +1,32 @@
1
1
  __version__ = "1.0.30"
2
+
3
+ # Re-export commonly used functions for convenient access
4
+ from clang_tool_chain.env_utils import (
5
+ CONTROLLABLE_FEATURES,
6
+ get_disabled_features,
7
+ is_auto_disabled,
8
+ is_feature_disabled,
9
+ )
10
+ from clang_tool_chain.execution.sanitizer_env import (
11
+ detect_sanitizers_from_flags,
12
+ get_symbolizer_path,
13
+ prepare_sanitizer_environment,
14
+ )
15
+ from clang_tool_chain.platform import find_tool_binary, get_platform_binary_dir, get_platform_info
16
+
17
+ __all__ = [
18
+ "__version__",
19
+ # Platform utilities
20
+ "get_platform_info",
21
+ "get_platform_binary_dir",
22
+ "find_tool_binary",
23
+ # Environment utilities
24
+ "is_feature_disabled",
25
+ "is_auto_disabled",
26
+ "get_disabled_features",
27
+ "CONTROLLABLE_FEATURES",
28
+ # Sanitizer environment
29
+ "prepare_sanitizer_environment",
30
+ "get_symbolizer_path",
31
+ "detect_sanitizers_from_flags",
32
+ ]
@@ -1,3 +1,3 @@
1
1
  """Version information for clang-tool-chain."""
2
2
 
3
- __version__ = "1.0.44"
3
+ __version__ = "1.0.47"
@@ -12,6 +12,7 @@ import re
12
12
  import subprocess
13
13
  from pathlib import Path
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
 
17
18
  from .base_deployer import BaseLibraryDeployer
@@ -510,9 +511,10 @@ def post_link_dll_deployment(output_exe_path: Path, platform_name: str, use_gnu_
510
511
  None
511
512
 
512
513
  Environment Variables:
513
- CLANG_TOOL_CHAIN_NO_DEPLOY_DLLS: Set to "1" to disable all DLL deployment
514
- CLANG_TOOL_CHAIN_NO_DEPLOY_DLLS_FOR_DLLS: Set to "1" to disable deployment for .dll outputs only
515
- CLANG_TOOL_CHAIN_DLL_DEPLOY_VERBOSE: Set to "1" for verbose logging
514
+ CLANG_TOOL_CHAIN_NO_DEPLOY_LIBS: Set to "1" to disable all library deployment
515
+ CLANG_TOOL_CHAIN_NO_DEPLOY_SHARED_LIB: Set to "1" to disable deployment for shared library outputs only
516
+ CLANG_TOOL_CHAIN_NO_AUTO: Set to "1" to disable all automatic features
517
+ CLANG_TOOL_CHAIN_LIB_DEPLOY_VERBOSE: Set to "1" for verbose logging
516
518
 
517
519
  Examples:
518
520
  >>> post_link_dll_deployment(Path("test.exe"), "win", True)
@@ -520,13 +522,12 @@ def post_link_dll_deployment(output_exe_path: Path, platform_name: str, use_gnu_
520
522
  >>> post_link_dll_deployment(Path("mylib.dll"), "win", True)
521
523
  # Deploys runtime DLLs alongside the shared library
522
524
  """
523
- # Check opt-out environment variable
524
- if os.environ.get("CLANG_TOOL_CHAIN_NO_DEPLOY_DLLS") == "1":
525
- logger.debug("DLL deployment disabled via CLANG_TOOL_CHAIN_NO_DEPLOY_DLLS")
525
+ # Check opt-out environment variable (via NO_DEPLOY_LIBS or NO_AUTO)
526
+ if is_feature_disabled("DEPLOY_LIBS"):
526
527
  return
527
528
 
528
529
  # Enable verbose logging if requested
529
- if os.environ.get("CLANG_TOOL_CHAIN_DLL_DEPLOY_VERBOSE") == "1":
530
+ if os.environ.get("CLANG_TOOL_CHAIN_LIB_DEPLOY_VERBOSE") == "1":
530
531
  logger.setLevel(logging.DEBUG)
531
532
 
532
533
  # Guard: only deploy on Windows
@@ -542,9 +543,8 @@ def post_link_dll_deployment(output_exe_path: Path, platform_name: str, use_gnu_
542
543
  # Guard: only deploy for .exe and .dll files
543
544
  suffix = output_exe_path.suffix.lower()
544
545
  if suffix == ".dll":
545
- # Check for DLL-specific opt-out
546
- if os.environ.get("CLANG_TOOL_CHAIN_NO_DEPLOY_DLLS_FOR_DLLS") == "1":
547
- logger.debug("DLL deployment for .dll outputs disabled via CLANG_TOOL_CHAIN_NO_DEPLOY_DLLS_FOR_DLLS")
546
+ # Check for shared library-specific opt-out (via NO_DEPLOY_SHARED_LIB or NO_AUTO)
547
+ if is_feature_disabled("DEPLOY_SHARED_LIB"):
548
548
  return
549
549
  elif suffix != ".exe":
550
550
  logger.debug(f"DLL deployment skipped: not .exe or .dll file (suffix={output_exe_path.suffix})")
@@ -596,10 +596,9 @@ def post_link_dependency_deployment(output_path: Path, platform_name: str, use_g
596
596
  None
597
597
 
598
598
  Environment Variables:
599
- CLANG_TOOL_CHAIN_NO_DEPLOY_DLLS: Set to "1" to disable deployment (all platforms)
600
- CLANG_TOOL_CHAIN_NO_DEPLOY_LIBS: Set to "1" to disable deployment (alias)
601
- CLANG_TOOL_CHAIN_DLL_DEPLOY_VERBOSE: Set to "1" for verbose logging (all platforms)
602
- CLANG_TOOL_CHAIN_LIB_DEPLOY_VERBOSE: Set to "1" for verbose logging (alias)
599
+ CLANG_TOOL_CHAIN_NO_DEPLOY_LIBS: Set to "1" to disable all library deployment
600
+ CLANG_TOOL_CHAIN_NO_AUTO: Set to "1" to disable all automatic features
601
+ CLANG_TOOL_CHAIN_LIB_DEPLOY_VERBOSE: Set to "1" for verbose logging
603
602
 
604
603
  Examples:
605
604
  >>> post_link_dependency_deployment(Path("mylib.dll"), "win", True)
@@ -609,17 +608,11 @@ def post_link_dependency_deployment(output_path: Path, platform_name: str, use_g
609
608
  >>> post_link_dependency_deployment(Path("mylib.dylib"), "darwin", False)
610
609
  # Deploys libunwind.1.dylib, libc++.1.dylib, etc. to mylib.dylib directory
611
610
  """
612
- # Check opt-out environment variables (either DLL-specific or generic)
613
- if os.environ.get("CLANG_TOOL_CHAIN_NO_DEPLOY_DLLS") == "1":
614
- logger.debug("Dependency deployment disabled via CLANG_TOOL_CHAIN_NO_DEPLOY_DLLS")
615
- return
616
- if os.environ.get("CLANG_TOOL_CHAIN_NO_DEPLOY_LIBS") == "1":
617
- logger.debug("Dependency deployment disabled via CLANG_TOOL_CHAIN_NO_DEPLOY_LIBS")
611
+ # Check opt-out environment variables (NO_DEPLOY_LIBS or NO_AUTO)
612
+ if is_feature_disabled("DEPLOY_LIBS"):
618
613
  return
619
614
 
620
- # Enable verbose logging if requested (either DLL-specific or generic)
621
- if os.environ.get("CLANG_TOOL_CHAIN_DLL_DEPLOY_VERBOSE") == "1":
622
- logger.setLevel(logging.DEBUG)
615
+ # Enable verbose logging if requested
623
616
  if os.environ.get("CLANG_TOOL_CHAIN_LIB_DEPLOY_VERBOSE") == "1":
624
617
  logger.setLevel(logging.DEBUG)
625
618
 
@@ -18,11 +18,12 @@ Example:
18
18
  """
19
19
 
20
20
  import logging
21
- import os
22
21
  import re
23
22
  import subprocess
24
23
  from pathlib import Path
25
24
 
25
+ from clang_tool_chain.env_utils import is_feature_disabled
26
+
26
27
  from .base_deployer import BaseLibraryDeployer
27
28
 
28
29
  logger = logging.getLogger(__name__)
@@ -499,7 +500,8 @@ def post_link_dylib_deployment(output_path: Path, arch: str = "x86_64") -> int:
499
500
 
500
501
  Environment variables:
501
502
  - CLANG_TOOL_CHAIN_NO_DEPLOY_LIBS=1 - Disable all library deployment
502
- - CLANG_TOOL_CHAIN_NO_DEPLOY_DYLIBS=1 - Disable macOS-specific deployment
503
+ - CLANG_TOOL_CHAIN_NO_DEPLOY_SHARED_LIB=1 - Disable deployment for shared library outputs
504
+ - CLANG_TOOL_CHAIN_NO_AUTO=1 - Disable all automatic features
503
505
 
504
506
  Args:
505
507
  output_path: Path to executable or shared library
@@ -512,14 +514,12 @@ def post_link_dylib_deployment(output_path: Path, arch: str = "x86_64") -> int:
512
514
  >>> # Called automatically after linking
513
515
  >>> count = post_link_dylib_deployment(Path("program"), arch="arm64")
514
516
  """
515
- # Check global disable
516
- if os.getenv("CLANG_TOOL_CHAIN_NO_DEPLOY_LIBS") == "1":
517
- logger.debug("dylib deployment disabled (CLANG_TOOL_CHAIN_NO_DEPLOY_LIBS=1)")
517
+ # Check environment variables (NO_DEPLOY_LIBS or NO_AUTO)
518
+ if is_feature_disabled("DEPLOY_LIBS"):
518
519
  return 0
519
520
 
520
- # Check macOS-specific disable
521
- if os.getenv("CLANG_TOOL_CHAIN_NO_DEPLOY_DYLIBS") == "1":
522
- logger.debug("dylib deployment disabled (CLANG_TOOL_CHAIN_NO_DEPLOY_DYLIBS=1)")
521
+ # Check if output is a shared library (.dylib) - if so, check NO_DEPLOY_SHARED_LIB
522
+ if output_path.suffix == ".dylib" and is_feature_disabled("DEPLOY_SHARED_LIB"):
523
523
  return 0
524
524
 
525
525
  # Check if output is executable or .dylib
@@ -0,0 +1,287 @@
1
+ """
2
+ Library deployment CLI for deploying runtime dependencies after the fact.
3
+
4
+ This module provides a command-line interface to deploy library dependencies
5
+ for already-built executables and shared libraries. It supports:
6
+ - Windows: .exe and .dll files (MinGW runtime DLLs)
7
+ - Linux: executables and .so files (libc++, libunwind, etc.)
8
+ - macOS: executables and .dylib files (libc++, libunwind, etc.)
9
+
10
+ Usage:
11
+ clang-tool-chain-libdeploy myprogram.exe
12
+ clang-tool-chain-libdeploy mylib.dll
13
+ clang-tool-chain-libdeploy myprogram
14
+ clang-tool-chain-libdeploy mylib.so
15
+ clang-tool-chain-libdeploy mylib.dylib
16
+ """
17
+
18
+ import argparse
19
+ import logging
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ from clang_tool_chain.interrupt_utils import handle_keyboard_interrupt_properly
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ def _setup_logging(verbose: bool) -> None:
29
+ """Configure logging based on verbosity."""
30
+ level = logging.DEBUG if verbose else logging.INFO
31
+ logging.basicConfig(
32
+ level=level,
33
+ format="%(message)s",
34
+ stream=sys.stderr,
35
+ )
36
+ # Also set the deployment module loggers
37
+ logging.getLogger("clang_tool_chain.deployment").setLevel(level)
38
+
39
+
40
+ def _detect_binary_type(binary_path: Path) -> tuple[str, str]:
41
+ """
42
+ Detect the binary type and platform from file extension and magic bytes.
43
+
44
+ Args:
45
+ binary_path: Path to the binary file
46
+
47
+ Returns:
48
+ Tuple of (platform, binary_type) where:
49
+ - platform: "windows", "linux", or "darwin"
50
+ - binary_type: "executable" or "shared_library"
51
+
52
+ Raises:
53
+ ValueError: If binary type cannot be determined
54
+ """
55
+ suffix = binary_path.suffix.lower()
56
+
57
+ # Windows binaries
58
+ if suffix == ".exe":
59
+ return ("windows", "executable")
60
+ if suffix == ".dll":
61
+ return ("windows", "shared_library")
62
+
63
+ # macOS binaries
64
+ if suffix == ".dylib":
65
+ return ("darwin", "shared_library")
66
+
67
+ # Linux shared libraries (including versioned: .so.1, .so.1.2.3)
68
+ if suffix == ".so" or ".so." in binary_path.name:
69
+ return ("linux", "shared_library")
70
+
71
+ # No extension - could be Linux/macOS executable or a .so with version
72
+ # Try to detect from magic bytes
73
+ if binary_path.exists() and binary_path.is_file():
74
+ try:
75
+ with open(binary_path, "rb") as f:
76
+ magic = f.read(4)
77
+
78
+ # ELF magic: 0x7f 'E' 'L' 'F'
79
+ if magic[:4] == b"\x7fELF":
80
+ return ("linux", "executable")
81
+
82
+ # Mach-O magic numbers
83
+ # MH_MAGIC (32-bit): 0xfeedface
84
+ # MH_MAGIC_64 (64-bit): 0xfeedfacf
85
+ # Fat binary: 0xcafebabe
86
+ if magic[:4] in (b"\xfe\xed\xfa\xce", b"\xfe\xed\xfa\xcf", b"\xcf\xfa\xed\xfe", b"\xce\xfa\xed\xfe"):
87
+ return ("darwin", "executable")
88
+ if magic[:4] == b"\xca\xfe\xba\xbe":
89
+ return ("darwin", "executable")
90
+
91
+ # PE magic: 'MZ'
92
+ if magic[:2] == b"MZ":
93
+ return ("windows", "executable")
94
+
95
+ except (OSError, PermissionError) as e:
96
+ logger.debug(f"Could not read magic bytes: {e}")
97
+
98
+ raise ValueError(
99
+ f"Cannot determine binary type for: {binary_path}\n"
100
+ "Supported formats: .exe, .dll (Windows), .so (Linux), .dylib (macOS), "
101
+ "or ELF/Mach-O executables without extension"
102
+ )
103
+
104
+
105
+ def deploy_dependencies(
106
+ binary_path: Path,
107
+ platform_override: str | None = None,
108
+ arch: str | None = None,
109
+ verbose: bool = False,
110
+ dry_run: bool = False,
111
+ ) -> int:
112
+ """
113
+ Deploy library dependencies for a binary.
114
+
115
+ Args:
116
+ binary_path: Path to executable or shared library
117
+ platform_override: Override auto-detected platform ("windows", "linux", "darwin")
118
+ arch: Target architecture (default: auto-detect from current platform)
119
+ verbose: Enable verbose output
120
+ dry_run: If True, only show what would be deployed without actually copying
121
+
122
+ Returns:
123
+ Number of libraries deployed, or -1 on error
124
+ """
125
+ from clang_tool_chain.deployment.factory import create_deployer
126
+ from clang_tool_chain.platform.detection import get_platform_info
127
+
128
+ # Get architecture if not specified
129
+ if arch is None:
130
+ _, arch = get_platform_info()
131
+
132
+ # Detect or use override for platform
133
+ if platform_override:
134
+ platform = platform_override.lower()
135
+ if platform not in ("windows", "linux", "darwin", "win", "win32", "macos"):
136
+ logger.error(f"Invalid platform: {platform_override}")
137
+ return -1
138
+ # Normalize platform names
139
+ if platform in ("win", "win32"):
140
+ platform = "windows"
141
+ elif platform == "macos":
142
+ platform = "darwin"
143
+ else:
144
+ try:
145
+ platform, _ = _detect_binary_type(binary_path)
146
+ except ValueError as e:
147
+ logger.error(str(e))
148
+ return -1
149
+
150
+ # Validate binary exists
151
+ if not binary_path.exists():
152
+ logger.error(f"Binary not found: {binary_path}")
153
+ return -1
154
+
155
+ if not binary_path.is_file():
156
+ logger.error(f"Not a file: {binary_path}")
157
+ return -1
158
+
159
+ # Create appropriate deployer
160
+ deployer = create_deployer(platform, arch)
161
+ if deployer is None:
162
+ logger.error(f"No deployer available for platform: {platform}")
163
+ return -1
164
+
165
+ if verbose:
166
+ logger.info(f"Deploying dependencies for: {binary_path}")
167
+ logger.info(f"Platform: {platform}, Architecture: {arch}")
168
+
169
+ if dry_run:
170
+ # Dry run - just show what would be deployed
171
+ dependencies = deployer.detect_all_dependencies(binary_path, recursive=True)
172
+ if not dependencies:
173
+ logger.info("No deployable dependencies found")
174
+ return 0
175
+
176
+ logger.info(f"Would deploy {len(dependencies)} libraries:")
177
+ for dep in sorted(dependencies):
178
+ src = deployer.find_library_in_toolchain(dep)
179
+ if src:
180
+ logger.info(f" {dep} <- {src}")
181
+ else:
182
+ logger.info(f" {dep} (source not found)")
183
+ return len(dependencies)
184
+
185
+ # Actually deploy
186
+ try:
187
+ deployed_count = deployer.deploy_all(binary_path)
188
+ if deployed_count == 0 and verbose:
189
+ logger.info("No libraries needed deployment (all up-to-date or none required)")
190
+ return deployed_count
191
+ except Exception as e:
192
+ logger.error(f"Deployment failed: {e}")
193
+ return -1
194
+
195
+
196
+ def main() -> int:
197
+ """
198
+ Main entry point for clang-tool-chain-libdeploy.
199
+
200
+ Returns:
201
+ Exit code (0 for success, non-zero for failure)
202
+ """
203
+ parser = argparse.ArgumentParser(
204
+ prog="clang-tool-chain-libdeploy",
205
+ description="Deploy runtime library dependencies for executables and shared libraries",
206
+ epilog="""
207
+ Examples:
208
+ clang-tool-chain-libdeploy myprogram.exe # Windows executable
209
+ clang-tool-chain-libdeploy mylib.dll # Windows DLL
210
+ clang-tool-chain-libdeploy myprogram # Linux/macOS executable
211
+ clang-tool-chain-libdeploy mylib.so # Linux shared library
212
+ clang-tool-chain-libdeploy mylib.dylib # macOS dynamic library
213
+ clang-tool-chain-libdeploy --dry-run myprogram # Show what would be deployed
214
+
215
+ Supported platforms:
216
+ Windows: Deploys MinGW runtime DLLs (libwinpthread, libgcc_s, libstdc++, etc.)
217
+ Linux: Deploys libc++, libunwind, sanitizer runtimes
218
+ macOS: Deploys libc++, libunwind, sanitizer runtimes
219
+ """,
220
+ formatter_class=argparse.RawDescriptionHelpFormatter,
221
+ )
222
+
223
+ parser.add_argument(
224
+ "binary",
225
+ type=Path,
226
+ help="Path to executable (.exe) or shared library (.dll, .so, .dylib)",
227
+ )
228
+
229
+ parser.add_argument(
230
+ "-v",
231
+ "--verbose",
232
+ action="store_true",
233
+ help="Enable verbose output",
234
+ )
235
+
236
+ parser.add_argument(
237
+ "-n",
238
+ "--dry-run",
239
+ action="store_true",
240
+ help="Show what would be deployed without copying files",
241
+ )
242
+
243
+ parser.add_argument(
244
+ "-p",
245
+ "--platform",
246
+ choices=["windows", "linux", "darwin"],
247
+ help="Override auto-detected platform",
248
+ )
249
+
250
+ parser.add_argument(
251
+ "-a",
252
+ "--arch",
253
+ help="Target architecture (default: auto-detect)",
254
+ )
255
+
256
+ args = parser.parse_args()
257
+
258
+ # Setup logging
259
+ _setup_logging(args.verbose)
260
+
261
+ try:
262
+ result = deploy_dependencies(
263
+ binary_path=args.binary.resolve(),
264
+ platform_override=args.platform,
265
+ arch=args.arch,
266
+ verbose=args.verbose,
267
+ dry_run=args.dry_run,
268
+ )
269
+
270
+ if result < 0:
271
+ return 1
272
+
273
+ if result == 0 and not args.dry_run:
274
+ # No libraries deployed is not necessarily an error
275
+ return 0
276
+
277
+ return 0
278
+
279
+ except KeyboardInterrupt as ke:
280
+ handle_keyboard_interrupt_properly(ke) # NoReturn - re-raises
281
+ except Exception as e:
282
+ logger.error(f"Error: {e}")
283
+ return 1
284
+
285
+
286
+ if __name__ == "__main__":
287
+ sys.exit(main())
@@ -12,6 +12,7 @@ import re
12
12
  import subprocess
13
13
  from pathlib import Path
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
 
17
18
  from .base_deployer import BaseLibraryDeployer
@@ -35,6 +36,8 @@ class SoDeployer(BaseLibraryDeployer):
35
36
  r"libc\+\+\.so[.\d]*",
36
37
  r"libc\+\+abi\.so[.\d]*",
37
38
  r"libunwind\.so[.\d]*",
39
+ r"libunwind-x86_64\.so[.\d]*", # Platform-specific libunwind (bundled)
40
+ r"libunwind-aarch64\.so[.\d]*", # Platform-specific libunwind (bundled)
38
41
  r"libclang_rt\..*\.so", # Sanitizer runtimes
39
42
  ]
40
43
 
@@ -309,14 +312,19 @@ def post_link_so_deployment(
309
312
 
310
313
  Returns:
311
314
  Number of .so files deployed
315
+
316
+ Environment Variables:
317
+ CLANG_TOOL_CHAIN_NO_DEPLOY_LIBS: Set to "1" to disable all library deployment
318
+ CLANG_TOOL_CHAIN_NO_DEPLOY_SHARED_LIB: Set to "1" to disable deployment for shared library outputs
319
+ CLANG_TOOL_CHAIN_NO_AUTO: Set to "1" to disable all automatic features
312
320
  """
313
- # Check environment variables
314
- if os.getenv("CLANG_TOOL_CHAIN_NO_DEPLOY_LIBS") == "1":
315
- logger.debug("Linux .so deployment disabled by CLANG_TOOL_CHAIN_NO_DEPLOY_LIBS")
321
+ # Check environment variables (NO_DEPLOY_LIBS or NO_AUTO)
322
+ if is_feature_disabled("DEPLOY_LIBS"):
316
323
  return 0
317
324
 
318
- if os.getenv("CLANG_TOOL_CHAIN_NO_DEPLOY_SO") == "1":
319
- logger.debug("Linux .so deployment disabled by CLANG_TOOL_CHAIN_NO_DEPLOY_SO")
325
+ # Check if output is a shared library (.so) - if so, check NO_DEPLOY_SHARED_LIB
326
+ is_shared_lib = output_path.suffix == ".so" or ".so." in output_path.name
327
+ if is_shared_lib and is_feature_disabled("DEPLOY_SHARED_LIB"):
320
328
  return 0
321
329
 
322
330
  # Check if output is a deployable binary
@@ -0,0 +1,137 @@
1
+ """
2
+ Environment variable utilities for clang-tool-chain.
3
+
4
+ This module provides centralized handling of environment variable checks,
5
+ including support for the aggregate CLANG_TOOL_CHAIN_NO_AUTO variable
6
+ that disables all automatic features.
7
+
8
+ Example:
9
+ >>> from clang_tool_chain.env_utils import is_feature_disabled
10
+ >>> if is_feature_disabled("DIRECTIVES"):
11
+ ... # Skip directive parsing
12
+ ... pass
13
+ """
14
+
15
+ import logging
16
+ import os
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # Truthy values for boolean environment variables
21
+ _TRUTHY_VALUES = ("1", "true", "yes")
22
+
23
+ # All features that can be disabled via NO_AUTO
24
+ # Maps feature name to the specific environment variable suffix
25
+ CONTROLLABLE_FEATURES = {
26
+ "DIRECTIVES": "Inlined build directives (@link, @std, @cflags)",
27
+ "SHARED_ASAN": "Automatic -shared-libasan injection (Linux)",
28
+ "SANITIZER_ENV": "Automatic ASAN_OPTIONS/LSAN_OPTIONS injection",
29
+ "SANITIZER_NOTE": "Sanitizer flag injection note to stderr",
30
+ "RPATH": "Automatic rpath injection for library loading",
31
+ "SYSROOT": "Automatic macOS SDK detection (-isysroot)",
32
+ "DEPLOY_LIBS": "Cross-platform library deployment (all outputs)",
33
+ "DEPLOY_SHARED_LIB": "Library deployment for shared library outputs only (.dll, .so, .dylib)",
34
+ }
35
+
36
+
37
+ def _is_truthy(value: str | None) -> bool:
38
+ """Check if an environment variable value is truthy."""
39
+ if value is None:
40
+ return False
41
+ return value.lower() in _TRUTHY_VALUES
42
+
43
+
44
+ def is_auto_disabled() -> bool:
45
+ """
46
+ Check if all automatic features are disabled via CLANG_TOOL_CHAIN_NO_AUTO.
47
+
48
+ Returns:
49
+ True if CLANG_TOOL_CHAIN_NO_AUTO is set to a truthy value.
50
+
51
+ Example:
52
+ >>> os.environ["CLANG_TOOL_CHAIN_NO_AUTO"] = "1"
53
+ >>> is_auto_disabled()
54
+ True
55
+ """
56
+ return _is_truthy(os.environ.get("CLANG_TOOL_CHAIN_NO_AUTO"))
57
+
58
+
59
+ def is_feature_disabled(feature: str) -> bool:
60
+ """
61
+ Check if a specific feature is disabled.
62
+
63
+ A feature is disabled if either:
64
+ 1. The aggregate CLANG_TOOL_CHAIN_NO_AUTO=1 is set, OR
65
+ 2. The specific CLANG_TOOL_CHAIN_NO_{feature}=1 is set
66
+
67
+ Args:
68
+ feature: The feature name (e.g., "DIRECTIVES", "SHARED_ASAN", "DEPLOY_LIBS").
69
+ Should match one of the keys in CONTROLLABLE_FEATURES.
70
+
71
+ Returns:
72
+ True if the feature is disabled, False otherwise.
73
+
74
+ Example:
75
+ >>> os.environ["CLANG_TOOL_CHAIN_NO_DIRECTIVES"] = "1"
76
+ >>> is_feature_disabled("DIRECTIVES")
77
+ True
78
+
79
+ >>> os.environ["CLANG_TOOL_CHAIN_NO_AUTO"] = "1"
80
+ >>> is_feature_disabled("SHARED_ASAN") # Disabled by NO_AUTO
81
+ True
82
+ """
83
+ # Check the aggregate NO_AUTO variable first
84
+ if is_auto_disabled():
85
+ logger.debug(f"Feature {feature} disabled via CLANG_TOOL_CHAIN_NO_AUTO=1")
86
+ return True
87
+
88
+ # Check the specific feature variable
89
+ var_name = f"CLANG_TOOL_CHAIN_NO_{feature}"
90
+ if _is_truthy(os.environ.get(var_name)):
91
+ logger.debug(f"Feature {feature} disabled via {var_name}=1")
92
+ return True
93
+
94
+ return False
95
+
96
+
97
+ def get_disabled_features() -> list[str]:
98
+ """
99
+ Get a list of all currently disabled features.
100
+
101
+ Returns:
102
+ List of feature names that are disabled.
103
+
104
+ Example:
105
+ >>> os.environ["CLANG_TOOL_CHAIN_NO_AUTO"] = "1"
106
+ >>> get_disabled_features()
107
+ ['DIRECTIVES', 'SHARED_ASAN', 'SANITIZER_ENV', ...]
108
+ """
109
+ disabled = []
110
+
111
+ # If NO_AUTO is set, all features are disabled
112
+ if is_auto_disabled():
113
+ return list(CONTROLLABLE_FEATURES.keys())
114
+
115
+ # Check each feature individually
116
+ for feature in CONTROLLABLE_FEATURES:
117
+ var_name = f"CLANG_TOOL_CHAIN_NO_{feature}"
118
+ if _is_truthy(os.environ.get(var_name)):
119
+ disabled.append(feature)
120
+
121
+ return disabled
122
+
123
+
124
+ def log_disabled_features_summary() -> None:
125
+ """
126
+ Log a summary of disabled features (useful for debugging).
127
+
128
+ Only logs if at least one feature is disabled.
129
+ """
130
+ disabled = get_disabled_features()
131
+ if not disabled:
132
+ return
133
+
134
+ if is_auto_disabled():
135
+ logger.info("All automatic features disabled via CLANG_TOOL_CHAIN_NO_AUTO=1")
136
+ else:
137
+ logger.info(f"Disabled features: {', '.join(disabled)}")
@@ -10,7 +10,11 @@ This package provides:
10
10
 
11
11
  from clang_tool_chain.execution.build import build_main, build_run_main
12
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
13
+ from clang_tool_chain.execution.sanitizer_env import (
14
+ detect_sanitizers_from_flags,
15
+ get_symbolizer_path,
16
+ prepare_sanitizer_environment,
17
+ )
14
18
 
15
19
  __all__ = [
16
20
  # Core execution
@@ -25,4 +29,5 @@ __all__ = [
25
29
  # Sanitizer utilities
26
30
  "prepare_sanitizer_environment",
27
31
  "detect_sanitizers_from_flags",
32
+ "get_symbolizer_path",
28
33
  ]