Skip to content

Commit cb6b856

Browse files
committed
Simply and speedup compilation database generation
1 parent aaa4bf9 commit cb6b856

File tree

3 files changed

+86
-184
lines changed

3 files changed

+86
-184
lines changed

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

3033
From William Deegan:
3134
- Fix SCons Docbook schema to work with lxml > 5

RELEASE.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ IMPROVEMENTS
5353

5454
- Used Gemini to refactor runtest.py to better organized the code and add docstrings.
5555

56+
- Simplified and sped up compilation database generation. No longer requires
57+
each entry to have a dedicated node that's always built; instead, the database
58+
*itself* is set to always build.
5659

5760
PACKAGING
5861
---------

SCons/Tool/compilation_db.py

Lines changed: 80 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -33,163 +33,66 @@
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
42-
43-
from .cxx import CXXSuffixes
44-
from .cc import CSuffixes
45-
from .asm import ASSuffixes, ASPPSuffixes
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
4647

4748
DEFAULT_DB_NAME = 'compile_commands.json'
4849

49-
# TODO: Is there a better way to do this than this global? Right now this exists so that the
50-
# emitter we add can record all of the things it emits, so that the scanner for the top level
51-
# compilation database can access the complete list, and also so that the writer has easy
52-
# access to write all of the files. But it seems clunky. How can the emitter and the scanner
53-
# communicate more gracefully?
54-
__COMPILATION_DB_ENTRIES = []
55-
56-
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-
11450

11551
class CompDBTEMPFILE(TempFileMunge):
11652
def __call__(self, target, source, env, for_signature):
11753
return self.cmd
11854

11955

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-
14956
def write_compilation_db(target, source, env) -> None:
150-
entries = []
57+
DIRECTORY = env.Dir("#").get_abspath()
58+
OVERRIDES = {"TEMPFILE": CompDBTEMPFILE}
59+
USE_ABSPATH = env['COMPILATIONDB_USE_ABSPATH'] in [True, 1, 'True', 'true']
60+
USE_PATH_FILTER = env.subst('$COMPILATIONDB_PATH_FILTER')
15161

152-
use_abspath = env['COMPILATIONDB_USE_ABSPATH'] in [True, 1, 'True', 'true']
153-
use_path_filter = env.subst('$COMPILATIONDB_PATH_FILTER')
154-
155-
for s in __COMPILATION_DB_ENTRIES:
156-
entry = s.read()
157-
source_file = entry['file']
158-
output_file = entry['output']
62+
entries = []
63+
for db_target, db_source, db_env, db_action in env._compilation_db_entries:
64+
# Parse command before filtering.
65+
command = db_action.strfunction(db_target, db_source, db_env, None, OVERRIDES)
15966

160-
if not source_file.is_derived():
161-
source_file = source_file.srcnode()
67+
if not db_source.is_derived():
68+
db_source = db_source.srcnode()
16269

163-
if use_abspath:
164-
source_file = source_file.abspath
165-
output_file = output_file.abspath
70+
if USE_ABSPATH:
71+
file = db_source.get_abspath()
72+
output = db_target.get_abspath()
16673
else:
167-
source_file = source_file.path
168-
output_file = output_file.path
74+
file = db_source.get_path()
75+
output = db_target.get_path()
16976

170-
if use_path_filter and not fnmatch.fnmatch(output_file, use_path_filter):
77+
if USE_PATH_FILTER and not fnmatch.fnmatch(output, USE_PATH_FILTER):
17178
continue
17279

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=(",", ": ")
80+
entries.append(
81+
{
82+
"command": command,
83+
"directory": DIRECTORY,
84+
"file": file,
85+
"output": output,
86+
}
18387
)
184-
output_file.write("\n")
185-
18688

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

19093

19194
def compilation_db_emitter(target, source, env):
192-
""" fix up the source/targets """
95+
"""Fix up the source/targets"""
19396

19497
# Someone called env.CompilationDatabase('my_targetname.json')
19598
if not target and len(source) == 1:
@@ -202,75 +105,68 @@ def compilation_db_emitter(target, source, env):
202105
if source:
203106
source = []
204107

108+
# TODO: Should eventually have a way to allow the entries themselves to
109+
# function as dependencies.
110+
env.AlwaysBuild(target)
111+
env.NoCache(target)
112+
205113
return target, source
206114

207115

208116
def generate(env, **kwargs) -> None:
209-
static_obj, shared_obj = SCons.Tool.createObjBuilders(env)
117+
def _generate_emitter(command):
118+
# Construct new action to bypass `COMSTR`.
119+
action = Action(command)
210120

211-
env["COMPILATIONDB_COMSTR"] = kwargs.get(
212-
"COMPILATIONDB_COMSTR", "Building compilation database $TARGET"
213-
)
121+
def _compilation_db_entry_emitter(target, source, env):
122+
env._compilation_db_entries.append((target[0], source[0], env, action))
123+
return target, source
124+
125+
return _compilation_db_entry_emitter
126+
127+
GEN_CCCOM = _generate_emitter("$CCCOM")
128+
GEN_SHCCCOM = _generate_emitter("$SHCCCOM")
129+
GEN_CXXCOM = _generate_emitter("$CXXCOM")
130+
GEN_SHCXXCOM = _generate_emitter("$SHCXXCOM")
131+
GEN_ASCOM = _generate_emitter("$ASCOM")
132+
GEN_ASPPCOM = _generate_emitter("$ASPPCOM")
214133

215-
components_by_suffix = itertools.chain(
134+
env._compilation_db_entries = []
135+
static_obj, shared_obj = createObjBuilders(env)
136+
137+
for suffix, (builder, emitter) in itertools.chain(
216138
itertools.product(
217-
CSuffixes,
218-
[
219-
(static_obj, SCons.Defaults.StaticObjectEmitter, "$CCCOM"),
220-
(shared_obj, SCons.Defaults.SharedObjectEmitter, "$SHCCCOM"),
221-
],
139+
CSuffixes, [(static_obj, GEN_CCCOM), (shared_obj, GEN_SHCCCOM)]
222140
),
223141
itertools.product(
224-
CXXSuffixes,
225-
[
226-
(static_obj, SCons.Defaults.StaticObjectEmitter, "$CXXCOM"),
227-
(shared_obj, SCons.Defaults.SharedObjectEmitter, "$SHCXXCOM"),
228-
],
142+
CXXSuffixes, [(static_obj, GEN_CXXCOM), (shared_obj, GEN_SHCXXCOM)]
229143
),
230144
itertools.product(
231-
ASSuffixes,
232-
[
233-
(static_obj, SCons.Defaults.StaticObjectEmitter, "$ASCOM"),
234-
(shared_obj, SCons.Defaults.SharedObjectEmitter, "$ASCOM")
235-
],
145+
ASSuffixes, [(static_obj, GEN_ASCOM), (shared_obj, GEN_ASCOM)]
236146
),
237147
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
148+
ASPPSuffixes, [(static_obj, GEN_ASPPCOM), (shared_obj, GEN_ASPPCOM)]
267149
),
150+
):
151+
emitter_old = builder.emitter.get(suffix)
152+
# Only setup emitters for Tools supported by the environment.
153+
if emitter_old:
154+
builder.emitter[suffix] = ListEmitter(env.Flatten(emitter_old) + [emitter])
155+
156+
env["BUILDERS"]["CompilationDatabase"] = Builder(
157+
action=Action(write_compilation_db, "$COMPILATIONDB_COMSTR"),
268158
emitter=compilation_db_emitter,
269-
suffix='json',
159+
suffix="json",
270160
)
271161

272-
env['COMPILATIONDB_USE_ABSPATH'] = False
273-
env['COMPILATIONDB_PATH_FILTER'] = ''
162+
if "COMPILATIONDB_USE_ABSPATH" not in env:
163+
env["COMPILATIONDB_USE_ABSPATH"] = False
164+
if "COMPILATIONDB_PATH_FILTER" not in env:
165+
env["COMPILATIONDB_PATH_FILTER"] = ""
166+
if "COMPILATIONDB_COMSTR" not in env:
167+
env["COMPILATIONDB_COMSTR"] = kwargs.get(
168+
"COMPILATIONDB_COMSTR", "Building compilation database $TARGET"
169+
)
274170

275171

276172
def exists(env) -> bool:

0 commit comments

Comments
 (0)