clang-tool-chain 1.0.2__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.

Potentially problematic release.


This version of clang-tool-chain might be problematic. Click here for more details.

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