Skip to content

Commit 8ea152e

Browse files
authored
Merge pull request #4823 from Repiteo/compilation-db-revamp
Simplify and speedup compilation database generation
2 parents 1f46738 + f9ad988 commit 8ea152e

3 files changed

Lines changed: 84 additions & 177 deletions

File tree

CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER
2626
- Purge vim/emac local variable bloat.
2727
- Implement type hints for Node subclasses.
2828
- Ruff: Handle F401 exclusions more granularly, remove per-file exclusions.
29+
- Simplified and sped up compilation database generation. No longer requires
30+
each entry to have a dedicated node that's always built; instead, the database
31+
*itself* is set to always build.
2932
- Implement type hints for Environment and environment utilities.
3033
- Deprecated Python 3.7 & 3.8 support.
3134
- Modernized GitHub Actions test runner. This consolidates the logic of

RELEASE.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ IMPROVEMENTS
6868

6969
- Used Gemini to refactor runtest.py to better organized the code and add docstrings.
7070

71+
- Simplified and sped up compilation database generation. No longer requires
72+
each entry to have a dedicated node that's always built; instead, the database
73+
*itself* is set to always build.
74+
7175
- Switch remaining "original style" docstring parameter listings to Google style.
7276

7377
- Additional small tweaks to Environment.py type hints, fold some overly

SCons/Tool/compilation_db.py

Lines changed: 77 additions & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,19 @@
3333
``compile_commands.json``, the name that most clang tools search for by default.
3434
"""
3535

36-
import json
37-
import itertools
3836
import fnmatch
39-
import SCons
37+
import itertools
38+
import json
4039

40+
from SCons.Action import Action
41+
from SCons.Builder import Builder, ListEmitter
4142
from SCons.Platform import TempFileMunge
43+
from SCons.Tool import createObjBuilders
44+
from SCons.Tool.asm import ASPPSuffixes, ASSuffixes
45+
from SCons.Tool.cc import CSuffixes
46+
from SCons.Tool.cxx import CXXSuffixes
4247

43-
from .cxx import CXXSuffixes
44-
from .cc import CSuffixes
45-
from .asm import ASSuffixes, ASPPSuffixes
46-
47-
DEFAULT_DB_NAME = 'compile_commands.json'
48+
DEFAULT_DB_NAME = "compile_commands.json"
4849

4950
# TODO: Is there a better way to do this than this global? Right now this exists so that the
5051
# emitter we add can record all of the things it emits, so that the scanner for the top level
@@ -54,142 +55,51 @@
5455
__COMPILATION_DB_ENTRIES = []
5556

5657

57-
# We make no effort to avoid rebuilding the entries. Someday, perhaps we could and even
58-
# integrate with the cache, but there doesn't seem to be much call for it.
59-
class __CompilationDbNode(SCons.Node.Python.Value):
60-
def __init__(self, value) -> None:
61-
SCons.Node.Python.Value.__init__(self, value)
62-
self.Decider(changed_since_last_build_node)
63-
64-
65-
def changed_since_last_build_node(child, target, prev_ni, node) -> bool:
66-
""" Dummy decider to force always building"""
67-
return True
68-
69-
70-
def make_emit_compilation_DB_entry(comstr):
71-
"""
72-
Effectively this creates a lambda function to capture:
73-
* command line
74-
* source
75-
* target
76-
:param comstr: unevaluated command line
77-
:return: an emitter which has captured the above
78-
"""
79-
user_action = SCons.Action.Action(comstr)
80-
81-
def emit_compilation_db_entry(target, source, env):
82-
"""
83-
This emitter will be added to each c/c++ object build to capture the info needed
84-
for clang tools
85-
:param target: target node(s)
86-
:param source: source node(s)
87-
:param env: Environment for use building this node
88-
:return: target(s), source(s)
89-
"""
90-
91-
dbtarget = __CompilationDbNode(source)
92-
93-
entry = env.__COMPILATIONDB_Entry(
94-
target=dbtarget,
95-
source=[],
96-
__COMPILATIONDB_UOUTPUT=target,
97-
__COMPILATIONDB_USOURCE=source,
98-
__COMPILATIONDB_UACTION=user_action,
99-
__COMPILATIONDB_ENV=env,
100-
)
101-
102-
# TODO: Technically, these next two lines should not be required: it should be fine to
103-
# cache the entries. However, they don't seem to update properly. Since they are quick
104-
# to re-generate disable caching and sidestep this problem.
105-
env.AlwaysBuild(entry)
106-
env.NoCache(entry)
107-
108-
__COMPILATION_DB_ENTRIES.append(dbtarget)
109-
110-
return target, source
111-
112-
return emit_compilation_db_entry
113-
114-
11558
class CompDBTEMPFILE(TempFileMunge):
11659
def __call__(self, target, source, env, for_signature):
11760
return self.cmd
11861

11962

120-
def compilation_db_entry_action(target, source, env, **kw) -> None:
121-
"""
122-
Create a dictionary with evaluated command line, target, source
123-
and store that info as an attribute on the target
124-
(Which has been stored in __COMPILATION_DB_ENTRIES array
125-
:param target: target node(s)
126-
:param source: source node(s)
127-
:param env: Environment for use building this node
128-
:param kw:
129-
:return: None
130-
"""
131-
132-
command = env["__COMPILATIONDB_UACTION"].strfunction(
133-
target=env["__COMPILATIONDB_UOUTPUT"],
134-
source=env["__COMPILATIONDB_USOURCE"],
135-
env=env["__COMPILATIONDB_ENV"],
136-
overrides={'TEMPFILE': CompDBTEMPFILE}
137-
)
138-
139-
entry = {
140-
"directory": env.Dir("#").abspath,
141-
"command": command,
142-
"file": env["__COMPILATIONDB_USOURCE"][0],
143-
"output": env['__COMPILATIONDB_UOUTPUT'][0]
144-
}
145-
146-
target[0].write(entry)
147-
148-
14963
def write_compilation_db(target, source, env) -> None:
150-
entries = []
151-
152-
use_abspath = env['COMPILATIONDB_USE_ABSPATH'] in [True, 1, 'True', 'true']
153-
use_path_filter = env.subst('$COMPILATIONDB_PATH_FILTER')
64+
directory = env.Dir("#").get_abspath()
65+
overrides = {"TEMPFILE": CompDBTEMPFILE}
66+
use_abspath = env["COMPILATIONDB_USE_ABSPATH"] in [True, 1, "True", "true"]
67+
use_path_filter = env.subst("$COMPILATIONDB_PATH_FILTER")
15468

155-
for s in __COMPILATION_DB_ENTRIES:
156-
entry = s.read()
157-
source_file = entry['file']
158-
output_file = entry['output']
69+
entries = []
70+
for db_target, db_source, db_env, db_action in __COMPILATION_DB_ENTRIES:
71+
# Parse command before filtering.
72+
command = db_action.strfunction(db_target, db_source, db_env, None, overrides)
15973

160-
if not source_file.is_derived():
161-
source_file = source_file.srcnode()
74+
if not db_source.is_derived():
75+
db_source = db_source.srcnode()
16276

16377
if use_abspath:
164-
source_file = source_file.abspath
165-
output_file = output_file.abspath
78+
file = db_source.get_abspath()
79+
output = db_target.get_abspath()
16680
else:
167-
source_file = source_file.path
168-
output_file = output_file.path
81+
file = db_source.get_path()
82+
output = db_target.get_path()
16983

170-
if use_path_filter and not fnmatch.fnmatch(output_file, use_path_filter):
84+
if use_path_filter and not fnmatch.fnmatch(output, use_path_filter):
17185
continue
17286

173-
path_entry = {'directory': entry['directory'],
174-
'command': entry['command'],
175-
'file': source_file,
176-
'output': output_file}
177-
178-
entries.append(path_entry)
179-
180-
with open(target[0].path, "w") as output_file:
181-
json.dump(
182-
entries, output_file, sort_keys=True, indent=4, separators=(",", ": ")
87+
entries.append(
88+
{
89+
"command": command,
90+
"directory": directory,
91+
"file": file,
92+
"output": output,
93+
}
18394
)
184-
output_file.write("\n")
185-
18695

187-
def scan_compilation_db(node, env, path):
188-
return __COMPILATION_DB_ENTRIES
96+
with open(target[0].get_path(), "w", encoding="utf-8", newline="\n") as output_file:
97+
json.dump(entries, output_file, sort_keys=True, indent=4)
98+
output_file.write("\n")
18999

190100

191101
def compilation_db_emitter(target, source, env):
192-
""" fix up the source/targets """
102+
"""Fix up the source/targets"""
193103

194104
# Someone called env.CompilationDatabase('my_targetname.json')
195105
if not target and len(source) == 1:
@@ -202,75 +112,65 @@ def compilation_db_emitter(target, source, env):
202112
if source:
203113
source = []
204114

115+
# TODO: Should eventually have a way to allow the entries themselves to
116+
# function as dependencies.
117+
env.AlwaysBuild(target)
118+
env.NoCache(target)
119+
205120
return target, source
206121

207122

208123
def generate(env, **kwargs) -> None:
209-
static_obj, shared_obj = SCons.Tool.createObjBuilders(env)
124+
def _generate_emitter(command):
125+
# Construct new action to bypass `COMSTR`.
126+
action = Action(command)
210127

211-
env["COMPILATIONDB_COMSTR"] = kwargs.get(
212-
"COMPILATIONDB_COMSTR", "Building compilation database $TARGET"
213-
)
128+
def _compilation_db_entry_emitter(target, source, env):
129+
__COMPILATION_DB_ENTRIES.append((target[0], source[0], env, action))
130+
return target, source
131+
132+
return _compilation_db_entry_emitter
133+
134+
cc_emitter = _generate_emitter("$CCCOM")
135+
shcc_emitter = _generate_emitter("$SHCCCOM")
136+
cxx_emitter = _generate_emitter("$CXXCOM")
137+
shcxx_emitter = _generate_emitter("$SHCXXCOM")
138+
as_emitter = _generate_emitter("$ASCOM")
139+
aspp_emitter = _generate_emitter("$ASPPCOM")
214140

215-
components_by_suffix = itertools.chain(
141+
static_obj, shared_obj = createObjBuilders(env)
142+
143+
for suffix, (builder, emitter) in itertools.chain(
216144
itertools.product(
217-
CSuffixes,
218-
[
219-
(static_obj, SCons.Defaults.StaticObjectEmitter, "$CCCOM"),
220-
(shared_obj, SCons.Defaults.SharedObjectEmitter, "$SHCCCOM"),
221-
],
145+
CSuffixes, [(static_obj, cc_emitter), (shared_obj, shcc_emitter)]
222146
),
223147
itertools.product(
224-
CXXSuffixes,
225-
[
226-
(static_obj, SCons.Defaults.StaticObjectEmitter, "$CXXCOM"),
227-
(shared_obj, SCons.Defaults.SharedObjectEmitter, "$SHCXXCOM"),
228-
],
148+
CXXSuffixes, [(static_obj, cxx_emitter), (shared_obj, shcxx_emitter)]
229149
),
230150
itertools.product(
231-
ASSuffixes,
232-
[
233-
(static_obj, SCons.Defaults.StaticObjectEmitter, "$ASCOM"),
234-
(shared_obj, SCons.Defaults.SharedObjectEmitter, "$ASCOM")
235-
],
151+
ASSuffixes, [(static_obj, as_emitter), (shared_obj, as_emitter)]
236152
),
237153
itertools.product(
238-
ASPPSuffixes,
239-
[
240-
(static_obj, SCons.Defaults.StaticObjectEmitter, "$ASPPCOM"),
241-
(shared_obj, SCons.Defaults.SharedObjectEmitter, "$ASPPCOM")
242-
],
243-
),
244-
)
245-
246-
for entry in components_by_suffix:
247-
suffix = entry[0]
248-
builder, base_emitter, command = entry[1]
249-
250-
# Assumes a dictionary emitter
251-
emitter = builder.emitter.get(suffix, False)
252-
if emitter:
253-
# We may not have tools installed which initialize all or any of
254-
# cxx, cc, or assembly. If not skip resetting the respective emitter.
255-
builder.emitter[suffix] = SCons.Builder.ListEmitter(
256-
[emitter, make_emit_compilation_DB_entry(command), ]
257-
)
258-
259-
env["BUILDERS"]["__COMPILATIONDB_Entry"] = SCons.Builder.Builder(
260-
action=SCons.Action.Action(compilation_db_entry_action, None),
261-
)
262-
263-
env["BUILDERS"]["CompilationDatabase"] = SCons.Builder.Builder(
264-
action=SCons.Action.Action(write_compilation_db, "$COMPILATIONDB_COMSTR"),
265-
target_scanner=SCons.Scanner.Scanner(
266-
function=scan_compilation_db, node_class=None
154+
ASPPSuffixes, [(static_obj, aspp_emitter), (shared_obj, aspp_emitter)]
267155
),
156+
):
157+
emitter_old = builder.emitter.get(suffix)
158+
# Only setup emitters for Tools supported by the environment.
159+
if emitter_old:
160+
builder.emitter[suffix] = ListEmitter(env.Flatten(emitter_old) + [emitter])
161+
162+
env["BUILDERS"]["CompilationDatabase"] = Builder(
163+
action=Action(write_compilation_db, "$COMPILATIONDB_COMSTR"),
268164
emitter=compilation_db_emitter,
269-
suffix='json',
165+
suffix="json",
270166
)
271167

272-
env['COMPILATIONDB_USE_ABSPATH'] = False
273-
env['COMPILATIONDB_PATH_FILTER'] = ''
168+
env["COMPILATIONDB_USE_ABSPATH"] = env.get("COMPILATIONDB_USE_ABSPATH", False)
169+
env["COMPILATIONDB_PATH_FILTER"] = env.get("COMPILATIONDB_PATH_FILTER", "")
170+
env["COMPILATIONDB_COMSTR"] = env.get(
171+
"COMPILATIONDB_COMSTR",
172+
kwargs.get("COMPILATIONDB_COMSTR", "Building compilation database $TARGET"),
173+
)
274174

275175

276176
def exists(env) -> bool:

0 commit comments

Comments
 (0)