Skip to content

Commit 23ccfeb

Browse files
committed
Write a compile_commands.json from build_ext
Produce a JSON database of the compiler commands executed while building extension modules as `build/compile_commands.json`. This is usable by various C and C++ language servers, linters, and IDEs, like `clangd`, `clang-tidy`, and CLion. These tools need to understand the header search path and macros passed on the compiler command line in order to correctly interpret source files. In the case of Python extension modules, the developer might not even know all of the compiler flags that are being used, since some are inherited from the interpreter via `sysconfig`.
1 parent 2212422 commit 23ccfeb

File tree

3 files changed

+38
-5
lines changed

3 files changed

+38
-5
lines changed

newsfragments/4358.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
A ``compile_commands.json`` is now written to the ``build/`` directory when building extension modules.

setuptools/_distutils/unixccompiler.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ class UnixCCompiler(CCompiler):
145145
if sys.platform == "cygwin":
146146
exe_extension = ".exe"
147147

148+
def __init__(self, *args, **kwargs):
149+
super().__init__(*args, **kwargs)
150+
self.compile_commands = []
151+
148152
def preprocess(
149153
self,
150154
source,
@@ -185,7 +189,11 @@ def preprocess(
185189
def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts):
186190
compiler_so = compiler_fixup(self.compiler_so, cc_args + extra_postargs)
187191
try:
188-
self.spawn(compiler_so + cc_args + [src, '-o', obj] + extra_postargs)
192+
cmd = compiler_so + cc_args + [src, '-o', obj] + extra_postargs
193+
self.spawn(cmd)
194+
self.compile_commands.append(
195+
{"directory": os.getcwd(), "arguments": cmd, "file": src}
196+
)
189197
except DistutilsExecError as msg:
190198
raise CompileError(msg)
191199

setuptools/command/build_ext.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import json
12
import os
23
import sys
34
import itertools
45
from importlib.machinery import EXTENSION_SUFFIXES
56
from importlib.util import cache_from_source as _compiled_file_name
6-
from typing import Dict, Iterator, List, Tuple
7+
from typing import Any, Dict, Iterator, List, Tuple
78
from pathlib import Path
89

910
from distutils.command.build_ext import build_ext as _du_build_ext
@@ -89,10 +90,31 @@ def run(self):
8990
"""Build extensions in build directory, then copy if --inplace"""
9091
old_inplace, self.inplace = self.inplace, 0
9192
_build_ext.run(self)
93+
self._update_compilation_database(
94+
getattr(self.compiler, "compile_commands", [])
95+
)
9296
self.inplace = old_inplace
9397
if old_inplace:
9498
self.copy_extensions_to_source()
9599

100+
def _update_compilation_database(self, commands: List[Dict[str, Any]]) -> None:
101+
build_base = Path(self.get_finalized_command('build').build_base)
102+
build_base.mkdir(exist_ok=True)
103+
104+
db_file = build_base / "compile_commands.json"
105+
try:
106+
database = json.loads(db_file.read_text(encoding="utf-8"))
107+
except OSError:
108+
database = []
109+
110+
# Drop existing entries for newly built files
111+
built_files = {command["file"] for command in commands}
112+
database = [obj for obj in database if obj.get("file") not in built_files]
113+
114+
database.extend(commands)
115+
output = json.dumps(database, allow_nan=False, indent=4, sort_keys=True)
116+
db_file.write_text(output, encoding="utf-8")
117+
96118
def _get_inplace_equivalent(self, build_py, ext: Extension) -> Tuple[str, str]:
97119
fullname = self.get_ext_fullname(ext.name)
98120
filename = self.get_ext_filename(fullname)
@@ -342,8 +364,9 @@ def _write_stub_file(self, stub_file: str, ext: Extension, compile=False):
342364
if compile and os.path.exists(stub_file):
343365
raise BaseError(stub_file + " already exists! Please delete.")
344366
if not self.dry_run:
345-
with open(stub_file, 'w', encoding="utf-8") as f:
346-
content = '\n'.join([
367+
f = open(stub_file, 'w')
368+
f.write(
369+
'\n'.join([
347370
"def __bootstrap__():",
348371
" global __bootstrap__, __file__, __loader__",
349372
" import sys, os, pkg_resources, importlib.util" + if_dl(", dl"),
@@ -367,7 +390,8 @@ def _write_stub_file(self, stub_file: str, ext: Extension, compile=False):
367390
"__bootstrap__()",
368391
"", # terminal \n
369392
])
370-
f.write(content)
393+
)
394+
f.close()
371395
if compile:
372396
self._compile_and_remove_stub(stub_file)
373397

0 commit comments

Comments
 (0)