clang-tool-chain 1.0.3__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.
@@ -0,0 +1,1589 @@
1
+ """
2
+ Wrapper infrastructure for executing LLVM/Clang tools.
3
+
4
+ This module provides the core functionality for wrapping LLVM toolchain
5
+ binaries and forwarding commands to them with proper platform detection.
6
+ """
7
+
8
+ import hashlib
9
+ import logging
10
+ import os
11
+ import platform
12
+ import shutil
13
+ import subprocess
14
+ import sys
15
+ from pathlib import Path
16
+ from typing import NoReturn
17
+
18
+ from . import downloader
19
+
20
+ # Configure logging for GitHub Actions and general debugging
21
+ logging.basicConfig(
22
+ level=logging.INFO,
23
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
24
+ handlers=[logging.StreamHandler(sys.stderr)],
25
+ )
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ def _get_toolchain_directory_listing(platform_name: str) -> str:
30
+ """
31
+ Get a directory listing of ~/.clang-tool-chain for debugging purposes.
32
+
33
+ Args:
34
+ platform_name: Platform name ("win", "linux", "darwin")
35
+
36
+ Returns:
37
+ Formatted directory listing string (2 levels deep)
38
+ """
39
+ import subprocess as _subprocess
40
+
41
+ toolchain_dir = Path.home() / ".clang-tool-chain"
42
+
43
+ try:
44
+ if platform_name == "win":
45
+ # On Windows, manually walk the directory tree (2 levels)
46
+ lines = []
47
+ if toolchain_dir.exists():
48
+ lines.append(str(toolchain_dir))
49
+ for item in toolchain_dir.iterdir():
50
+ lines.append(f" {item.name}")
51
+ if item.is_dir():
52
+ try:
53
+ for subitem in item.iterdir():
54
+ lines.append(f" {item.name}/{subitem.name}")
55
+ except (PermissionError, OSError):
56
+ pass
57
+ return "\n".join(lines)
58
+ else:
59
+ # On Unix, use find
60
+ result = _subprocess.run(
61
+ ["find", str(toolchain_dir), "-maxdepth", "2"], capture_output=True, text=True, timeout=5
62
+ )
63
+ return result.stdout
64
+ except Exception as e:
65
+ return f"Could not list directory: {e}"
66
+
67
+
68
+ def get_platform_info() -> tuple[str, str]:
69
+ """
70
+ Detect the current platform and architecture.
71
+
72
+ Returns:
73
+ Tuple of (platform, architecture) strings
74
+ Platform: "win", "linux", or "darwin"
75
+ Architecture: "x86_64" or "aarch64"
76
+ """
77
+ system = platform.system().lower()
78
+ machine = platform.machine().lower()
79
+
80
+ logger.debug(f"Detecting platform: system={system}, machine={machine}")
81
+
82
+ # Normalize platform name
83
+ if system == "windows":
84
+ platform_name = "win"
85
+ elif system == "linux":
86
+ platform_name = "linux"
87
+ elif system == "darwin":
88
+ platform_name = "darwin"
89
+ else:
90
+ logger.error(f"Unsupported platform detected: {system}")
91
+ raise RuntimeError(
92
+ f"Unsupported platform: {system}\n"
93
+ f"clang-tool-chain currently supports: Windows, Linux, and macOS (Darwin)\n"
94
+ f"Your system: {system}\n"
95
+ f"If you believe this platform should be supported, please report this at:\n"
96
+ f"https://github.com/zackees/clang-tool-chain/issues"
97
+ )
98
+
99
+ # Normalize architecture
100
+ if machine in ("x86_64", "amd64"):
101
+ arch = "x86_64"
102
+ elif machine in ("aarch64", "arm64"):
103
+ arch = "arm64"
104
+ else:
105
+ logger.error(f"Unsupported architecture detected: {machine}")
106
+ raise RuntimeError(
107
+ f"Unsupported architecture: {machine}\n"
108
+ f"clang-tool-chain currently supports: x86_64 (AMD64) and ARM64\n"
109
+ f"Your architecture: {machine}\n"
110
+ f"Supported architectures:\n"
111
+ f" - x86_64, amd64 (Intel/AMD 64-bit)\n"
112
+ f" - aarch64, arm64 (ARM 64-bit)\n"
113
+ f"If you believe this architecture should be supported, please report this at:\n"
114
+ f"https://github.com/zackees/clang-tool-chain/issues"
115
+ )
116
+
117
+ logger.info(f"Platform detected: {platform_name}/{arch}")
118
+ return platform_name, arch
119
+
120
+
121
+ def get_assets_dir() -> Path:
122
+ """
123
+ Get the path to the assets directory containing LLVM binaries.
124
+
125
+ Returns:
126
+ Path to the assets directory
127
+ """
128
+ # Get the package directory
129
+ package_dir = Path(__file__).parent
130
+
131
+ # Assets should be in the project root (two levels up from package)
132
+ project_root = package_dir.parent.parent
133
+ assets_dir = project_root / "assets"
134
+
135
+ return assets_dir
136
+
137
+
138
+ def get_platform_binary_dir() -> Path:
139
+ """
140
+ Get the directory containing binaries for the current platform.
141
+
142
+ This function ensures the toolchain is downloaded before returning the path.
143
+
144
+ Returns:
145
+ Path to the platform-specific binary directory
146
+
147
+ Raises:
148
+ RuntimeError: If the platform is not supported or binaries cannot be installed
149
+ """
150
+ logger.info("Getting platform binary directory")
151
+ platform_name, arch = get_platform_info()
152
+
153
+ # Ensure toolchain is downloaded and installed
154
+ logger.info(f"Ensuring toolchain is available for {platform_name}/{arch}")
155
+ downloader.ensure_toolchain(platform_name, arch)
156
+
157
+ # Get the installation directory
158
+ install_dir = downloader.get_install_dir(platform_name, arch)
159
+ bin_dir = install_dir / "bin"
160
+ logger.debug(f"Binary directory: {bin_dir}")
161
+
162
+ if not bin_dir.exists():
163
+ logger.error(f"Binary directory does not exist: {bin_dir}")
164
+ # Get directory listing for debugging
165
+ dir_listing = _get_toolchain_directory_listing(platform_name)
166
+
167
+ raise RuntimeError(
168
+ f"Binaries not found for {platform_name}-{arch}\n"
169
+ f"Expected location: {bin_dir}\n"
170
+ f"\n"
171
+ f"Directory structure of ~/.clang-tool-chain (2 levels deep):\n"
172
+ f"{dir_listing}\n"
173
+ f"\n"
174
+ f"The toolchain download may have failed. Please try again or report this issue at:\n"
175
+ f"https://github.com/zackees/clang-tool-chain/issues"
176
+ )
177
+
178
+ logger.info(f"Binary directory found: {bin_dir}")
179
+ return bin_dir
180
+
181
+
182
+ def find_tool_binary(tool_name: str) -> Path:
183
+ """
184
+ Find the path to a specific tool binary.
185
+
186
+ Args:
187
+ tool_name: Name of the tool (e.g., "clang", "llvm-ar")
188
+
189
+ Returns:
190
+ Path to the tool binary
191
+
192
+ Raises:
193
+ RuntimeError: If the tool binary is not found
194
+ """
195
+ logger.info(f"Finding binary for tool: {tool_name}")
196
+ bin_dir = get_platform_binary_dir()
197
+ platform_name, _ = get_platform_info()
198
+
199
+ # Add .exe extension on Windows
200
+ tool_path = bin_dir / f"{tool_name}.exe" if platform_name == "win" else bin_dir / tool_name
201
+ logger.debug(f"Looking for tool at: {tool_path}")
202
+
203
+ if not tool_path.exists():
204
+ logger.warning(f"Tool not found at primary location: {tool_path}")
205
+ # Try alternative names for some tools
206
+ alternatives = {
207
+ "lld": ["lld-link", "ld.lld"],
208
+ "clang": ["clang++", "clang-cpp"],
209
+ "lld-link": ["lld", "ld.lld"],
210
+ "ld.lld": ["lld", "lld-link"],
211
+ }
212
+
213
+ if tool_name in alternatives:
214
+ logger.debug(f"Trying alternative names for {tool_name}: {alternatives[tool_name]}")
215
+ for alt_name in alternatives[tool_name]:
216
+ alt_path = bin_dir / f"{alt_name}.exe" if platform_name == "win" else bin_dir / alt_name
217
+ logger.debug(f"Checking alternative: {alt_path}")
218
+
219
+ if alt_path.exists():
220
+ logger.info(f"Found alternative tool at: {alt_path}")
221
+ return alt_path
222
+
223
+ # List available tools
224
+ available_tools = [f.stem for f in bin_dir.iterdir() if f.is_file()]
225
+ logger.error(f"Tool '{tool_name}' not found. Available tools: {', '.join(sorted(available_tools)[:20])}")
226
+
227
+ # Get directory listing for debugging
228
+ dir_listing = _get_toolchain_directory_listing(platform_name)
229
+
230
+ raise RuntimeError(
231
+ f"Tool '{tool_name}' not found\n"
232
+ f"Expected location: {tool_path}\n"
233
+ f"\n"
234
+ f"This tool may not be included in your LLVM installation.\n"
235
+ f"\n"
236
+ f"Available tools in {bin_dir.name}/:\n"
237
+ f" {', '.join(sorted(available_tools)[:20])}\n"
238
+ f" {'... and more' if len(available_tools) > 20 else ''}\n"
239
+ f"\n"
240
+ f"Directory structure of ~/.clang-tool-chain (2 levels deep):\n"
241
+ f"{dir_listing}\n"
242
+ f"\n"
243
+ f"Troubleshooting:\n"
244
+ f" - Verify the tool name is correct\n"
245
+ f" - Check if the tool is part of LLVM {tool_name}\n"
246
+ f" - Re-download binaries: python scripts/download_binaries.py\n"
247
+ f" - Report issue: https://github.com/zackees/clang-tool-chain/issues"
248
+ )
249
+
250
+ logger.info(f"Tool binary found: {tool_path}")
251
+ return tool_path
252
+
253
+
254
+ def find_sccache_binary() -> str:
255
+ """
256
+ Find the sccache binary in PATH.
257
+
258
+ Returns:
259
+ Path to the sccache binary
260
+
261
+ Raises:
262
+ RuntimeError: If sccache is not found in PATH
263
+ """
264
+ sccache_path = shutil.which("sccache")
265
+
266
+ if sccache_path is None:
267
+ raise RuntimeError(
268
+ "sccache not found in PATH\n"
269
+ "\n"
270
+ "sccache is required to use the sccache wrapper commands.\n"
271
+ "\n"
272
+ "Installation options:\n"
273
+ " - pip install clang-tool-chain[sccache]\n"
274
+ " - cargo install sccache\n"
275
+ " - Download from: https://github.com/mozilla/sccache/releases\n"
276
+ " - Linux: apt install sccache / yum install sccache\n"
277
+ " - macOS: brew install sccache\n"
278
+ "\n"
279
+ "After installation, ensure sccache is in your PATH.\n"
280
+ "Verify with: sccache --version"
281
+ )
282
+
283
+ return sccache_path
284
+
285
+
286
+ def _detect_windows_sdk() -> dict[str, str] | None:
287
+ """
288
+ Detect Windows SDK installation via environment variables.
289
+
290
+ This function checks for Visual Studio and Windows SDK environment variables
291
+ that are typically set by vcvars*.bat or Visual Studio Developer Command Prompt.
292
+
293
+ Returns:
294
+ Dictionary with SDK information if found, None otherwise.
295
+ Dictionary keys: 'sdk_dir', 'vc_tools_dir', 'sdk_version' (if available)
296
+
297
+ Note:
298
+ This function only checks environment variables. It does not search the
299
+ registry or filesystem for SDK installations. The goal is to detect if
300
+ the user has already set up their Visual Studio environment.
301
+ """
302
+ sdk_info = {}
303
+
304
+ # Check for Windows SDK environment variables
305
+ # These are set by vcvarsall.bat and similar VS setup scripts
306
+ sdk_dir = os.environ.get("WindowsSdkDir") or os.environ.get("WindowsSDKDir") # noqa: SIM112
307
+ if sdk_dir:
308
+ sdk_info["sdk_dir"] = sdk_dir
309
+ logger.debug(f"Windows SDK found via environment: {sdk_dir}")
310
+
311
+ # Check for Universal CRT SDK (required for C runtime)
312
+ ucrt_sdk_dir = os.environ.get("UniversalCRTSdkDir") # noqa: SIM112
313
+ if ucrt_sdk_dir:
314
+ sdk_info["ucrt_dir"] = ucrt_sdk_dir
315
+ logger.debug(f"Universal CRT SDK found: {ucrt_sdk_dir}")
316
+
317
+ # Check for VC Tools (MSVC compiler toolchain)
318
+ vc_tools_dir = os.environ.get("VCToolsInstallDir") # noqa: SIM112
319
+ if vc_tools_dir:
320
+ sdk_info["vc_tools_dir"] = vc_tools_dir
321
+ logger.debug(f"VC Tools found: {vc_tools_dir}")
322
+
323
+ # Check for VS installation directory
324
+ vs_install_dir = os.environ.get("VSINSTALLDIR")
325
+ if vs_install_dir:
326
+ sdk_info["vs_install_dir"] = vs_install_dir
327
+ logger.debug(f"Visual Studio installation found: {vs_install_dir}")
328
+
329
+ # Check for Windows SDK version
330
+ sdk_version = os.environ.get("WindowsSDKVersion") # noqa: SIM112
331
+ if sdk_version:
332
+ sdk_info["sdk_version"] = sdk_version.rstrip("\\") # Remove trailing backslash if present
333
+ logger.debug(f"Windows SDK version: {sdk_version}")
334
+
335
+ # Return SDK info if we found at least the SDK directory or VC tools
336
+ if sdk_info:
337
+ logger.info(f"Windows SDK detected: {', '.join(sdk_info.keys())}")
338
+ return sdk_info
339
+
340
+ logger.debug("Windows SDK not detected in environment variables")
341
+ return None
342
+
343
+
344
+ def _print_msvc_sdk_warning() -> None:
345
+ """
346
+ Print a helpful warning message to stderr when Windows SDK is not detected.
347
+
348
+ This is called when MSVC target is being used but we cannot detect the
349
+ Windows SDK via environment variables. The compilation may still succeed
350
+ if clang can find the SDK automatically, or it may fail with missing
351
+ headers/libraries errors.
352
+ """
353
+ print("\n" + "=" * 70, file=sys.stderr)
354
+ print("⚠️ Windows SDK Not Detected in Environment", file=sys.stderr)
355
+ print("=" * 70, file=sys.stderr)
356
+ print("\nThe MSVC target requires Windows SDK for system headers and libraries.", file=sys.stderr)
357
+ print("\nNo SDK environment variables found. This may mean:", file=sys.stderr)
358
+ print(" • Visual Studio or Windows SDK is not installed", file=sys.stderr)
359
+ print(" • VS Developer Command Prompt is not being used", file=sys.stderr)
360
+ print(" • Environment variables are not set (vcvarsall.bat not run)", file=sys.stderr)
361
+ print("\n" + "-" * 70, file=sys.stderr)
362
+ print("Recommendation: Set up Visual Studio environment", file=sys.stderr)
363
+ print("-" * 70, file=sys.stderr)
364
+ print("\nOption 1: Use Visual Studio Developer Command Prompt", file=sys.stderr)
365
+ print(" • Search for 'Developer Command Prompt' in Start Menu", file=sys.stderr)
366
+ print(" • Run your build commands from that prompt", file=sys.stderr)
367
+ print("\nOption 2: Run vcvarsall.bat in your current shell", file=sys.stderr)
368
+ print(" • Typical location:", file=sys.stderr)
369
+ print(
370
+ " C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Auxiliary\\Build\\vcvarsall.bat",
371
+ file=sys.stderr,
372
+ )
373
+ print(" • Run: vcvarsall.bat x64", file=sys.stderr)
374
+ print("\nOption 3: Install Visual Studio or Windows SDK", file=sys.stderr)
375
+ print(" • Visual Studio: https://visualstudio.microsoft.com/downloads/", file=sys.stderr)
376
+ print(" • Windows SDK only: https://developer.microsoft.com/windows/downloads/windows-sdk/", file=sys.stderr)
377
+ print("\n" + "-" * 70, file=sys.stderr)
378
+ print("Alternative: Use GNU ABI (MinGW) instead of MSVC", file=sys.stderr)
379
+ print("-" * 70, file=sys.stderr)
380
+ print("\nIf you don't need MSVC compatibility, use the default commands:", file=sys.stderr)
381
+ print(" • clang-tool-chain-c (uses GNU ABI, no SDK required)", file=sys.stderr)
382
+ print(" • clang-tool-chain-cpp (uses GNU ABI, no SDK required)", file=sys.stderr)
383
+ print("\n" + "=" * 70, file=sys.stderr)
384
+ print("Clang will attempt to find Windows SDK automatically...", file=sys.stderr)
385
+ print("=" * 70 + "\n", file=sys.stderr)
386
+
387
+
388
+ def _print_macos_sdk_error(reason: str) -> None:
389
+ """
390
+ Print a helpful error message to stderr when macOS SDK detection fails.
391
+
392
+ This is called when compilation is about to proceed without SDK detection,
393
+ which will likely cause 'stdio.h' or 'iostream' not found errors.
394
+
395
+ Args:
396
+ reason: Brief description of why SDK detection failed
397
+ """
398
+ print("\n" + "=" * 70, file=sys.stderr)
399
+ print("⚠️ macOS SDK Detection Failed", file=sys.stderr)
400
+ print("=" * 70, file=sys.stderr)
401
+ print(f"\nReason: {reason}", file=sys.stderr)
402
+ print("\nYour compilation may fail with errors like:", file=sys.stderr)
403
+ print(" fatal error: 'stdio.h' file not found", file=sys.stderr)
404
+ print(" fatal error: 'iostream' file not found", file=sys.stderr)
405
+ print("\n" + "-" * 70, file=sys.stderr)
406
+ print("Solution: Install Xcode Command Line Tools", file=sys.stderr)
407
+ print("-" * 70, file=sys.stderr)
408
+ print("\nRun this command in your terminal:", file=sys.stderr)
409
+ print("\n \033[1;36mxcode-select --install\033[0m", file=sys.stderr)
410
+ print("\nThen try compiling again.", file=sys.stderr)
411
+ print("\n" + "-" * 70, file=sys.stderr)
412
+ print("Alternative Solutions:", file=sys.stderr)
413
+ print("-" * 70, file=sys.stderr)
414
+ print("\n1. Specify SDK path manually:", file=sys.stderr)
415
+ print(" clang-tool-chain-c -isysroot /Library/Developer/.../MacOSX.sdk file.c", file=sys.stderr)
416
+ print("\n2. Set SDKROOT environment variable:", file=sys.stderr)
417
+ print(" export SDKROOT=$(xcrun --show-sdk-path) # if xcrun works", file=sys.stderr)
418
+ print("\n3. Use freestanding compilation (no standard library):", file=sys.stderr)
419
+ print(" clang-tool-chain-c -ffreestanding -nostdlib file.c", file=sys.stderr)
420
+ print("\n4. Disable automatic SDK detection:", file=sys.stderr)
421
+ print(" export CLANG_TOOL_CHAIN_NO_SYSROOT=1", file=sys.stderr)
422
+ print(" # Then specify SDK manually with -isysroot", file=sys.stderr)
423
+ print("\n" + "=" * 70, file=sys.stderr)
424
+ print("More info: https://github.com/zackees/clang-tool-chain#macos-sdk-detection-automatic", file=sys.stderr)
425
+ print("=" * 70 + "\n", file=sys.stderr)
426
+
427
+
428
+ def _add_macos_sysroot_if_needed(args: list[str]) -> list[str]:
429
+ """
430
+ Add -isysroot flag for macOS if needed to find system headers.
431
+
432
+ On macOS, system headers (like stdio.h, iostream) are NOT in /usr/include.
433
+ Instead, they're only available in SDK bundles provided by Xcode or Command Line Tools.
434
+ Standalone clang binaries cannot automatically find these headers without help.
435
+
436
+ This function implements LLVM's official three-tier SDK detection strategy
437
+ (see LLVM patch D136315: https://reviews.llvm.org/D136315):
438
+ 1. Explicit -isysroot flag (user override)
439
+ 2. SDKROOT environment variable (Xcode/xcrun standard)
440
+ 3. Automatic xcrun --show-sdk-path (fallback detection)
441
+
442
+ The function automatically detects the macOS SDK path and adds it to
443
+ the compiler arguments, unless:
444
+ - User has disabled it via CLANG_TOOL_CHAIN_NO_SYSROOT=1
445
+ - User has already specified -isysroot in the arguments
446
+ - SDKROOT environment variable is set (will be used by clang automatically)
447
+ - User specified flags indicating freestanding/no stdlib compilation:
448
+ -nostdinc, -nostdinc++, -nostdlib, -ffreestanding
449
+
450
+ Args:
451
+ args: Original compiler arguments
452
+
453
+ Returns:
454
+ Modified arguments with -isysroot prepended if needed
455
+
456
+ References:
457
+ - LLVM D136315: Try to guess SDK root with xcrun when unspecified
458
+ - Apple no longer ships headers in /usr/include since macOS 10.14 Mojave
459
+ """
460
+ # Check if user wants to disable automatic sysroot
461
+ if os.environ.get("CLANG_TOOL_CHAIN_NO_SYSROOT") == "1":
462
+ return args
463
+
464
+ # Check if SDKROOT is already set (standard macOS environment variable)
465
+ if "SDKROOT" in os.environ:
466
+ return args
467
+
468
+ # Check if user already specified -isysroot
469
+ if "-isysroot" in args:
470
+ return args
471
+
472
+ # Check for flags that indicate freestanding or no-stdlib compilation
473
+ # In these cases, the user explicitly doesn't want system headers/libraries
474
+ no_sysroot_flags = {"-nostdinc", "-nostdinc++", "-nostdlib", "-ffreestanding"}
475
+ if any(flag in args for flag in no_sysroot_flags):
476
+ return args
477
+
478
+ # Try to detect the SDK path using xcrun
479
+ try:
480
+ result = subprocess.run(
481
+ ["xcrun", "--show-sdk-path"],
482
+ capture_output=True,
483
+ text=True,
484
+ check=True,
485
+ timeout=5,
486
+ )
487
+ sdk_path = result.stdout.strip()
488
+
489
+ if sdk_path and Path(sdk_path).exists():
490
+ # Prepend -isysroot to arguments
491
+ logger.info(f"macOS SDK detected: {sdk_path}")
492
+ return ["-isysroot", sdk_path] + args
493
+ else:
494
+ # xcrun succeeded but returned invalid path
495
+ logger.warning(f"xcrun returned invalid SDK path: {sdk_path}")
496
+ _print_macos_sdk_error("xcrun returned invalid SDK path")
497
+ return args
498
+
499
+ except FileNotFoundError:
500
+ # xcrun command not found - Command Line Tools likely not installed
501
+ logger.error("xcrun command not found - Xcode Command Line Tools may not be installed")
502
+ _print_macos_sdk_error("xcrun command not found")
503
+ return args
504
+
505
+ except subprocess.CalledProcessError as e:
506
+ # xcrun failed with non-zero exit code
507
+ stderr_output = e.stderr.strip() if e.stderr else "No error output"
508
+ logger.error(f"xcrun failed: {stderr_output}")
509
+ _print_macos_sdk_error(f"xcrun failed: {stderr_output}")
510
+ return args
511
+
512
+ except subprocess.TimeoutExpired:
513
+ # xcrun took too long to respond
514
+ logger.warning("xcrun command timed out")
515
+ return args
516
+
517
+ except Exception as e:
518
+ # Unexpected error
519
+ logger.warning(f"Unexpected error detecting SDK: {e}")
520
+ return args
521
+
522
+
523
+ def _should_use_gnu_abi(platform_name: str, args: list[str]) -> bool:
524
+ """
525
+ Determine if GNU ABI should be used based on platform and arguments.
526
+
527
+ Windows defaults to GNU ABI (MinGW) in v2.0+ for cross-platform consistency.
528
+ This matches the approach of zig cc and ensures consistent C++ ABI across platforms.
529
+
530
+ Args:
531
+ platform_name: Platform name ("win", "linux", "darwin")
532
+ args: Command-line arguments
533
+
534
+ Returns:
535
+ True if GNU ABI should be used (Windows + no explicit target), False otherwise
536
+ """
537
+ # Non-Windows always uses default (which is GNU-like anyway)
538
+ if platform_name != "win":
539
+ return False
540
+
541
+ # Check if user explicitly specified target
542
+ args_str = " ".join(args)
543
+ if "--target=" in args_str or "--target " in args_str:
544
+ # User specified target explicitly, don't override
545
+ logger.debug("User specified explicit target, skipping GNU ABI injection")
546
+ return False
547
+
548
+ # Windows defaults to GNU ABI in v2.0+
549
+ logger.debug("Windows detected without explicit target, will use GNU ABI")
550
+ return True
551
+
552
+
553
+ def _get_gnu_target_args(platform_name: str, arch: str) -> list[str]:
554
+ """
555
+ Get GNU ABI target arguments for Windows.
556
+
557
+ This function ensures the MinGW sysroot is installed and returns
558
+ the necessary compiler arguments to use GNU ABI instead of MSVC ABI.
559
+
560
+ Args:
561
+ platform_name: Platform name
562
+ arch: Architecture
563
+
564
+ Returns:
565
+ List of additional compiler arguments for GNU ABI
566
+
567
+ Raises:
568
+ RuntimeError: If MinGW sysroot installation fails or is not found
569
+ """
570
+ if platform_name != "win":
571
+ return []
572
+
573
+ logger.info(f"Setting up GNU ABI for Windows {arch}")
574
+
575
+ # Ensure MinGW sysroot is installed
576
+ try:
577
+ sysroot_dir = downloader.ensure_mingw_sysroot_installed(platform_name, arch)
578
+ logger.debug(f"MinGW sysroot installed at: {sysroot_dir}")
579
+ except Exception as e:
580
+ logger.error(f"Failed to install MinGW sysroot: {e}")
581
+ raise RuntimeError(
582
+ f"Failed to install MinGW sysroot for Windows GNU ABI support\n"
583
+ f"Error: {e}\n"
584
+ f"\n"
585
+ f"This is required for GNU ABI support on Windows.\n"
586
+ f"If this persists, please report at:\n"
587
+ f"https://github.com/zackees/clang-tool-chain/issues"
588
+ ) from e
589
+
590
+ # Determine target triple and sysroot path
591
+ if arch == "x86_64":
592
+ target = "x86_64-w64-mingw32"
593
+ elif arch == "arm64":
594
+ target = "aarch64-w64-mingw32"
595
+ else:
596
+ raise ValueError(f"Unsupported architecture for MinGW: {arch}")
597
+
598
+ # The sysroot is the directory containing include/ and the target subdirectory
599
+ sysroot_path = sysroot_dir
600
+ if not sysroot_path.exists():
601
+ logger.error(f"MinGW sysroot not found at expected location: {sysroot_path}")
602
+ raise RuntimeError(
603
+ f"MinGW sysroot not found: {sysroot_path}\n"
604
+ f"The sysroot was downloaded but the expected directory is missing.\n"
605
+ f"Please report this issue at:\n"
606
+ f"https://github.com/zackees/clang-tool-chain/issues"
607
+ )
608
+
609
+ logger.info(f"Using GNU target: {target} with sysroot: {sysroot_path}")
610
+
611
+ # Check if resource directory exists in the sysroot
612
+ # The archive should contain lib/clang/<version>/ with resource headers
613
+ resource_dir = sysroot_path / "lib" / "clang"
614
+ resource_dir_arg = []
615
+ if resource_dir.exists():
616
+ # Find the version directory (should be only one, e.g., "21")
617
+ version_dirs = [d for d in resource_dir.iterdir() if d.is_dir()]
618
+ if version_dirs:
619
+ # Use the first (and should be only) version directory
620
+ clang_version_dir = version_dirs[0]
621
+ resource_include = clang_version_dir / "include"
622
+ if resource_include.exists():
623
+ logger.info(f"Found clang resource directory at: {clang_version_dir}")
624
+ # Use -resource-dir to tell clang where to find its builtin headers
625
+ # This makes clang look in <resource-dir>/include/ for headers like stddef.h, mm_malloc.h
626
+ resource_dir_arg = [f"-resource-dir={clang_version_dir}"]
627
+ else:
628
+ logger.warning(f"Resource include directory not found: {resource_include}")
629
+ else:
630
+ logger.warning(f"No version directories found in: {resource_dir}")
631
+ else:
632
+ logger.warning(f"Resource directory not found: {resource_dir}")
633
+
634
+ # Add -stdlib=libc++ to use the libc++ standard library included in the sysroot
635
+ # Add -fuse-ld=lld to use LLVM's linker instead of system ld
636
+ # Add -rtlib=compiler-rt to use LLVM's compiler-rt instead of libgcc
637
+ # Add --unwindlib=libunwind to use LLVM's libunwind instead of libgcc_s
638
+ # Add -static-libgcc -static-libstdc++ to link runtime libraries statically
639
+ # This avoids DLL dependency issues at runtime
640
+ return [
641
+ f"--target={target}",
642
+ f"--sysroot={sysroot_path}",
643
+ "-stdlib=libc++",
644
+ "-rtlib=compiler-rt",
645
+ "-fuse-ld=lld",
646
+ "--unwindlib=libunwind",
647
+ "-static-libgcc",
648
+ "-static-libstdc++",
649
+ ] + resource_dir_arg
650
+
651
+
652
+ def _should_use_msvc_abi(platform_name: str, args: list[str]) -> bool:
653
+ """
654
+ Determine if MSVC ABI should be used based on platform and arguments.
655
+
656
+ MSVC ABI is explicitly requested via the *-msvc variant commands.
657
+ Unlike GNU ABI (which is the Windows default), MSVC ABI is opt-in.
658
+
659
+ This function checks if the user has explicitly provided a --target flag.
660
+ If so, we respect the user's choice and don't inject MSVC target.
661
+
662
+ Args:
663
+ platform_name: Platform name ("win", "linux", "darwin")
664
+ args: Command-line arguments
665
+
666
+ Returns:
667
+ True if MSVC ABI should be used (Windows + no explicit target), False otherwise
668
+ """
669
+ # MSVC ABI only applies to Windows
670
+ if platform_name != "win":
671
+ logger.debug("Not Windows platform, MSVC ABI not applicable")
672
+ return False
673
+
674
+ # Check if user explicitly specified target
675
+ args_str = " ".join(args)
676
+ if "--target=" in args_str or "--target " in args_str:
677
+ # User specified target explicitly, don't override
678
+ logger.debug("User specified explicit target, skipping MSVC ABI injection")
679
+ return False
680
+
681
+ # MSVC variant was requested and no user override
682
+ logger.debug("MSVC ABI will be used (no user target override)")
683
+ return True
684
+
685
+
686
+ def _get_msvc_target_args(platform_name: str, arch: str) -> list[str]:
687
+ """
688
+ Get MSVC ABI target arguments for Windows.
689
+
690
+ This function returns the necessary compiler arguments to use MSVC ABI
691
+ instead of GNU ABI. It also detects Windows SDK availability and shows
692
+ helpful warnings if the SDK is not found in environment variables.
693
+
694
+ Args:
695
+ platform_name: Platform name
696
+ arch: Architecture
697
+
698
+ Returns:
699
+ List of additional compiler arguments for MSVC ABI (just --target)
700
+
701
+ Note:
702
+ Unlike GNU ABI which requires downloading a MinGW sysroot, MSVC ABI
703
+ relies on the system's Visual Studio or Windows SDK installation.
704
+ We detect SDK presence via environment variables and warn if not found,
705
+ but still return the target triple and let clang attempt its own SDK detection.
706
+ """
707
+ if platform_name != "win":
708
+ return []
709
+
710
+ logger.info(f"Setting up MSVC ABI for Windows {arch}")
711
+
712
+ # Detect Windows SDK and warn if not found
713
+ sdk_info = _detect_windows_sdk()
714
+ if sdk_info:
715
+ logger.info(f"Windows SDK detected with keys: {', '.join(sdk_info.keys())}")
716
+ else:
717
+ logger.warning("Windows SDK not detected in environment variables")
718
+ # Show helpful warning about SDK requirements
719
+ _print_msvc_sdk_warning()
720
+
721
+ # Determine target triple for MSVC ABI
722
+ if arch == "x86_64":
723
+ target = "x86_64-pc-windows-msvc"
724
+ elif arch == "arm64":
725
+ target = "aarch64-pc-windows-msvc"
726
+ else:
727
+ raise ValueError(f"Unsupported architecture for MSVC: {arch}")
728
+
729
+ logger.info(f"Using MSVC target: {target}")
730
+
731
+ # Return just the target triple
732
+ # Clang will automatically:
733
+ # - Select lld-link as the linker (MSVC-compatible)
734
+ # - Use MSVC name mangling for C++
735
+ # - Attempt to find Windows SDK via its own detection logic
736
+ return [f"--target={target}"]
737
+
738
+
739
+ def execute_tool(tool_name: str, args: list[str] | None = None, use_msvc: bool = False) -> NoReturn:
740
+ """
741
+ Execute a tool with the given arguments and exit with its return code.
742
+
743
+ This function does not return - it replaces the current process with
744
+ the tool process (on Unix) or exits with the tool's return code (on Windows).
745
+
746
+ Args:
747
+ tool_name: Name of the tool to execute
748
+ args: Arguments to pass to the tool (defaults to sys.argv[1:])
749
+ use_msvc: If True on Windows, skip GNU ABI injection (use MSVC target)
750
+
751
+ Raises:
752
+ RuntimeError: If the tool cannot be found or executed
753
+
754
+ Environment Variables:
755
+ SDKROOT: Custom SDK path to use (macOS, standard macOS variable)
756
+ CLANG_TOOL_CHAIN_NO_SYSROOT: Set to '1' to disable automatic -isysroot injection (macOS)
757
+ """
758
+ if args is None:
759
+ args = sys.argv[1:]
760
+
761
+ logger.info(f"Executing tool: {tool_name} with {len(args)} arguments")
762
+ logger.debug(f"Arguments: {args}")
763
+
764
+ try:
765
+ tool_path = find_tool_binary(tool_name)
766
+ except RuntimeError as e:
767
+ logger.error(f"Failed to find tool binary: {e}")
768
+ print(f"\n{'='*60}", file=sys.stderr)
769
+ print("clang-tool-chain Error", file=sys.stderr)
770
+ print(f"{'='*60}", file=sys.stderr)
771
+ print(f"{e}", file=sys.stderr)
772
+ print(f"{'='*60}\n", file=sys.stderr)
773
+ sys.exit(1)
774
+
775
+ # Add macOS SDK path automatically for clang/clang++ if not already specified
776
+ platform_name, arch = get_platform_info()
777
+ if platform_name == "darwin" and tool_name in ("clang", "clang++"):
778
+ logger.debug("Checking if macOS sysroot needs to be added")
779
+ args = _add_macos_sysroot_if_needed(args)
780
+
781
+ # Add Windows GNU ABI target automatically for clang/clang++ if not MSVC variant
782
+ if not use_msvc and tool_name in ("clang", "clang++") and _should_use_gnu_abi(platform_name, args):
783
+ try:
784
+ gnu_args = _get_gnu_target_args(platform_name, arch)
785
+ args = gnu_args + args
786
+ logger.info(f"Using GNU ABI with args: {gnu_args}")
787
+ except Exception as e:
788
+ # If GNU setup fails, let the tool try anyway (may fail at compile time)
789
+ logger.error(f"Failed to set up GNU ABI: {e}")
790
+ print(f"\nWarning: Failed to set up Windows GNU ABI: {e}", file=sys.stderr)
791
+ print("Continuing with default target (may fail)...\n", file=sys.stderr)
792
+
793
+ # Add Windows MSVC ABI target for clang/clang++ when using MSVC variant
794
+ if use_msvc and tool_name in ("clang", "clang++") and _should_use_msvc_abi(platform_name, args):
795
+ try:
796
+ msvc_args = _get_msvc_target_args(platform_name, arch)
797
+ args = msvc_args + args
798
+ logger.info(f"Using MSVC ABI with args: {msvc_args}")
799
+ except Exception as e:
800
+ # If MSVC setup fails, let the tool try anyway (may fail at compile time)
801
+ logger.error(f"Failed to set up MSVC ABI: {e}")
802
+ print(f"\nWarning: Failed to set up Windows MSVC ABI: {e}", file=sys.stderr)
803
+ print("Continuing with default target (may fail)...\n", file=sys.stderr)
804
+
805
+ # Build command
806
+ cmd = [str(tool_path)] + args
807
+ logger.info(f"Executing command: {tool_path} (with {len(args)} args)")
808
+
809
+ # On Unix systems, we can use exec to replace the current process
810
+ # On Windows, we need to use subprocess and exit with the return code
811
+ platform_name, _ = get_platform_info()
812
+
813
+ if platform_name == "win":
814
+ logger.debug("Using Windows subprocess execution")
815
+ # Windows: use subprocess
816
+ try:
817
+ result = subprocess.run(cmd)
818
+ sys.exit(result.returncode)
819
+ except FileNotFoundError:
820
+ print(f"\n{'='*60}", file=sys.stderr)
821
+ print("clang-tool-chain Error", file=sys.stderr)
822
+ print(f"{'='*60}", file=sys.stderr)
823
+ print(f"Tool not found: {tool_path}", file=sys.stderr)
824
+ print("\nThe binary exists in the package but cannot be executed.", file=sys.stderr)
825
+ print("This may be a permission or compatibility issue.", file=sys.stderr)
826
+ print("\nTroubleshooting:", file=sys.stderr)
827
+ print(" - Verify the binary is compatible with your Windows version", file=sys.stderr)
828
+ print(" - Check Windows Defender or antivirus isn't blocking it", file=sys.stderr)
829
+ print(" - Report issue: https://github.com/zackees/clang-tool-chain/issues", file=sys.stderr)
830
+ print(f"{'='*60}\n", file=sys.stderr)
831
+ sys.exit(1)
832
+ except Exception as e:
833
+ print(f"\n{'='*60}", file=sys.stderr)
834
+ print("clang-tool-chain Error", file=sys.stderr)
835
+ print(f"{'='*60}", file=sys.stderr)
836
+ print(f"Error executing tool: {e}", file=sys.stderr)
837
+ print(f"\nUnexpected error while running: {tool_path}", file=sys.stderr)
838
+ print(f"Arguments: {args}", file=sys.stderr)
839
+ print("\nPlease report this issue at:", file=sys.stderr)
840
+ print("https://github.com/zackees/clang-tool-chain/issues", file=sys.stderr)
841
+ print(f"{'='*60}\n", file=sys.stderr)
842
+ sys.exit(1)
843
+ else:
844
+ logger.debug("Using Unix exec replacement")
845
+ # Unix: use exec to replace current process
846
+ try:
847
+ logger.info(f"Replacing process with: {tool_path}")
848
+ os.execv(str(tool_path), cmd)
849
+ except FileNotFoundError:
850
+ print(f"\n{'='*60}", file=sys.stderr)
851
+ print("clang-tool-chain Error", file=sys.stderr)
852
+ print(f"{'='*60}", file=sys.stderr)
853
+ print(f"Tool not found: {tool_path}", file=sys.stderr)
854
+ print("\nThe binary exists in the package but cannot be executed.", file=sys.stderr)
855
+ print("This may be a permission or compatibility issue.", file=sys.stderr)
856
+ print("\nTroubleshooting:", file=sys.stderr)
857
+ print(f" - Check file permissions: chmod +x {tool_path}", file=sys.stderr)
858
+ print(" - Verify the binary is compatible with your system", file=sys.stderr)
859
+ print(" - On macOS: Right-click > Open, then allow in Security settings", file=sys.stderr)
860
+ print(" - Report issue: https://github.com/zackees/clang-tool-chain/issues", file=sys.stderr)
861
+ print(f"{'='*60}\n", file=sys.stderr)
862
+ sys.exit(1)
863
+ except Exception as e:
864
+ print(f"\n{'='*60}", file=sys.stderr)
865
+ print("clang-tool-chain Error", file=sys.stderr)
866
+ print(f"{'='*60}", file=sys.stderr)
867
+ print(f"Error executing tool: {e}", file=sys.stderr)
868
+ print(f"\nUnexpected error while running: {tool_path}", file=sys.stderr)
869
+ print(f"Arguments: {args}", file=sys.stderr)
870
+ print("\nPlease report this issue at:", file=sys.stderr)
871
+ print("https://github.com/zackees/clang-tool-chain/issues", file=sys.stderr)
872
+ print(f"{'='*60}\n", file=sys.stderr)
873
+ sys.exit(1)
874
+
875
+
876
+ def run_tool(tool_name: str, args: list[str] | None = None, use_msvc: bool = False) -> int:
877
+ """
878
+ Run a tool with the given arguments and return its exit code.
879
+
880
+ Unlike execute_tool, this function returns to the caller with the
881
+ tool's exit code instead of exiting the process.
882
+
883
+ Args:
884
+ tool_name: Name of the tool to execute
885
+ args: Arguments to pass to the tool (defaults to sys.argv[1:])
886
+ use_msvc: If True on Windows, skip GNU ABI injection (use MSVC target)
887
+
888
+ Returns:
889
+ Exit code from the tool
890
+
891
+ Raises:
892
+ RuntimeError: If the tool cannot be found
893
+
894
+ Environment Variables:
895
+ SDKROOT: Custom SDK path to use (macOS, standard macOS variable)
896
+ CLANG_TOOL_CHAIN_NO_SYSROOT: Set to '1' to disable automatic -isysroot injection (macOS)
897
+ """
898
+ if args is None:
899
+ args = sys.argv[1:]
900
+
901
+ tool_path = find_tool_binary(tool_name)
902
+
903
+ # Add macOS SDK path automatically for clang/clang++ if not already specified
904
+ platform_name, arch = get_platform_info()
905
+ if platform_name == "darwin" and tool_name in ("clang", "clang++"):
906
+ logger.debug("Checking if macOS sysroot needs to be added")
907
+ args = _add_macos_sysroot_if_needed(args)
908
+
909
+ # Add Windows GNU ABI target automatically for clang/clang++ if not MSVC variant
910
+ if not use_msvc and tool_name in ("clang", "clang++") and _should_use_gnu_abi(platform_name, args):
911
+ try:
912
+ gnu_args = _get_gnu_target_args(platform_name, arch)
913
+ args = gnu_args + args
914
+ logger.info(f"Using GNU ABI with args: {gnu_args}")
915
+ except Exception as e:
916
+ # If GNU setup fails, let the tool try anyway (may fail at compile time)
917
+ logger.error(f"Failed to set up GNU ABI: {e}")
918
+ print(f"\nWarning: Failed to set up Windows GNU ABI: {e}", file=sys.stderr)
919
+ print("Continuing with default target (may fail)...\n", file=sys.stderr)
920
+
921
+ # Add Windows MSVC ABI target for clang/clang++ when using MSVC variant
922
+ if use_msvc and tool_name in ("clang", "clang++") and _should_use_msvc_abi(platform_name, args):
923
+ try:
924
+ msvc_args = _get_msvc_target_args(platform_name, arch)
925
+ args = msvc_args + args
926
+ logger.info(f"Using MSVC ABI with args: {msvc_args}")
927
+ except Exception as e:
928
+ # If MSVC setup fails, let the tool try anyway (may fail at compile time)
929
+ logger.error(f"Failed to set up MSVC ABI: {e}")
930
+ print(f"\nWarning: Failed to set up Windows MSVC ABI: {e}", file=sys.stderr)
931
+ print("Continuing with default target (may fail)...\n", file=sys.stderr)
932
+
933
+ # Build command
934
+ cmd = [str(tool_path)] + args
935
+
936
+ # Run the tool
937
+ try:
938
+ result = subprocess.run(cmd)
939
+ return result.returncode
940
+ except FileNotFoundError as err:
941
+ raise RuntimeError(f"Tool not found: {tool_path}") from err
942
+ except Exception as e:
943
+ raise RuntimeError(f"Error executing tool: {e}") from e
944
+
945
+
946
+ # Wrapper functions for specific tools
947
+ def clang_main() -> NoReturn:
948
+ """Entry point for clang wrapper (GNU ABI on Windows by default)."""
949
+ execute_tool("clang")
950
+
951
+
952
+ def clang_cpp_main() -> NoReturn:
953
+ """Entry point for clang++ wrapper (GNU ABI on Windows by default)."""
954
+ execute_tool("clang++")
955
+
956
+
957
+ def clang_msvc_main() -> NoReturn:
958
+ """Entry point for clang-tool-chain-c-msvc (MSVC ABI on Windows)."""
959
+ execute_tool("clang", use_msvc=True)
960
+
961
+
962
+ def clang_cpp_msvc_main() -> NoReturn:
963
+ """Entry point for clang-tool-chain-cpp-msvc (MSVC ABI on Windows)."""
964
+ execute_tool("clang++", use_msvc=True)
965
+
966
+
967
+ def lld_main() -> NoReturn:
968
+ """Entry point for lld linker wrapper."""
969
+ platform_name, _ = get_platform_info()
970
+ if platform_name == "win":
971
+ execute_tool("lld-link")
972
+ else:
973
+ execute_tool("lld")
974
+
975
+
976
+ def llvm_ar_main() -> NoReturn:
977
+ """Entry point for llvm-ar wrapper."""
978
+ execute_tool("llvm-ar")
979
+
980
+
981
+ def llvm_nm_main() -> NoReturn:
982
+ """Entry point for llvm-nm wrapper."""
983
+ execute_tool("llvm-nm")
984
+
985
+
986
+ def llvm_objdump_main() -> NoReturn:
987
+ """Entry point for llvm-objdump wrapper."""
988
+ execute_tool("llvm-objdump")
989
+
990
+
991
+ def llvm_objcopy_main() -> NoReturn:
992
+ """Entry point for llvm-objcopy wrapper."""
993
+ execute_tool("llvm-objcopy")
994
+
995
+
996
+ def llvm_ranlib_main() -> NoReturn:
997
+ """Entry point for llvm-ranlib wrapper."""
998
+ execute_tool("llvm-ranlib")
999
+
1000
+
1001
+ def llvm_strip_main() -> NoReturn:
1002
+ """Entry point for llvm-strip wrapper."""
1003
+ execute_tool("llvm-strip")
1004
+
1005
+
1006
+ def llvm_readelf_main() -> NoReturn:
1007
+ """Entry point for llvm-readelf wrapper."""
1008
+ execute_tool("llvm-readelf")
1009
+
1010
+
1011
+ def llvm_as_main() -> NoReturn:
1012
+ """Entry point for llvm-as wrapper."""
1013
+ execute_tool("llvm-as")
1014
+
1015
+
1016
+ def llvm_dis_main() -> NoReturn:
1017
+ """Entry point for llvm-dis wrapper."""
1018
+ execute_tool("llvm-dis")
1019
+
1020
+
1021
+ def clang_format_main() -> NoReturn:
1022
+ """Entry point for clang-format wrapper."""
1023
+ execute_tool("clang-format")
1024
+
1025
+
1026
+ def clang_tidy_main() -> NoReturn:
1027
+ """Entry point for clang-tidy wrapper."""
1028
+ execute_tool("clang-tidy")
1029
+
1030
+
1031
+ def build_main() -> NoReturn:
1032
+ """
1033
+ Entry point for build wrapper.
1034
+
1035
+ Simple build utility that compiles and links a C/C++ source file to an executable.
1036
+
1037
+ Usage:
1038
+ clang-tool-chain-build <source_file> <output_file> [additional_args...]
1039
+
1040
+ Examples:
1041
+ clang-tool-chain-build main.cpp main.exe
1042
+ clang-tool-chain-build main.c main -O2
1043
+ clang-tool-chain-build main.cpp app.exe -std=c++17 -Wall
1044
+ """
1045
+ args = sys.argv[1:]
1046
+
1047
+ if len(args) < 2:
1048
+ print("\n" + "=" * 60, file=sys.stderr)
1049
+ print("clang-tool-chain-build - Build Utility", file=sys.stderr)
1050
+ print("=" * 60, file=sys.stderr)
1051
+ print("Usage: clang-tool-chain-build <source_file> <output_file> [compiler_flags...]", file=sys.stderr)
1052
+ print("\nExamples:", file=sys.stderr)
1053
+ print(" clang-tool-chain-build main.cpp main.exe", file=sys.stderr)
1054
+ print(" clang-tool-chain-build main.c main -O2", file=sys.stderr)
1055
+ print(" clang-tool-chain-build main.cpp app.exe -std=c++17 -Wall", file=sys.stderr)
1056
+ print("\nArguments:", file=sys.stderr)
1057
+ print(" source_file - C/C++ source file to compile (.c, .cpp, .cc, .cxx)", file=sys.stderr)
1058
+ print(" output_file - Output executable file", file=sys.stderr)
1059
+ print(" compiler_flags - Optional additional compiler flags", file=sys.stderr)
1060
+ print("=" * 60 + "\n", file=sys.stderr)
1061
+ sys.exit(1)
1062
+
1063
+ source_file = args[0]
1064
+ output_file = args[1]
1065
+ additional_flags = args[2:] if len(args) > 2 else []
1066
+
1067
+ # Determine if this is C or C++ based on file extension
1068
+ source_path = Path(source_file)
1069
+ cpp_extensions = {".cpp", ".cc", ".cxx", ".C", ".c++"}
1070
+ is_cpp = source_path.suffix.lower() in cpp_extensions
1071
+
1072
+ # Choose the appropriate compiler
1073
+ compiler = "clang++" if is_cpp else "clang"
1074
+
1075
+ # Build the compiler command
1076
+ compiler_args = [source_file, "-o", output_file] + additional_flags
1077
+
1078
+ # Execute the compiler
1079
+ execute_tool(compiler, compiler_args)
1080
+
1081
+
1082
+ def _compute_file_hash(file_path: Path) -> str:
1083
+ """
1084
+ Compute SHA256 hash of a file.
1085
+
1086
+ Args:
1087
+ file_path: Path to the file to hash
1088
+
1089
+ Returns:
1090
+ Hexadecimal string representation of the SHA256 hash
1091
+ """
1092
+ sha256_hash = hashlib.sha256()
1093
+ with open(file_path, "rb") as f:
1094
+ # Read in chunks to handle large files efficiently
1095
+ for byte_block in iter(lambda: f.read(4096), b""):
1096
+ sha256_hash.update(byte_block)
1097
+ return sha256_hash.hexdigest()
1098
+
1099
+
1100
+ def build_run_main() -> NoReturn:
1101
+ """
1102
+ Entry point for build-run wrapper.
1103
+
1104
+ Simple build-and-run utility that:
1105
+ 1. Takes a C/C++ source file (e.g., src.cpp)
1106
+ 2. Compiles it to an executable (src or src.exe)
1107
+ 3. Runs the executable
1108
+
1109
+ With --cached flag:
1110
+ 1. Computes hash of source file
1111
+ 2. Checks if cached hash matches (stored in src.hash)
1112
+ 3. Skips compilation if hash matches and executable exists
1113
+ 4. Runs the executable
1114
+
1115
+ Usage:
1116
+ clang-tool-chain-build-run [--cached] <source_file> [compiler_flags...] [-- program_args...]
1117
+
1118
+ Examples:
1119
+ clang-tool-chain-build-run main.cpp
1120
+ clang-tool-chain-build-run --cached main.c
1121
+ clang-tool-chain-build-run --cached main.cpp -O2
1122
+ clang-tool-chain-build-run main.cpp -std=c++17 -Wall
1123
+ clang-tool-chain-build-run --cached main.cpp -- arg1 arg2 # Pass args to program
1124
+ """
1125
+ args = sys.argv[1:]
1126
+
1127
+ if len(args) < 1:
1128
+ print("\n" + "=" * 60, file=sys.stderr)
1129
+ print("clang-tool-chain-build-run - Build and Run Utility", file=sys.stderr)
1130
+ print("=" * 60, file=sys.stderr)
1131
+ print(
1132
+ "Usage: clang-tool-chain-build-run [--cached] <source_file> [compiler_flags...] [-- program_args...]",
1133
+ file=sys.stderr,
1134
+ )
1135
+ print("\nExamples:", file=sys.stderr)
1136
+ print(" clang-tool-chain-build-run main.cpp", file=sys.stderr)
1137
+ print(" clang-tool-chain-build-run --cached main.c", file=sys.stderr)
1138
+ print(" clang-tool-chain-build-run --cached main.cpp -O2", file=sys.stderr)
1139
+ print(" clang-tool-chain-build-run main.cpp -std=c++17 -Wall", file=sys.stderr)
1140
+ print(" clang-tool-chain-build-run --cached main.cpp -- arg1 arg2 # Pass args to program", file=sys.stderr)
1141
+ print("\nBehavior:", file=sys.stderr)
1142
+ print(" - Compiles source_file to an executable with the same base name", file=sys.stderr)
1143
+ print(" - On Windows: src.cpp -> src.exe", file=sys.stderr)
1144
+ print(" - On Unix: src.cpp -> src", file=sys.stderr)
1145
+ print(" - Runs the executable immediately after successful compilation", file=sys.stderr)
1146
+ print(" - Use '--' to separate compiler flags from program arguments", file=sys.stderr)
1147
+ print("\nCaching (--cached flag):", file=sys.stderr)
1148
+ print(" - Computes SHA256 hash of source file", file=sys.stderr)
1149
+ print(" - Stores hash in src.hash file", file=sys.stderr)
1150
+ print(" - Skips compilation if hash matches and executable exists", file=sys.stderr)
1151
+ print(" - Useful for quick development iterations", file=sys.stderr)
1152
+ print("\nArguments:", file=sys.stderr)
1153
+ print(" --cached - Enable hash-based compilation caching", file=sys.stderr)
1154
+ print(" source_file - C/C++ source file to compile (.c, .cpp, .cc, .cxx)", file=sys.stderr)
1155
+ print(" compiler_flags - Optional compiler flags (before '--')", file=sys.stderr)
1156
+ print(" program_args - Optional arguments to pass to the program (after '--')", file=sys.stderr)
1157
+ print("=" * 60 + "\n", file=sys.stderr)
1158
+ sys.exit(1)
1159
+
1160
+ # Check for --cached flag
1161
+ use_cache = False
1162
+ if args[0] == "--cached":
1163
+ use_cache = True
1164
+ args = args[1:]
1165
+
1166
+ if len(args) < 1:
1167
+ print("Error: source_file is required", file=sys.stderr)
1168
+ sys.exit(1)
1169
+
1170
+ # Split args into compiler flags and program args
1171
+ if "--" in args:
1172
+ separator_idx = args.index("--")
1173
+ compile_args = args[:separator_idx]
1174
+ program_args = args[separator_idx + 1 :]
1175
+ else:
1176
+ compile_args = args
1177
+ program_args = []
1178
+
1179
+ source_file = compile_args[0]
1180
+ compiler_flags = compile_args[1:] if len(compile_args) > 1 else []
1181
+
1182
+ # Determine output executable name from source file
1183
+ source_path = Path(source_file)
1184
+
1185
+ if not source_path.exists():
1186
+ print(f"Error: Source file not found: {source_file}", file=sys.stderr)
1187
+ sys.exit(1)
1188
+
1189
+ platform_name, _ = get_platform_info()
1190
+
1191
+ # Generate output filename: src.cpp -> src (or src.exe on Windows)
1192
+ if platform_name == "win":
1193
+ output_file = str(source_path.with_suffix(".exe"))
1194
+ else:
1195
+ output_file = str(source_path.with_suffix(""))
1196
+
1197
+ output_path = Path(output_file)
1198
+ hash_file = source_path.with_suffix(".hash")
1199
+
1200
+ # Determine if this is C or C++ based on file extension
1201
+ cpp_extensions = {".cpp", ".cc", ".cxx", ".C", ".c++"}
1202
+ is_cpp = source_path.suffix.lower() in cpp_extensions
1203
+
1204
+ # Choose the appropriate compiler
1205
+ compiler = "clang++" if is_cpp else "clang"
1206
+
1207
+ # Check cache if enabled
1208
+ should_compile = True
1209
+ if use_cache:
1210
+ print(f"Checking cache for {source_file}...", file=sys.stderr)
1211
+
1212
+ # Compute current hash
1213
+ current_hash = _compute_file_hash(source_path)
1214
+
1215
+ # Check if hash file exists and matches
1216
+ if hash_file.exists() and output_path.exists():
1217
+ try:
1218
+ stored_hash = hash_file.read_text().strip()
1219
+ if stored_hash == current_hash:
1220
+ print(f"Cache hit! Hash matches, skipping compilation.", file=sys.stderr)
1221
+ print(f"Using cached executable: {output_file}", file=sys.stderr)
1222
+ should_compile = False
1223
+ else:
1224
+ print(f"Cache miss: Hash mismatch, recompiling...", file=sys.stderr)
1225
+ except Exception as e:
1226
+ print(f"Warning: Could not read hash file: {e}", file=sys.stderr)
1227
+ print(f"Recompiling...", file=sys.stderr)
1228
+ else:
1229
+ if not output_path.exists():
1230
+ print(f"Cache miss: Executable not found, compiling...", file=sys.stderr)
1231
+ else:
1232
+ print(f"Cache miss: No hash file found, compiling...", file=sys.stderr)
1233
+
1234
+ # Compile if needed
1235
+ if should_compile:
1236
+ # Build the compiler command
1237
+ compiler_args = [source_file, "-o", output_file] + compiler_flags
1238
+
1239
+ print(f"Compiling: {source_file} -> {output_file}", file=sys.stderr)
1240
+
1241
+ # Run the compiler (returns exit code instead of calling sys.exit)
1242
+ exit_code = run_tool(compiler, compiler_args)
1243
+
1244
+ if exit_code != 0:
1245
+ print(f"\n{'='*60}", file=sys.stderr)
1246
+ print("Compilation failed", file=sys.stderr)
1247
+ print(f"{'='*60}\n", file=sys.stderr)
1248
+ sys.exit(exit_code)
1249
+
1250
+ # Update hash file if caching is enabled
1251
+ if use_cache:
1252
+ try:
1253
+ current_hash = _compute_file_hash(source_path)
1254
+ hash_file.write_text(current_hash)
1255
+ print(f"Updated cache hash: {hash_file}", file=sys.stderr)
1256
+ except Exception as e:
1257
+ print(f"Warning: Could not write hash file: {e}", file=sys.stderr)
1258
+
1259
+ print(f"\nRunning: {output_file}", file=sys.stderr)
1260
+ if program_args:
1261
+ print(f"Program arguments: {' '.join(program_args)}", file=sys.stderr)
1262
+ print("=" * 60, file=sys.stderr)
1263
+
1264
+ # Run the compiled executable
1265
+ try:
1266
+ # Use absolute path for Windows compatibility
1267
+ abs_output = output_path.absolute()
1268
+ result = subprocess.run([str(abs_output)] + program_args)
1269
+ sys.exit(result.returncode)
1270
+ except FileNotFoundError:
1271
+ print(f"\n{'='*60}", file=sys.stderr)
1272
+ print("Execution Error", file=sys.stderr)
1273
+ print(f"{'='*60}", file=sys.stderr)
1274
+ print(f"Compiled executable not found: {output_file}", file=sys.stderr)
1275
+ print("\nThe compilation appeared to succeed, but the output file cannot be found.", file=sys.stderr)
1276
+ print(f"{'='*60}\n", file=sys.stderr)
1277
+ sys.exit(1)
1278
+ except Exception as e:
1279
+ print(f"\n{'='*60}", file=sys.stderr)
1280
+ print("Execution Error", file=sys.stderr)
1281
+ print(f"{'='*60}", file=sys.stderr)
1282
+ print(f"Error running {output_file}: {e}", file=sys.stderr)
1283
+ print(f"{'='*60}\n", file=sys.stderr)
1284
+ sys.exit(1)
1285
+
1286
+
1287
+ # sccache wrapper functions
1288
+ def sccache_clang_main(use_msvc: bool = False) -> NoReturn:
1289
+ """
1290
+ Entry point for sccache + clang wrapper.
1291
+
1292
+ Args:
1293
+ use_msvc: If True on Windows, use MSVC ABI instead of GNU ABI
1294
+ """
1295
+ args = sys.argv[1:]
1296
+
1297
+ try:
1298
+ sccache_path = find_sccache_binary()
1299
+ clang_path = find_tool_binary("clang")
1300
+ except RuntimeError as e:
1301
+ print(f"\n{'='*60}", file=sys.stderr)
1302
+ print("clang-tool-chain Error", file=sys.stderr)
1303
+ print(f"{'='*60}", file=sys.stderr)
1304
+ print(f"{e}", file=sys.stderr)
1305
+ print(f"{'='*60}\n", file=sys.stderr)
1306
+ sys.exit(1)
1307
+
1308
+ # Add macOS SDK path automatically if needed
1309
+ platform_name, arch = get_platform_info()
1310
+ if platform_name == "darwin":
1311
+ args = _add_macos_sysroot_if_needed(args)
1312
+
1313
+ # Add Windows GNU ABI target automatically (if not using MSVC variant)
1314
+ if not use_msvc and _should_use_gnu_abi(platform_name, args):
1315
+ try:
1316
+ gnu_args = _get_gnu_target_args(platform_name, arch)
1317
+ args = gnu_args + args
1318
+ logger.info(f"Using GNU ABI with sccache: {gnu_args}")
1319
+ except Exception as e:
1320
+ logger.error(f"Failed to set up GNU ABI: {e}")
1321
+ print(f"\nWarning: Failed to set up Windows GNU ABI: {e}", file=sys.stderr)
1322
+ print("Continuing with default target (may fail)...\n", file=sys.stderr)
1323
+
1324
+ # Add Windows MSVC ABI target when using MSVC variant
1325
+ if use_msvc and _should_use_msvc_abi(platform_name, args):
1326
+ try:
1327
+ msvc_args = _get_msvc_target_args(platform_name, arch)
1328
+ args = msvc_args + args
1329
+ logger.info(f"Using MSVC ABI with sccache: {msvc_args}")
1330
+ except Exception as e:
1331
+ logger.error(f"Failed to set up MSVC ABI: {e}")
1332
+ print(f"\nWarning: Failed to set up Windows MSVC ABI: {e}", file=sys.stderr)
1333
+ print("Continuing with default target (may fail)...\n", file=sys.stderr)
1334
+
1335
+ # Build command: sccache <clang_path> <args>
1336
+ cmd = [sccache_path, str(clang_path)] + args
1337
+
1338
+ # Execute with platform-appropriate method
1339
+ platform_name, _ = get_platform_info()
1340
+
1341
+ if platform_name == "win":
1342
+ # Windows: use subprocess
1343
+ try:
1344
+ result = subprocess.run(cmd)
1345
+ sys.exit(result.returncode)
1346
+ except Exception as e:
1347
+ print(f"\n{'='*60}", file=sys.stderr)
1348
+ print("clang-tool-chain Error", file=sys.stderr)
1349
+ print(f"{'='*60}", file=sys.stderr)
1350
+ print(f"Error executing sccache: {e}", file=sys.stderr)
1351
+ print(f"{'='*60}\n", file=sys.stderr)
1352
+ sys.exit(1)
1353
+ else:
1354
+ # Unix: use exec to replace current process
1355
+ try:
1356
+ os.execv(sccache_path, cmd)
1357
+ except Exception as e:
1358
+ print(f"\n{'='*60}", file=sys.stderr)
1359
+ print("clang-tool-chain Error", file=sys.stderr)
1360
+ print(f"{'='*60}", file=sys.stderr)
1361
+ print(f"Error executing sccache: {e}", file=sys.stderr)
1362
+ print(f"{'='*60}\n", file=sys.stderr)
1363
+ sys.exit(1)
1364
+
1365
+
1366
+ def sccache_clang_cpp_main(use_msvc: bool = False) -> NoReturn:
1367
+ """
1368
+ Entry point for sccache + clang++ wrapper.
1369
+
1370
+ Args:
1371
+ use_msvc: If True on Windows, use MSVC ABI instead of GNU ABI
1372
+ """
1373
+ args = sys.argv[1:]
1374
+
1375
+ try:
1376
+ sccache_path = find_sccache_binary()
1377
+ clang_cpp_path = find_tool_binary("clang++")
1378
+ except RuntimeError as e:
1379
+ print(f"\n{'='*60}", file=sys.stderr)
1380
+ print("clang-tool-chain Error", file=sys.stderr)
1381
+ print(f"{'='*60}", file=sys.stderr)
1382
+ print(f"{e}", file=sys.stderr)
1383
+ print(f"{'='*60}\n", file=sys.stderr)
1384
+ sys.exit(1)
1385
+
1386
+ # Add macOS SDK path automatically if needed
1387
+ platform_name, arch = get_platform_info()
1388
+ if platform_name == "darwin":
1389
+ args = _add_macos_sysroot_if_needed(args)
1390
+
1391
+ # Add Windows GNU ABI target automatically (if not using MSVC variant)
1392
+ if not use_msvc and _should_use_gnu_abi(platform_name, args):
1393
+ try:
1394
+ gnu_args = _get_gnu_target_args(platform_name, arch)
1395
+ args = gnu_args + args
1396
+ logger.info(f"Using GNU ABI with sccache: {gnu_args}")
1397
+ except Exception as e:
1398
+ logger.error(f"Failed to set up GNU ABI: {e}")
1399
+ print(f"\nWarning: Failed to set up Windows GNU ABI: {e}", file=sys.stderr)
1400
+ print("Continuing with default target (may fail)...\n", file=sys.stderr)
1401
+
1402
+ # Add Windows MSVC ABI target when using MSVC variant
1403
+ if use_msvc and _should_use_msvc_abi(platform_name, args):
1404
+ try:
1405
+ msvc_args = _get_msvc_target_args(platform_name, arch)
1406
+ args = msvc_args + args
1407
+ logger.info(f"Using MSVC ABI with sccache: {msvc_args}")
1408
+ except Exception as e:
1409
+ logger.error(f"Failed to set up MSVC ABI: {e}")
1410
+ print(f"\nWarning: Failed to set up Windows MSVC ABI: {e}", file=sys.stderr)
1411
+ print("Continuing with default target (may fail)...\n", file=sys.stderr)
1412
+
1413
+ # Build command: sccache <clang++_path> <args>
1414
+ cmd = [sccache_path, str(clang_cpp_path)] + args
1415
+
1416
+ # Execute with platform-appropriate method
1417
+ platform_name, _ = get_platform_info()
1418
+
1419
+ if platform_name == "win":
1420
+ # Windows: use subprocess
1421
+ try:
1422
+ result = subprocess.run(cmd)
1423
+ sys.exit(result.returncode)
1424
+ except Exception as e:
1425
+ print(f"\n{'='*60}", file=sys.stderr)
1426
+ print("clang-tool-chain Error", file=sys.stderr)
1427
+ print(f"{'='*60}", file=sys.stderr)
1428
+ print(f"Error executing sccache: {e}", file=sys.stderr)
1429
+ print(f"{'='*60}\n", file=sys.stderr)
1430
+ sys.exit(1)
1431
+ else:
1432
+ # Unix: use exec to replace current process
1433
+ try:
1434
+ os.execv(sccache_path, cmd)
1435
+ except Exception as e:
1436
+ print(f"\n{'='*60}", file=sys.stderr)
1437
+ print("clang-tool-chain Error", file=sys.stderr)
1438
+ print(f"{'='*60}", file=sys.stderr)
1439
+ print(f"Error executing sccache: {e}", file=sys.stderr)
1440
+ print(f"{'='*60}\n", file=sys.stderr)
1441
+ sys.exit(1)
1442
+
1443
+
1444
+ # ============================================================================
1445
+ # IWYU (Include What You Use) Support
1446
+ # ============================================================================
1447
+
1448
+
1449
+ def get_iwyu_binary_dir() -> Path:
1450
+ """
1451
+ Get the binary directory for IWYU.
1452
+
1453
+ Returns:
1454
+ Path to the IWYU binary directory
1455
+
1456
+ Raises:
1457
+ RuntimeError: If binary directory is not found
1458
+ """
1459
+ platform_name, arch = get_platform_info()
1460
+ logger.info(f"Getting IWYU binary directory for {platform_name}/{arch}")
1461
+
1462
+ # Ensure IWYU is downloaded and installed
1463
+ logger.info(f"Ensuring IWYU is available for {platform_name}/{arch}")
1464
+ downloader.ensure_iwyu(platform_name, arch)
1465
+
1466
+ # Get the installation directory
1467
+ install_dir = downloader.get_iwyu_install_dir(platform_name, arch)
1468
+ bin_dir = install_dir / "bin"
1469
+ logger.debug(f"IWYU binary directory: {bin_dir}")
1470
+
1471
+ if not bin_dir.exists():
1472
+ logger.error(f"IWYU binary directory does not exist: {bin_dir}")
1473
+ raise RuntimeError(
1474
+ f"IWYU binaries not found for {platform_name}-{arch}\n"
1475
+ f"Expected location: {bin_dir}\n"
1476
+ f"\n"
1477
+ f"The IWYU download may have failed. Please try again or report this issue at:\n"
1478
+ f"https://github.com/zackees/clang-tool-chain/issues"
1479
+ )
1480
+
1481
+ logger.info(f"IWYU binary directory found: {bin_dir}")
1482
+ return bin_dir
1483
+
1484
+
1485
+ def find_iwyu_tool(tool_name: str) -> Path:
1486
+ """
1487
+ Find the path to an IWYU tool.
1488
+
1489
+ Args:
1490
+ tool_name: Name of the tool (e.g., "include-what-you-use", "iwyu_tool.py")
1491
+
1492
+ Returns:
1493
+ Path to the tool
1494
+
1495
+ Raises:
1496
+ RuntimeError: If the tool is not found
1497
+ """
1498
+ logger.info(f"Finding IWYU tool: {tool_name}")
1499
+ bin_dir = get_iwyu_binary_dir()
1500
+ platform_name, _ = get_platform_info()
1501
+
1502
+ # Add .exe extension on Windows for the binary
1503
+ if tool_name == "include-what-you-use" and platform_name == "win":
1504
+ tool_path = bin_dir / f"{tool_name}.exe"
1505
+ else:
1506
+ tool_path = bin_dir / tool_name
1507
+
1508
+ logger.debug(f"Looking for IWYU tool at: {tool_path}")
1509
+
1510
+ if not tool_path.exists():
1511
+ logger.error(f"IWYU tool not found: {tool_path}")
1512
+ # List available tools
1513
+ available_tools = [f.name for f in bin_dir.iterdir() if f.is_file()]
1514
+ raise RuntimeError(
1515
+ f"IWYU tool '{tool_name}' not found at: {tool_path}\n"
1516
+ f"Available tools in {bin_dir}:\n"
1517
+ f" {', '.join(available_tools)}"
1518
+ )
1519
+
1520
+ logger.info(f"Found IWYU tool: {tool_path}")
1521
+ return tool_path
1522
+
1523
+
1524
+ def execute_iwyu_tool(tool_name: str, args: list[str] | None = None) -> NoReturn:
1525
+ """
1526
+ Execute an IWYU tool with the given arguments.
1527
+
1528
+ Args:
1529
+ tool_name: Name of the IWYU tool
1530
+ args: Command-line arguments (default: sys.argv[1:])
1531
+
1532
+ Raises:
1533
+ SystemExit: Always exits with the tool's return code
1534
+ """
1535
+ if args is None:
1536
+ args = sys.argv[1:]
1537
+
1538
+ tool_path = find_iwyu_tool(tool_name)
1539
+ platform_name, _ = get_platform_info()
1540
+
1541
+ # For Python scripts, we need to run them with Python
1542
+ if tool_name.endswith(".py"):
1543
+ # Find Python executable
1544
+ python_exe = sys.executable
1545
+ cmd = [python_exe, str(tool_path)] + args
1546
+ else:
1547
+ cmd = [str(tool_path)] + args
1548
+
1549
+ logger.info(f"Executing IWYU tool: {' '.join(cmd)}")
1550
+
1551
+ # Execute tool
1552
+ if platform_name == "win":
1553
+ # Windows: use subprocess
1554
+ try:
1555
+ result = subprocess.run(cmd)
1556
+ sys.exit(result.returncode)
1557
+ except FileNotFoundError as err:
1558
+ raise RuntimeError(f"IWYU tool not found: {tool_path}") from err
1559
+ except Exception as e:
1560
+ raise RuntimeError(f"Error executing IWYU tool: {e}") from e
1561
+ else:
1562
+ # Unix: use exec to replace current process
1563
+ try:
1564
+ if tool_name.endswith(".py"):
1565
+ # For Python scripts, we can't use execv directly
1566
+ result = subprocess.run(cmd)
1567
+ sys.exit(result.returncode)
1568
+ else:
1569
+ os.execv(cmd[0], cmd)
1570
+ except FileNotFoundError as err:
1571
+ raise RuntimeError(f"IWYU tool not found: {tool_path}") from err
1572
+ except Exception as e:
1573
+ raise RuntimeError(f"Error executing IWYU tool: {e}") from e
1574
+
1575
+
1576
+ # IWYU wrapper entry points
1577
+ def iwyu_main() -> NoReturn:
1578
+ """Entry point for include-what-you-use wrapper."""
1579
+ execute_iwyu_tool("include-what-you-use")
1580
+
1581
+
1582
+ def iwyu_tool_main() -> NoReturn:
1583
+ """Entry point for iwyu_tool.py wrapper."""
1584
+ execute_iwyu_tool("iwyu_tool.py")
1585
+
1586
+
1587
+ def fix_includes_main() -> NoReturn:
1588
+ """Entry point for fix_includes.py wrapper."""
1589
+ execute_iwyu_tool("fix_includes.py")