#!/usr/bin/env python
# -*- coding: utf-8 -*-
import json
import sys
import re
'''A simple compilation database generator, or cdg in short.
It works by parsing the output of the GNU make command.'''
file_name_regex = re.compile(r"[\w./+\-]+\.(s|cc?|cpp|cxx)\b",
re.IGNORECASE)
enter_dir_regex = re.compile(r"^\s*(?:make|ninja)(?:\[\d+\])?: Entering directory [`\'\"](?P
.*)[`\'\"]\s*$",
re.MULTILINE)
leave_dir_regex = re.compile(r"^\s*(?:make|ninja)(?:\[\d+\])?: Leaving directory .*$",
re.MULTILINE)
compilers_regex = re.compile(r'\b(g?cc|[gc]\+\+|clang\+?\+?|icecc|s?ccache)(?:.exe)?"?\s')
def parse(make_output):
'''Parse the make output into a list of objects.
Per https://clang.llvm.org/docs/JSONCompilationDatabase.html
'''
result = []
pwd = ""
path_stack = []
for line in make_output.replace('\r', '').split('\n'):
line = line.strip()
enter_dir_match = enter_dir_regex.match(line)
if enter_dir_match:
pwd = enter_dir_match.group('dir')
path_stack.append(pwd)
# logger.debug("stack after append: {}".format(path_stack))
continue
elif leave_dir_regex.match(line):
# logger.debug("stack before pop: {}".format(path_stack))
path_stack.pop()
if path_stack:
pwd = path_stack[-1]
continue
match = compilers_regex.search(line)
if not match:
continue
# look backward and discard anything before delimiters
i = match.start()
if line[match.start():match.end()].rstrip()[-1] == '"':
while i > 0:
i -= 1
if line[i] == '"' and (i == 0 or line[i-1] != '\\'):
break
else:
while i > 0:
j = i - 1
if line[j] in (' ', '\t', '\n', ';', '&'):
break
i -= 1
line = line[i:]
file_match = file_name_regex.search(line)
# logger.debug(line, file_match)
if not file_match:
continue
# To workaround that there is no "entering directory..."
if not pwd:
pwd = "/path/to/your/project/"
path_stack.append(pwd)
# Special handling for projects like Redis,
# which has output like "printf xxx; cc xxx"
command = line
ri = command.rfind(';')
if -1 != ri:
command = command[:ri]
ri = command.rfind('&&')
if -1 != ri:
command = command[:ri]
result.append({
"directory": pwd.strip(),
"file": file_match.group(0).strip(),
"command": command.strip(),
})
return result
def usage():
print('''Usage: {} [compilation-db-file]
[compilation-db-file] is optional, which is `compile_commands.json` by default.
Specify it if you want to write to another file, and specify `-` for stdout.
This CLI program takes GNU make output from stdin from a pipe,
parse it, and write the json string to a file.
'''.format(sys.argv[0]))
sys.exit(1)
def main():
make_output = sys.stdin.read().strip()
if not make_output:
usage()
db = json.dumps(parse(make_output),
indent=2) + '\n'
file_name = 'compile_commands.json' if len(sys.argv) == 1 else sys.argv[1]
if '-' != file_name:
with open(file_name, "w") as f:
f.write(db)
else:
sys.stdout.write(db)
if __name__ == '__main__':
main()