#!/usr/bin/env python
# -*- coding: utf-8 -*-
import json
import sys
import re
from os import getcwd
'''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 = getcwd()
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()