aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBao <qubidt@gmail.com>2021-09-15 23:59:57 -0500
committerGitHub <noreply@github.com>2021-09-16 14:59:57 +1000
commit590b405468fec906a51767a5cda4aa30ada5d52f (patch)
tree0c907b8836440475e7b2800737800f4d50d81649
parentf7054522106644a5fd9ee58b5117a44b3209b7b2 (diff)
downloadqmk_firmware-590b405468fec906a51767a5cda4aa30ada5d52f.tar.gz
qmk_firmware-590b405468fec906a51767a5cda4aa30ada5d52f.zip
New CLI subcommand to create clang-compatible compilation database (`compile_commands.json`) (#14370)
* pulled source from dev branch * missed a file from origin * formatting * revised argument names. relaxed matching rules to work for avr too * add docstrings * added docs. tightened up regex * remove unused imports * cleaning up command file. use existing qmk dir constant * rename parser library file * move lib functions into command file. there are only 2 and they aren't large * currently debugging... * more robustly find config * updated docs * remove unused imports * reuse make executable from the main make command * pulled source from dev branch * missed a file from origin * formatting * revised argument names. relaxed matching rules to work for avr too * add docstrings * added docs. tightened up regex * remove unused imports * cleaning up command file. use existing qmk dir constant * rename parser library file * move lib functions into command file. there are only 2 and they aren't large * currently debugging... * more robustly find config * updated docs * remove unused imports * reuse make executable from the main make command * remove MAKEFLAGS from environment for better control over process management * Update .gitignore Co-authored-by: Michael Forster <forster@google.com> * add a usage line to docs * doc change as suggested Co-authored-by: Nick Brassel <nick@tzarc.org> * rename command * remove debug print statements * generate-compilation-database: fix arg handling * generate-comilation-db: improve error handling * use cli.run() instead of Popen() Co-authored-by: Xton <cdewan@apple.com> Co-authored-by: Christon DeWan <cmdpix@mac.com> Co-authored-by: Michael Forster <forster@google.com> Co-authored-by: Nick Brassel <nick@tzarc.org>
-rw-r--r--.gitignore5
-rw-r--r--docs/cli_commands.md27
-rw-r--r--lib/python/qmk/cli/__init__.py1
-rwxr-xr-xlib/python/qmk/cli/generate/compilation_database.py123
-rw-r--r--lib/python/qmk/commands.py14
5 files changed, 166 insertions, 4 deletions
diff --git a/.gitignore b/.gitignore
index f3b76f7ed..768f40b19 100644
--- a/.gitignore
+++ b/.gitignore
@@ -86,3 +86,8 @@ __pycache__
86 86
87# Allow to exist but don't include it in the repo 87# Allow to exist but don't include it in the repo
88user_song_list.h 88user_song_list.h
89
90# clangd
91compile_commands.json
92.clangd/
93.cache/
diff --git a/docs/cli_commands.md b/docs/cli_commands.md
index 06568afb4..1427b592f 100644
--- a/docs/cli_commands.md
+++ b/docs/cli_commands.md
@@ -320,6 +320,33 @@ qmk format-c
320qmk format-c -b branch_name 320qmk format-c -b branch_name
321``` 321```
322 322
323## `qmk generate-compilation-database`
324
325**Usage**:
326
327```
328qmk generate-compilation-database [-kb KEYBOARD] [-km KEYMAP]
329```
330
331Creates a `compile_commands.json` file.
332
333Does your IDE/editor use a language server but doesn't _quite_ find all the necessary include files? Do you hate red squigglies? Do you wish your editor could figure out `#include QMK_KEYBOARD_H`? You might need a [compilation database](https://clang.llvm.org/docs/JSONCompilationDatabase.html)! The qmk tool can build this for you.
334
335This command needs to know which keyboard and keymap to build. It uses the same configuration options as the `qmk compile` command: arguments, current directory, and config files.
336
337**Example:**
338
339```
340$ cd ~/qmk_firmware/keyboards/gh60/satan/keymaps/colemak
341$ qmk generate-compilation-database
342Ψ Making clean
343Ψ Gathering build instructions from make -n gh60/satan:colemak
344Ψ Found 50 compile commands
345Ψ Writing build database to /Users/you/src/qmk_firmware/compile_commands.json
346```
347
348Now open your dev environment and live a squiggly-free life.
349
323## `qmk docs` 350## `qmk docs`
324 351
325This command starts a local HTTP server which you can use for browsing or improving the docs. Default port is 8936. 352This command starts a local HTTP server which you can use for browsing or improving the docs. Default port is 8936.
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
index 292dcbe81..944938824 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -44,6 +44,7 @@ subcommands = [
44 'qmk.cli.format.python', 44 'qmk.cli.format.python',
45 'qmk.cli.format.text', 45 'qmk.cli.format.text',
46 'qmk.cli.generate.api', 46 'qmk.cli.generate.api',
47 'qmk.cli.generate.compilation_database',
47 'qmk.cli.generate.config_h', 48 'qmk.cli.generate.config_h',
48 'qmk.cli.generate.dfu_header', 49 'qmk.cli.generate.dfu_header',
49 'qmk.cli.generate.docs', 50 'qmk.cli.generate.docs',
diff --git a/lib/python/qmk/cli/generate/compilation_database.py b/lib/python/qmk/cli/generate/compilation_database.py
new file mode 100755
index 000000000..2748d96e7
--- /dev/null
+++ b/lib/python/qmk/cli/generate/compilation_database.py
@@ -0,0 +1,123 @@
1"""Creates a compilation database for the given keyboard build.
2"""
3
4import itertools
5import json
6import os
7import re
8import shlex
9import shutil
10from functools import lru_cache
11from pathlib import Path
12from typing import Dict, Iterator, List, Union
13
14from milc import cli, MILC
15
16from qmk.commands import create_make_command
17from qmk.constants import QMK_FIRMWARE
18from qmk.decorators import automagic_keyboard, automagic_keymap
19
20
21@lru_cache(maxsize=10)
22def system_libs(binary: str) -> List[Path]:
23 """Find the system include directory that the given build tool uses.
24 """
25 cli.log.debug("searching for system library directory for binary: %s", binary)
26 bin_path = shutil.which(binary)
27 return list(Path(bin_path).resolve().parent.parent.glob("*/include")) if bin_path else []
28
29
30file_re = re.compile(r'printf "Compiling: ([^"]+)')
31cmd_re = re.compile(r'LOG=\$\((.+?)&&')
32
33
34def parse_make_n(f: Iterator[str]) -> List[Dict[str, str]]:
35 """parse the output of `make -n <target>`
36
37 This function makes many assumptions about the format of your build log.
38 This happens to work right now for qmk.
39 """
40
41 state = 'start'
42 this_file = None
43 records = []
44 for line in f:
45 if state == 'start':
46 m = file_re.search(line)
47 if m:
48 this_file = m.group(1)
49 state = 'cmd'
50
51 if state == 'cmd':
52 assert this_file
53 m = cmd_re.search(line)
54 if m:
55 # we have a hit!
56 this_cmd = m.group(1)
57 args = shlex.split(this_cmd)
58 args += ['-I%s' % s for s in system_libs(args[0])]
59 new_cmd = ' '.join(shlex.quote(s) for s in args if s != '-mno-thumb-interwork')
60 records.append({"directory": str(QMK_FIRMWARE.resolve()), "command": new_cmd, "file": this_file})
61 state = 'start'
62
63 return records
64
65
66@cli.argument('-kb', '--keyboard', help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.')
67@cli.argument('-km', '--keymap', help='The keymap to build a firmware for. Ignored when a configurator export is supplied.')
68@cli.subcommand('Create a compilation database.')
69@automagic_keyboard
70@automagic_keymap
71def generate_compilation_database(cli: MILC) -> Union[bool, int]:
72 """Creates a compilation database for the given keyboard build.
73
74 Does a make clean, then a make -n for this target and uses the dry-run output to create
75 a compilation database (compile_commands.json). This file can help some IDEs and
76 IDE-like editors work better. For more information about this:
77
78 https://clang.llvm.org/docs/JSONCompilationDatabase.html
79 """
80 command = None
81 # check both config domains: the magic decorator fills in `generate_compilation_database` but the user is
82 # more likely to have set `compile` in their config file.
83 current_keyboard = cli.config.generate_compilation_database.keyboard or cli.config.user.keyboard
84 current_keymap = cli.config.generate_compilation_database.keymap or cli.config.user.keymap
85
86 if current_keyboard and current_keymap:
87 # Generate the make command for a specific keyboard/keymap.
88 command = create_make_command(current_keyboard, current_keymap, dry_run=True)
89 elif not current_keyboard:
90 cli.log.error('Could not determine keyboard!')
91 elif not current_keymap:
92 cli.log.error('Could not determine keymap!')
93
94 if not command:
95 cli.log.error('You must supply both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.')
96 cli.echo('usage: qmk compiledb [-kb KEYBOARD] [-km KEYMAP]')
97 return False
98
99 # remove any environment variable overrides which could trip us up
100 env = os.environ.copy()
101 env.pop("MAKEFLAGS", None)
102
103 # re-use same executable as the main make invocation (might be gmake)
104 clean_command = [command[0], 'clean']
105 cli.log.info('Making clean with {fg_cyan}%s', ' '.join(clean_command))
106 cli.run(clean_command, capture_output=False, check=True, env=env)
107
108 cli.log.info('Gathering build instructions from {fg_cyan}%s', ' '.join(command))
109
110 result = cli.run(command, capture_output=True, check=True, env=env)
111 db = parse_make_n(result.stdout.splitlines())
112 if not db:
113 cli.log.error("Failed to parse output from make output:\n%s", result.stdout)
114 return False
115
116 cli.log.info("Found %s compile commands", len(db))
117
118 dbpath = QMK_FIRMWARE / 'compile_commands.json'
119
120 cli.log.info(f"Writing build database to {dbpath}")
121 dbpath.write_text(json.dumps(db, indent=4))
122
123 return True
diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py
index 01c23b261..2995a5fda 100644
--- a/lib/python/qmk/commands.py
+++ b/lib/python/qmk/commands.py
@@ -28,7 +28,7 @@ def _find_make():
28 return make_cmd 28 return make_cmd
29 29
30 30
31def create_make_target(target, parallel=1, **env_vars): 31def create_make_target(target, dry_run=False, parallel=1, **env_vars):
32 """Create a make command 32 """Create a make command
33 33
34 Args: 34 Args:
@@ -36,6 +36,9 @@ def create_make_target(target, parallel=1, **env_vars):
36 target 36 target
37 Usually a make rule, such as 'clean' or 'all'. 37 Usually a make rule, such as 'clean' or 'all'.
38 38
39 dry_run
40 make -n -- don't actually build
41
39 parallel 42 parallel
40 The number of make jobs to run in parallel 43 The number of make jobs to run in parallel
41 44
@@ -52,10 +55,10 @@ def create_make_target(target, parallel=1, **env_vars):
52 for key, value in env_vars.items(): 55 for key, value in env_vars.items():
53 env.append(f'{key}={value}') 56 env.append(f'{key}={value}')
54 57
55 return [make_cmd, *get_make_parallel_args(parallel), *env, target] 58 return [make_cmd, *(['-n'] if dry_run else []), *get_make_parallel_args(parallel), *env, target]
56 59
57 60
58def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars): 61def create_make_command(keyboard, keymap, target=None, dry_run=False, parallel=1, **env_vars):
59 """Create a make compile command 62 """Create a make compile command
60 63
61 Args: 64 Args:
@@ -69,6 +72,9 @@ def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):
69 target 72 target
70 Usually a bootloader. 73 Usually a bootloader.
71 74
75 dry_run
76 make -n -- don't actually build
77
72 parallel 78 parallel
73 The number of make jobs to run in parallel 79 The number of make jobs to run in parallel
74 80
@@ -84,7 +90,7 @@ def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):
84 if target: 90 if target:
85 make_args.append(target) 91 make_args.append(target)
86 92
87 return create_make_target(':'.join(make_args), parallel, **env_vars) 93 return create_make_target(':'.join(make_args), dry_run=dry_run, parallel=parallel, **env_vars)
88 94
89 95
90def get_git_version(current_time, repo_dir='.', check_dir='.'): 96def get_git_version(current_time, repo_dir='.', check_dir='.'):