Skip to content

Commit d865968

Browse files
committed
Fix libclang not being compiled with the standard include headers.
1 parent b75896b commit d865968

File tree

1 file changed

+153
-0
lines changed

1 file changed

+153
-0
lines changed

clang/bindings/python/clang/cindex.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@
8383

8484
import os
8585
import sys
86+
import subprocess
87+
import platform
8688
from enum import Enum
8789

8890
from typing import (
@@ -3335,6 +3337,23 @@ def from_source(
33353337
if index is None:
33363338
index = Index.create()
33373339

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+
33383357
args_array = None
33393358
if len(args) > 0:
33403359
args_array = (c_char_p * len(args))(*[b(x) for x in args])
@@ -4309,6 +4328,8 @@ class Config:
43094328
library_file: str | None = None
43104329
compatibility_check = True
43114330
loaded = False
4331+
auto_include_builtin_headers = True
4332+
_builtin_include_path: str | None = None
43124333

43134334
@staticmethod
43144335
def set_library_path(path: StrPath) -> None:
@@ -4358,6 +4379,138 @@ def set_compatibility_check(check_status: bool) -> None:
43584379

43594380
Config.compatibility_check = check_status
43604381

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+
43614514
@CachedProperty
43624515
def lib(self) -> CDLL:
43634516
lib = self.get_cindex_library()

0 commit comments

Comments
 (0)