Skip to content

Commit 3c0cded

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

2 files changed

Lines changed: 387 additions & 0 deletions

File tree

tools/tweak-compile-commands.md

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# tweak-compile-commands.py
2+
3+
## NAME
4+
5+
tweak-compile-commands.py - tweak `-isystem`/`--sysroot`/`-I` 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+
Cppcheck internally does not have handling of `--sysroot` and skips
18+
`-isystem` paths.
19+
20+
In many cases the system headers should not be included in Cppcheck
21+
analysis, it is preferable to use `--library` instead. The headers
22+
do not provide the knowledge needed for static analysis, i.e. they
23+
can say what types the arguments to a function has but the header
24+
do not provide the semantics of the functions.
25+
26+
However sometimes you do want to include system headers in Cppcheck
27+
analysis. And you need to have handling of `--sysroot` and
28+
`-isystem`. This script will tweak the compile_commands.json file.
29+
30+
### SYSROOT
31+
32+
Example build command such as:
33+
34+
```
35+
gcc --sysroot /a/b -isystem /opt/x -c foo.c
36+
```
37+
38+
gcc searches both `/opt/x` *and* `/a/b/opt/x` for headers.
39+
40+
`tweak-compile-commands.py` rewrites each build command in a
41+
`compile_commands.json` file so this implicit behaviour is spelled out
42+
explicitly: for every command that has a `--sysroot` argument, every
43+
existing `-isystem PATH` argument gets a matching, explicit
44+
`-isystem SYSROOT/PATH` argument added right after it. Commands without a
45+
`--sysroot` argument are left unchanged.
46+
47+
### ISYSTEM
48+
49+
The script has an option `--isystem-to-i`, this tells the script to
50+
convert `-isystem` arguments to `-I`. This will effectively tell
51+
Cppcheck to not ignore the path.
52+
53+
The option `--exclude-folder` can be used to skip certain folders. Use
54+
that for a folder if Cppcheck option `--library` can be used instead.
55+
56+
### REMOVE -I
57+
58+
The script also has `--remove-include-path`, the script will remove
59+
any `-I PATH` argument whose path contains a given string. This is
60+
useful for stripping include paths that Cppcheck should not see at all.
61+
62+
## ARGUMENTS
63+
64+
`COMPILE_COMMANDS`
65+
: Path to the `compile_commands.json` file to read.
66+
67+
## OPTIONS
68+
69+
`-o OUTPUT`, `--output OUTPUT`
70+
: Write the result to `OUTPUT` instead of stdout. Cannot be combined with
71+
`-i`.
72+
73+
`-i`, `--in-place`
74+
: Overwrite `COMPILE_COMMANDS` with the result. Cannot be combined with
75+
`-o`.
76+
77+
`--isystem-to-i`
78+
: Also convert `-isystem PATH` arguments to `-I PATH`, except for paths
79+
excluded with `--exclude-folder`. Has no effect on its own if not given
80+
(the sysroot tweak still applies).
81+
82+
`--exclude-folder FOLDER`
83+
: When used with `--isystem-to-i`, keep any `-isystem` argument as
84+
`-isystem` (instead of converting it to `-I`) if `FOLDER` is one of the
85+
path's folder components (an exact match of a path segment, not a
86+
substring). May be given multiple times. Ignored if `--isystem-to-i` is
87+
not given.
88+
89+
`--remove-include-path PATH`
90+
: Remove any `-I` argument whose path contains `PATH` as a substring. May be
91+
given multiple times; a path is removed if it matches any of them.
92+
Independent of `--isystem-to-i`/`--exclude-folder`, and applies after them,
93+
so a path converted from `-isystem` to `-I` can also be removed by this
94+
option.
95+
96+
With neither `-o` nor `-i`, the resulting JSON is written to stdout, and
97+
the input file is left untouched. A summary (`tweaked N of M entries`) is
98+
always printed to stderr.
99+
100+
## EXAMPLES
101+
102+
Preview the sysroot tweak without touching any file:
103+
104+
```
105+
$ tools/tweak-compile-commands.py compile_commands.json
106+
```
107+
108+
Apply the sysroot tweak in place:
109+
110+
```
111+
$ tools/tweak-compile-commands.py -i compile_commands.json
112+
```
113+
114+
Apply the sysroot tweak and convert `-isystem` to `-I`, keeping any path
115+
that goes through a `lib1` or `lib2` folder as `-isystem`:
116+
117+
```
118+
$ tools/tweak-compile-commands.py -i compile_commands.json \
119+
--isystem-to-i --exclude-folder lib1 --exclude-folder lib2
120+
```
121+
122+
Given this input entry:
123+
124+
```json
125+
{
126+
"command": "gcc --sysroot /a/b -isystem /opt/x -isystem /path/lib1/include -c foo.c -o foo.o"
127+
}
128+
```
129+
130+
the last command above produces:
131+
132+
```json
133+
{
134+
"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"
135+
}
136+
```
137+
138+
Note that `/path/lib1/include` is kept as `-isystem` (matching
139+
`--exclude-folder lib1`), and so is its sysroot-relative duplicate
140+
`/a/b/path/lib1/include`, since it also contains a `lib1` folder component.
141+
142+
Remove all `-I` include paths that go through `/path/lib1`:
143+
144+
```
145+
$ tools/tweak-compile-commands.py -i compile_commands.json \
146+
--remove-include-path /path/lib1
147+
```
148+
149+
Given this input entry:
150+
151+
```json
152+
{
153+
"command": "gcc -I /opt/x -I /path/lib1/include -c foo.c -o foo.o"
154+
}
155+
```
156+
157+
the command above produces:
158+
159+
```json
160+
{
161+
"command": "gcc -I /opt/x -c foo.c -o foo.o"
162+
}
163+
```
164+
165+
## EXIT STATUS
166+
167+
Exits with a non-zero status and a traceback if `COMPILE_COMMANDS` cannot
168+
be read or does not contain valid JSON. Otherwise exits 0, even if no
169+
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)