Skip to content

Commit 3149a94

Browse files
committed
Fix #13290 (Import project: -isystem in compile_commands.json)
1 parent c193dfd commit 3149a94

2 files changed

Lines changed: 377 additions & 0 deletions

File tree

tools/tweak-compile-commands.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# tweak-compile-commands.py
2+
3+
## NAME
4+
5+
tweak-compile-commands.py - tweak `-isystem`/`--sysroot` handling in a `compile_commands.json` file
6+
7+
## SYNOPSIS
8+
9+
```
10+
tools/tweak-compile-commands.py COMPILE_COMMANDS [-o OUTPUT | -i]
11+
[--isystem-to-i] [--exclude-folder FOLDER ...]
12+
[--remove-include-path PATH ...]
13+
```
14+
15+
## DESCRIPTION
16+
17+
Some cross-compiling toolchains resolve `-isystem` paths relative to
18+
`--sysroot` internally, so a build command such as:
19+
20+
```
21+
gcc --sysroot /a/b -isystem /opt/x -c foo.c
22+
```
23+
24+
actually searches both `/opt/x` *and* `/a/b/opt/x` for headers. A tool that
25+
reads `compile_commands.json` (such as Cppcheck) does not know about this
26+
implicit sysroot-relative lookup and only sees `/opt/x`, which can cause it
27+
to miss headers or resolve the wrong ones.
28+
29+
`tweak-compile-commands.py` rewrites each build command in a
30+
`compile_commands.json` file so this implicit behaviour is spelled out
31+
explicitly: for every command that has a `--sysroot` argument, every
32+
existing `-isystem PATH` argument gets a matching, explicit
33+
`-isystem SYSROOT/PATH` argument added right after it. Commands without a
34+
`--sysroot` argument are left unchanged.
35+
36+
The script also has an unrelated, optional second tweak: `--isystem-to-i`
37+
converts `-isystem` arguments to `-I`. This is useful because Cppcheck
38+
treats headers found via `-isystem` as "system" headers and suppresses some
39+
checks in them; converting to `-I` makes Cppcheck check those headers
40+
normally. Use `--exclude-folder` to keep specific include paths (e.g. a
41+
genuinely third-party library) as `-isystem` instead of converting them.
42+
43+
The script also has a third, unrelated, optional tweak: `--remove-include-path`
44+
removes any `-I PATH` argument whose path contains a given string. This is
45+
useful for stripping include paths that Cppcheck should not see at all, such
46+
as a path to a library that is being replaced by a Cppcheck library
47+
configuration, or a path that causes Cppcheck to pick up the wrong headers.
48+
49+
All tweaks understand the two forms a `compile_commands.json` entry can use:
50+
a single shell-quoted `"command"` string, or a `"arguments"` list.
51+
52+
## ARGUMENTS
53+
54+
`COMPILE_COMMANDS`
55+
: Path to the `compile_commands.json` file to read.
56+
57+
## OPTIONS
58+
59+
`-o OUTPUT`, `--output OUTPUT`
60+
: Write the result to `OUTPUT` instead of stdout. Cannot be combined with
61+
`-i`.
62+
63+
`-i`, `--in-place`
64+
: Overwrite `COMPILE_COMMANDS` with the result. Cannot be combined with
65+
`-o`.
66+
67+
`--isystem-to-i`
68+
: Also convert `-isystem PATH` arguments to `-I PATH`, except for paths
69+
excluded with `--exclude-folder`. Has no effect on its own if not given
70+
(the sysroot tweak still applies).
71+
72+
`--exclude-folder FOLDER`
73+
: When used with `--isystem-to-i`, keep any `-isystem` argument as
74+
`-isystem` (instead of converting it to `-I`) if `FOLDER` is one of the
75+
path's folder components (an exact match of a path segment, not a
76+
substring). May be given multiple times. Ignored if `--isystem-to-i` is
77+
not given.
78+
79+
`--remove-include-path PATH`
80+
: Remove any `-I` argument whose path contains `PATH` as a substring. May be
81+
given multiple times; a path is removed if it matches any of them.
82+
Independent of `--isystem-to-i`/`--exclude-folder`, and applies after them,
83+
so a path converted from `-isystem` to `-I` can also be removed by this
84+
option.
85+
86+
With neither `-o` nor `-i`, the resulting JSON is written to stdout, and
87+
the input file is left untouched. A summary (`tweaked N of M entries`) is
88+
always printed to stderr.
89+
90+
## EXAMPLES
91+
92+
Preview the sysroot tweak without touching any file:
93+
94+
```
95+
$ tools/tweak-compile-commands.py compile_commands.json
96+
```
97+
98+
Apply the sysroot tweak in place:
99+
100+
```
101+
$ tools/tweak-compile-commands.py -i compile_commands.json
102+
```
103+
104+
Apply the sysroot tweak and convert `-isystem` to `-I`, keeping any path
105+
that goes through a `lib1` or `lib2` folder as `-isystem`:
106+
107+
```
108+
$ tools/tweak-compile-commands.py -i compile_commands.json \
109+
--isystem-to-i --exclude-folder lib1 --exclude-folder lib2
110+
```
111+
112+
Given this input entry:
113+
114+
```json
115+
{
116+
"command": "gcc --sysroot /a/b -isystem /opt/x -isystem /path/lib1/include -c foo.c -o foo.o"
117+
}
118+
```
119+
120+
the last command above produces:
121+
122+
```json
123+
{
124+
"command": "gcc --sysroot /a/b -I /opt/x -I /a/b/opt/x -isystem /path/lib1/include -isystem /a/b/path/lib1/include -c foo.c -o foo.o"
125+
}
126+
```
127+
128+
Note that `/path/lib1/include` is kept as `-isystem` (matching
129+
`--exclude-folder lib1`), and so is its sysroot-relative duplicate
130+
`/a/b/path/lib1/include`, since it also contains a `lib1` folder component.
131+
132+
Remove all `-I` include paths that go through `/path/lib1`:
133+
134+
```
135+
$ tools/tweak-compile-commands.py -i compile_commands.json \
136+
--remove-include-path /path/lib1
137+
```
138+
139+
Given this input entry:
140+
141+
```json
142+
{
143+
"command": "gcc -I /opt/x -I /path/lib1/include -c foo.c -o foo.o"
144+
}
145+
```
146+
147+
the command above produces:
148+
149+
```json
150+
{
151+
"command": "gcc -I /opt/x -c foo.c -o foo.o"
152+
}
153+
```
154+
155+
## EXIT STATUS
156+
157+
Exits with a non-zero status and a traceback if `COMPILE_COMMANDS` cannot
158+
be read or does not contain valid JSON. Otherwise exits 0, even if no
159+
entries needed changes.

tools/tweak-compile-commands.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Tweaks a compile_commands.json file: for every build command that has a
4+
# --sysroot argument, each existing -isystem argument gets a matching extra
5+
# -isystem argument pointing into the sysroot. This is useful when a
6+
# compiler resolves -isystem paths relative to --sysroot internally (as
7+
# part of its built-in search path handling) but a tool consuming
8+
# compile_commands.json (such as Cppcheck) does not, so the sysroot-relative
9+
# path needs to be spelled out explicitly.
10+
#
11+
# Example:
12+
# --sysroot /a/b -isystem /opt/x
13+
# =>
14+
# --sysroot /a/b -isystem /opt/x -isystem /a/b/opt/x
15+
#
16+
# Optionally, --isystem-to-i converts -isystem arguments to -I, which can be
17+
# useful since Cppcheck otherwise treats -isystem headers as "system"
18+
# headers and skips some checks in them. Paths whose folder name matches one
19+
# of the --exclude-folder values are left as -isystem. For example, with
20+
# --isystem-to-i --exclude-folder lib1:
21+
# -isystem /opt/x -isystem /path/lib1/include
22+
# =>
23+
# -I /opt/x -isystem /path/lib1/include
24+
#
25+
# Optionally, --remove-include-path removes -I arguments whose path contains
26+
# the given path. For example, with --remove-include-path /path/lib1:
27+
# -I /opt/x -I /path/lib1/include
28+
# =>
29+
# -I /opt/x
30+
#
31+
# Usage:
32+
# tools/tweak-compile-commands.py compile_commands.json -o out.json
33+
# tools/tweak-compile-commands.py -i compile_commands.json
34+
# tools/tweak-compile-commands.py -i compile_commands.json --isystem-to-i --exclude-folder lib1
35+
# tools/tweak-compile-commands.py -i compile_commands.json --remove-include-path /path/lib1
36+
#
37+
# With neither -o nor -i, the result is written to stdout.
38+
39+
import argparse
40+
import json
41+
import shlex
42+
import sys
43+
44+
45+
def find_sysroot(tokens):
46+
for i, tok in enumerate(tokens):
47+
if tok == '--sysroot' and i + 1 < len(tokens):
48+
return tokens[i + 1]
49+
if tok.startswith('--sysroot='):
50+
return tok[len('--sysroot='):]
51+
return None
52+
53+
54+
def join_sysroot(sysroot, path):
55+
return sysroot.rstrip('/') + '/' + path.lstrip('/')
56+
57+
58+
def add_isystem_sysroot(tokens, sysroot):
59+
result = []
60+
i = 0
61+
n = len(tokens)
62+
while i < n:
63+
tok = tokens[i]
64+
if tok == '-isystem' and i + 1 < n:
65+
path = tokens[i + 1]
66+
result.append(tok)
67+
result.append(path)
68+
result.append('-isystem')
69+
result.append(join_sysroot(sysroot, path))
70+
i += 2
71+
continue
72+
if tok.startswith('-isystem') and tok != '-isystem':
73+
path = tok[len('-isystem'):]
74+
result.append(tok)
75+
result.append('-isystem' + join_sysroot(sysroot, path))
76+
i += 1
77+
continue
78+
result.append(tok)
79+
i += 1
80+
return result
81+
82+
83+
def is_excluded(path, exclude_folders):
84+
parts = path.replace('\\', '/').split('/')
85+
return any(part in exclude_folders for part in parts if part)
86+
87+
88+
def convert_isystem_to_i(tokens, exclude_folders):
89+
result = []
90+
i = 0
91+
n = len(tokens)
92+
while i < n:
93+
tok = tokens[i]
94+
if tok == '-isystem' and i + 1 < n:
95+
path = tokens[i + 1]
96+
result.append(tok if is_excluded(path, exclude_folders) else '-I')
97+
result.append(path)
98+
i += 2
99+
continue
100+
if tok.startswith('-isystem') and tok != '-isystem':
101+
path = tok[len('-isystem'):]
102+
result.append(tok if is_excluded(path, exclude_folders) else '-I' + path)
103+
i += 1
104+
continue
105+
result.append(tok)
106+
i += 1
107+
return result
108+
109+
110+
def matches_remove_path(path, remove_paths):
111+
normalized = path.replace('\\', '/')
112+
return any(remove_path.replace('\\', '/') in normalized for remove_path in remove_paths)
113+
114+
115+
def remove_include_paths(tokens, remove_paths):
116+
result = []
117+
i = 0
118+
n = len(tokens)
119+
while i < n:
120+
tok = tokens[i]
121+
if tok == '-I' and i + 1 < n:
122+
path = tokens[i + 1]
123+
if matches_remove_path(path, remove_paths):
124+
i += 2
125+
continue
126+
result.append(tok)
127+
result.append(path)
128+
i += 2
129+
continue
130+
if tok.startswith('-I') and tok != '-I':
131+
path = tok[len('-I'):]
132+
if matches_remove_path(path, remove_paths):
133+
i += 1
134+
continue
135+
result.append(tok)
136+
i += 1
137+
continue
138+
result.append(tok)
139+
i += 1
140+
return result
141+
142+
143+
def tweak_entry(entry, isystem_to_i, exclude_folders, remove_include_path):
144+
if 'command' in entry:
145+
tokens = shlex.split(entry['command'])
146+
elif 'arguments' in entry:
147+
tokens = entry['arguments']
148+
else:
149+
return False
150+
151+
changed = False
152+
153+
sysroot = find_sysroot(tokens)
154+
if sysroot is not None:
155+
tokens = add_isystem_sysroot(tokens, sysroot)
156+
changed = True
157+
158+
if isystem_to_i:
159+
new_tokens = convert_isystem_to_i(tokens, exclude_folders)
160+
if new_tokens != tokens:
161+
tokens = new_tokens
162+
changed = True
163+
164+
if remove_include_path:
165+
new_tokens = remove_include_paths(tokens, remove_include_path)
166+
if new_tokens != tokens:
167+
tokens = new_tokens
168+
changed = True
169+
170+
if not changed:
171+
return False
172+
173+
if 'command' in entry:
174+
entry['command'] = shlex.join(tokens)
175+
else:
176+
entry['arguments'] = tokens
177+
return True
178+
179+
180+
def main():
181+
parser = argparse.ArgumentParser(
182+
description='Add sysroot-relative -isystem arguments to build commands in a compile_commands.json file.')
183+
parser.add_argument('compile_commands', help='path to the compile_commands.json file to read')
184+
group = parser.add_mutually_exclusive_group()
185+
group.add_argument('-o', '--output', help='write the result to this file instead of stdout')
186+
group.add_argument('-i', '--in-place', action='store_true', help='overwrite the input file with the result')
187+
parser.add_argument('--isystem-to-i', action='store_true',
188+
help='convert -isystem arguments to -I (except excluded folders)')
189+
parser.add_argument('--exclude-folder', action='append', default=[], metavar='FOLDER',
190+
help='folder name to keep as -isystem when using --isystem-to-i; can be given multiple times')
191+
parser.add_argument('--remove-include-path', action='append', default=[], metavar='PATH',
192+
help='remove -I arguments whose path contains PATH; can be given multiple times')
193+
args = parser.parse_args()
194+
195+
with open(args.compile_commands, encoding='utf-8') as f:
196+
entries = json.load(f)
197+
198+
changed = 0
199+
for entry in entries:
200+
if tweak_entry(entry, args.isystem_to_i, args.exclude_folder, args.remove_include_path):
201+
changed += 1
202+
203+
out = json.dumps(entries, indent=2) + '\n'
204+
205+
if args.in_place:
206+
with open(args.compile_commands, 'w', encoding='utf-8') as f:
207+
f.write(out)
208+
elif args.output:
209+
with open(args.output, 'w', encoding='utf-8') as f:
210+
f.write(out)
211+
else:
212+
sys.stdout.write(out)
213+
214+
print('tweaked {} of {} entries'.format(changed, len(entries)), file=sys.stderr)
215+
216+
217+
if __name__ == '__main__':
218+
main()

0 commit comments

Comments
 (0)