Skip to content

Commit 13c0e3c

Browse files
committed
Add compile commands extension
Signed-off-by: Philip Sakievich <[email protected]>
1 parent d98c4b5 commit 13c0e3c

File tree

3 files changed

+451
-0
lines changed

3 files changed

+451
-0
lines changed

manager/cmd/manager.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from ..manager_cmds import (
88
binary_finder,
99
cache_query,
10+
compile_commands,
1011
cli_config,
1112
create_dev_env,
1213
create_env,
@@ -46,6 +47,7 @@ def setup_parser(subparser):
4647
analyze.add_command(sp, _subcommands)
4748
binary_finder.add_command(sp, _subcommands)
4849
cache_query.add_command(sp, _subcommands)
50+
compile_commands.add_command(sp, _subcommands)
4951
create_env.add_command(sp, _subcommands)
5052
create_dev_env.add_command(sp, _subcommands)
5153
develop.add_command(sp, _subcommands)
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import sys
2+
import re
3+
import os
4+
import shutil
5+
import json
6+
import shlex
7+
import time
8+
import concurrent.futures
9+
from functools import partial
10+
from contextlib import contextmanager
11+
12+
import spack
13+
from spack import environment
14+
15+
"""
16+
This extension was contributed by vbrunini
17+
"""
18+
19+
20+
command_name = "compile-commands"
21+
description = "Copy compile_commands.json for all develop packages in the active environment, then remove cross-package -isystem flags and substitute source/build -I flags."
22+
section = "sierra"
23+
level = "long"
24+
aliases = []
25+
26+
def setup_parser(subparser):
27+
subparser.add_argument(
28+
"--serial",
29+
action="store_true",
30+
help="Process compile_commands.json files in serial.",
31+
)
32+
33+
34+
@contextmanager
35+
def timer(name: str, args):
36+
start = time.perf_counter()
37+
yield
38+
end = time.perf_counter()
39+
if args.verbose:
40+
print(f"[{name}] elapsed: {end-start:.3f} s")
41+
42+
43+
def build_dir(builder, pkg):
44+
if hasattr(builder, "build_directory"):
45+
return os.path.normpath(os.path.join(pkg.stage.path, builder.build_directory))
46+
return pkg.stage.source_path
47+
48+
49+
def source_root(pkg):
50+
if hasattr(pkg, "root_cmakelists_dir"):
51+
return os.path.join(pkg.stage.source_path, pkg.root_cmakelists_dir)
52+
return pkg.stage.source_path
53+
54+
55+
def _process_spec(spec, args):
56+
"""Worker that handles a single spec and returns (info, (regex, includes))."""
57+
58+
if not spec.is_develop:
59+
return None
60+
61+
pkg = spec.package
62+
if args.verbose:
63+
sys.stdout.write(f"\nProcessing {pkg.name}\n")
64+
try:
65+
builder = spack.builder.create(pkg)
66+
src_cc_path = os.path.join(build_dir(builder, pkg), "compile_commands.json")
67+
except Exception:
68+
return None
69+
70+
if not os.path.exists(src_cc_path):
71+
return None
72+
73+
dst_dir = pkg.stage.source_path
74+
if getattr(pkg, "root_cmakelists_dir", None):
75+
dst_dir = os.path.join(dst_dir, pkg.root_cmakelists_dir)
76+
os.makedirs(dst_dir, exist_ok=True)
77+
dest_cc_path = os.path.join(dst_dir, "compile_commands.json")
78+
79+
if os.path.islink(dest_cc_path):
80+
if args.verbose:
81+
sys.stdout.write(f" removing existing symlink {dest_cc_path}\n")
82+
os.unlink(dest_cc_path)
83+
84+
try:
85+
with open(src_cc_path) as f:
86+
cc_entries = json.load(f)
87+
except Exception:
88+
cc_entries = []
89+
90+
pkg_build_dir = build_dir(builder, pkg)
91+
pkg_src_root = source_root(pkg)
92+
93+
source_include_flags = []
94+
source_include_re = re.compile(rf"-I(?:{pkg_src_root}|{pkg_build_dir})\S*")
95+
for entry in cc_entries:
96+
if "command" not in entry:
97+
continue
98+
for match in source_include_re.findall(entry["command"]):
99+
if match not in source_include_flags:
100+
source_include_flags.append(match)
101+
102+
replacement_pattern = rf"-isystem\s+{pkg.prefix}\S*"
103+
if args.verbose:
104+
sys.stdout.write(f" replacement pattern = {replacement_pattern}\n")
105+
sys.stdout.write(f" source include flags = {source_include_flags}\n")
106+
107+
info = {"cc_entries": cc_entries, "dest_cc_path": dest_cc_path}
108+
repl = (re.compile(replacement_pattern), " ".join(source_include_flags))
109+
return (info, repl)
110+
111+
112+
def _apply_replacements(info, include_path_replacements):
113+
cc_entries = info["cc_entries"]
114+
dest_cc_path = info["dest_cc_path"]
115+
116+
for entry in cc_entries:
117+
if "command" not in entry:
118+
continue
119+
command = entry["command"]
120+
for regex, source_includes in include_path_replacements:
121+
# First replace with the gathered -I flags, then strip any leftover -isystem
122+
command = regex.sub(source_includes, command, 1)
123+
command = regex.sub("", command)
124+
entry["command"] = command
125+
126+
# Write out the (now cleaned) compile_commands.json
127+
with open(dest_cc_path, "w") as f:
128+
json.dump(cc_entries, f, indent=2)
129+
f.write("\n")
130+
return dest_cc_path
131+
132+
133+
def _get_executor(args):
134+
if args.serial:
135+
return concurrent.futures.ThreadPoolExecutor(max_workers=1)
136+
137+
return concurrent.futures.ProcessPoolExecutor()
138+
139+
140+
def compile_commands(parser, args):
141+
env = environment.active_environment()
142+
143+
# First pass: copy each compile_commands.json into its source dir
144+
infos = []
145+
include_path_replacements = []
146+
with timer("Determine include path replacements", args):
147+
with _get_executor(args) as exe:
148+
futures = [
149+
exe.submit(_process_spec, spec, args) for spec in env.all_specs()
150+
]
151+
for fut in concurrent.futures.as_completed(futures):
152+
result = fut.result()
153+
if result is None:
154+
continue
155+
info, repl = result
156+
infos.append(info)
157+
include_path_replacements.append(repl)
158+
159+
# Second pass: Apply isystem removal regexes and insert corresponding source include flags if they were applied
160+
with timer("Apply include path replacements", args):
161+
with _get_executor(args) as exe:
162+
list(
163+
exe.map(
164+
partial(
165+
_apply_replacements,
166+
include_path_replacements=include_path_replacements,
167+
),
168+
infos,
169+
)
170+
)
171+
172+
173+
def add_command(parser, command_dict):
174+
sub_parser = parser.add_parser(
175+
command_name, help=description, description=description, aliases=aliases
176+
)
177+
setup_parser(sub_parser)
178+
command_dict[command_name] = compile_commands
179+
for alias in aliases:
180+
command_dict[alias] = compile_commands

0 commit comments

Comments
 (0)