|
83 | 83 |
|
84 | 84 | import os |
85 | 85 | import sys |
| 86 | +import subprocess |
| 87 | +import platform |
86 | 88 | from enum import Enum |
87 | 89 |
|
88 | 90 | from typing import ( |
@@ -3335,6 +3337,23 @@ def from_source( |
3335 | 3337 | if index is None: |
3336 | 3338 | index = Index.create() |
3337 | 3339 |
|
| 3340 | + # Automatically include builtin headers if enabled |
| 3341 | + if Config.auto_include_builtin_headers: |
| 3342 | + builtin_include_path = Config.get_builtin_include_path() |
| 3343 | + if builtin_include_path: |
| 3344 | + # Check if include path is already specified |
| 3345 | + has_include_path = any( |
| 3346 | + arg == '-I' and i + 1 < len(args) and builtin_include_path in args[i + 1] |
| 3347 | + for i, arg in enumerate(args) |
| 3348 | + ) or any( |
| 3349 | + arg.startswith('-I') and builtin_include_path in arg[2:] |
| 3350 | + for arg in args |
| 3351 | + ) |
| 3352 | + |
| 3353 | + if not has_include_path: |
| 3354 | + # Add the builtin include path |
| 3355 | + args = ['-I', builtin_include_path] + list(args) |
| 3356 | + |
3338 | 3357 | args_array = None |
3339 | 3358 | if len(args) > 0: |
3340 | 3359 | args_array = (c_char_p * len(args))(*[b(x) for x in args]) |
@@ -4309,6 +4328,8 @@ class Config: |
4309 | 4328 | library_file: str | None = None |
4310 | 4329 | compatibility_check = True |
4311 | 4330 | loaded = False |
| 4331 | + auto_include_builtin_headers = True |
| 4332 | + _builtin_include_path: str | None = None |
4312 | 4333 |
|
4313 | 4334 | @staticmethod |
4314 | 4335 | def set_library_path(path: StrPath) -> None: |
@@ -4358,6 +4379,138 @@ def set_compatibility_check(check_status: bool) -> None: |
4358 | 4379 |
|
4359 | 4380 | Config.compatibility_check = check_status |
4360 | 4381 |
|
| 4382 | + @staticmethod |
| 4383 | + def set_auto_include_builtin_headers(enable: bool) -> None: |
| 4384 | + """Enable/disable automatic inclusion of builtin clang headers. |
| 4385 | + |
| 4386 | + When enabled (default), the Python bindings will automatically detect |
| 4387 | + and include the builtin clang headers (such as stddef.h, stdint.h, etc.) |
| 4388 | + that contain essential macros like NULL, offsetof, etc. This prevents |
| 4389 | + issues where these macros are not recognized during parsing. |
| 4390 | + |
| 4391 | + Parameters: |
| 4392 | + enable -- True to automatically include builtin headers, False to disable |
| 4393 | + """ |
| 4394 | + if Config.loaded: |
| 4395 | + raise Exception( |
| 4396 | + "auto_include_builtin_headers must be set before using " |
| 4397 | + "any other functionalities in libclang." |
| 4398 | + ) |
| 4399 | + |
| 4400 | + Config.auto_include_builtin_headers = enable |
| 4401 | + |
| 4402 | + @staticmethod |
| 4403 | + def get_builtin_include_path() -> str | None: |
| 4404 | + """Get the path to clang's builtin headers. |
| 4405 | + |
| 4406 | + Returns the path to clang's builtin include directory, or None if not found. |
| 4407 | + This path contains essential headers like stddef.h that define macros such as NULL. |
| 4408 | + """ |
| 4409 | + if Config._builtin_include_path is not None: |
| 4410 | + return Config._builtin_include_path |
| 4411 | + |
| 4412 | + # Try multiple strategies to find clang's builtin headers |
| 4413 | + candidates = [] |
| 4414 | + |
| 4415 | + # Strategy 1: Query clang directly for its resource directory |
| 4416 | + try: |
| 4417 | + result = subprocess.run( |
| 4418 | + ['clang', '-print-resource-dir'], |
| 4419 | + capture_output=True, text=True, timeout=10 |
| 4420 | + ) |
| 4421 | + if result.returncode == 0: |
| 4422 | + resource_dir = result.stdout.strip() |
| 4423 | + include_dir = os.path.join(resource_dir, 'include') |
| 4424 | + candidates.append(include_dir) |
| 4425 | + except (subprocess.SubprocessError, OSError, subprocess.TimeoutExpired): |
| 4426 | + pass |
| 4427 | + |
| 4428 | + # Strategy 2: Try clang version-based paths |
| 4429 | + try: |
| 4430 | + result = subprocess.run( |
| 4431 | + ['clang', '--version'], |
| 4432 | + capture_output=True, text=True, timeout=10 |
| 4433 | + ) |
| 4434 | + if result.returncode == 0: |
| 4435 | + # Extract version from output like "clang version 19.1.7" |
| 4436 | + for line in result.stdout.splitlines(): |
| 4437 | + if 'clang version' in line.lower(): |
| 4438 | + parts = line.split() |
| 4439 | + for part in parts: |
| 4440 | + if part and part[0].isdigit(): |
| 4441 | + major_version = part.split('.')[0] |
| 4442 | + # Common paths on different systems |
| 4443 | + candidates.extend([ |
| 4444 | + f"/usr/lib/clang/{major_version}/include", |
| 4445 | + f"/usr/local/lib/clang/{major_version}/include", |
| 4446 | + f"/opt/homebrew/lib/clang/{major_version}/include", # macOS Homebrew |
| 4447 | + f"/usr/lib/llvm-{major_version}/lib/clang/{major_version}/include", # Ubuntu |
| 4448 | + ]) |
| 4449 | + break |
| 4450 | + break |
| 4451 | + except (subprocess.SubprocessError, OSError, subprocess.TimeoutExpired): |
| 4452 | + pass |
| 4453 | + |
| 4454 | + # Strategy 3: Check LLVM source tree locations (for developers working with source) |
| 4455 | + # Try to detect if we're running from within an LLVM source tree |
| 4456 | + current_dir = os.path.dirname(os.path.abspath(__file__)) |
| 4457 | + # Navigate up to find the LLVM project root |
| 4458 | + llvm_project_roots = [] |
| 4459 | + check_dir = current_dir |
| 4460 | + for _ in range(10): # Don't go more than 10 levels up |
| 4461 | + if os.path.basename(check_dir) in ['llvm-project', 'llvm']: |
| 4462 | + llvm_project_roots.append(check_dir) |
| 4463 | + parent = os.path.dirname(check_dir) |
| 4464 | + if parent == check_dir: # Reached root |
| 4465 | + break |
| 4466 | + check_dir = parent |
| 4467 | + |
| 4468 | + # Also check common relative paths from current location |
| 4469 | + possible_roots = [ |
| 4470 | + os.path.join(current_dir, '..', '..', '..', '..'), # From clang/bindings/python/clang |
| 4471 | + os.path.join(current_dir, '..', '..', '..'), |
| 4472 | + os.path.join(current_dir, '..', '..'), |
| 4473 | + ] |
| 4474 | + |
| 4475 | + for root in llvm_project_roots + possible_roots: |
| 4476 | + if os.path.exists(root): |
| 4477 | + # Check for clang/lib/Headers in the source tree |
| 4478 | + headers_path = os.path.join(root, 'clang', 'lib', 'Headers') |
| 4479 | + if os.path.exists(headers_path): |
| 4480 | + candidates.append(headers_path) |
| 4481 | + |
| 4482 | + # Strategy 4: Check common installation paths |
| 4483 | + system = platform.system() |
| 4484 | + if system == "Windows": |
| 4485 | + # On Windows, check common LLVM installation paths |
| 4486 | + program_files_paths = [ |
| 4487 | + os.environ.get('ProgramFiles', r'C:\Program Files'), |
| 4488 | + os.environ.get('ProgramFiles(x86)', r'C:\Program Files (x86)'), |
| 4489 | + ] |
| 4490 | + for pf in program_files_paths: |
| 4491 | + if pf and os.path.exists(pf): |
| 4492 | + llvm_base = os.path.join(pf, 'LLVM') |
| 4493 | + if os.path.exists(llvm_base): |
| 4494 | + for item in os.listdir(llvm_base): |
| 4495 | + lib_path = os.path.join(llvm_base, item, 'lib', 'clang') |
| 4496 | + if os.path.exists(lib_path): |
| 4497 | + for version in os.listdir(lib_path): |
| 4498 | + include_path = os.path.join(lib_path, version, 'include') |
| 4499 | + candidates.append(include_path) |
| 4500 | + |
| 4501 | + # Find the first existing candidate |
| 4502 | + for candidate in candidates: |
| 4503 | + if candidate and os.path.isdir(candidate): |
| 4504 | + # Verify it contains stddef.h as a sanity check |
| 4505 | + stddef_path = os.path.join(candidate, 'stddef.h') |
| 4506 | + if os.path.isfile(stddef_path): |
| 4507 | + Config._builtin_include_path = candidate |
| 4508 | + return candidate |
| 4509 | + |
| 4510 | + # If nothing found, cache the negative result |
| 4511 | + Config._builtin_include_path = "" |
| 4512 | + return None |
| 4513 | + |
4361 | 4514 | @CachedProperty |
4362 | 4515 | def lib(self) -> CDLL: |
4363 | 4516 | lib = self.get_cindex_library() |
|
0 commit comments