Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New CLI subcommand to create clang-compatible compilation database (compile_commands.json) #14370

Merged
merged 44 commits into from
Sep 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
47365ae
pulled source from dev branch
Apr 25, 2020
e0bff4d
missed a file from origin
Apr 25, 2020
5a51eaa
formatting
Apr 25, 2020
d817b7d
revised argument names. relaxed matching rules to work for avr too
Apr 25, 2020
30b564f
add docstrings
Apr 25, 2020
849a12f
added docs. tightened up regex
Apr 25, 2020
b671c40
remove unused imports
Apr 25, 2020
f42cadc
cleaning up command file. use existing qmk dir constant
May 9, 2020
e768fac
rename parser library file
May 9, 2020
21c5b63
move lib functions into command file. there are only 2 and they aren'…
May 9, 2020
3fe7817
currently debugging...
May 9, 2020
ec3739b
more robustly find config
May 9, 2020
bf2aef2
updated docs
May 9, 2020
10171f8
remove unused imports
May 9, 2020
697ca67
reuse make executable from the main make command
May 19, 2020
82a8be3
pulled source from dev branch
Apr 25, 2020
3a95195
missed a file from origin
Apr 25, 2020
3ed55aa
formatting
Apr 25, 2020
c9a4d97
revised argument names. relaxed matching rules to work for avr too
Apr 25, 2020
9226ba5
add docstrings
Apr 25, 2020
764f35d
added docs. tightened up regex
Apr 25, 2020
b0a5e1a
remove unused imports
Apr 25, 2020
67c08c6
cleaning up command file. use existing qmk dir constant
May 9, 2020
c0fc66e
rename parser library file
May 9, 2020
2f103f5
move lib functions into command file. there are only 2 and they aren'…
May 9, 2020
c03da56
currently debugging...
May 9, 2020
8db4e13
more robustly find config
May 9, 2020
67ae7d8
updated docs
May 9, 2020
976b0b1
remove unused imports
May 9, 2020
7ab3465
reuse make executable from the main make command
May 19, 2020
16c47fd
Merge branch 'compile_commands' of https://github.com/xton/qmk_firmwa…
Aug 24, 2020
70dfd86
Merge branch 'master' into compile_commands
Sep 7, 2020
c558c5d
remove MAKEFLAGS from environment for better control over process man…
Sep 7, 2020
afb2527
Update .gitignore
xton Sep 27, 2020
f64f64a
add a usage line to docs
Oct 11, 2020
3830823
Merge branch 'compile_commands' of https://github.com/xton/qmk_firmwa…
Oct 11, 2020
6cbcc9c
doc change as suggested
xton Nov 30, 2020
bae3306
Merge remote-tracking branch 'xton/compile_commands' into develop
baodrate Sep 10, 2021
13044e0
rename command
baodrate Sep 10, 2021
ecffaa4
Merge branch 'develop' into compile_commands
baodrate Sep 10, 2021
42014f0
remove debug print statements
baodrate Sep 10, 2021
d8d0f25
generate-compilation-database: fix arg handling
baodrate Sep 10, 2021
7df7b9e
generate-comilation-db: improve error handling
baodrate Sep 10, 2021
66e090d
use cli.run() instead of Popen()
baodrate Sep 10, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,8 @@ __pycache__

# Allow to exist but don't include it in the repo
user_song_list.h

# clangd
compile_commands.json
.clangd/
.cache/
27 changes: 27 additions & 0 deletions docs/cli_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,33 @@ qmk format-c
qmk format-c -b branch_name
```

## `qmk generate-compilation-database`

**Usage**:

```
qmk generate-compilation-database [-kb KEYBOARD] [-km KEYMAP]
```

Creates a `compile_commands.json` file.

Does 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.

This 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.

**Example:**

```
$ cd ~/qmk_firmware/keyboards/gh60/satan/keymaps/colemak
$ qmk generate-compilation-database
Ψ Making clean
Ψ Gathering build instructions from make -n gh60/satan:colemak
Ψ Found 50 compile commands
Ψ Writing build database to /Users/you/src/qmk_firmware/compile_commands.json
```

Now open your dev environment and live a squiggly-free life.

## `qmk docs`

This command starts a local HTTP server which you can use for browsing or improving the docs. Default port is 8936.
Expand Down
1 change: 1 addition & 0 deletions lib/python/qmk/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
'qmk.cli.format.python',
'qmk.cli.format.text',
'qmk.cli.generate.api',
'qmk.cli.generate.compilation_database',
'qmk.cli.generate.config_h',
'qmk.cli.generate.dfu_header',
'qmk.cli.generate.docs',
Expand Down
123 changes: 123 additions & 0 deletions lib/python/qmk/cli/generate/compilation_database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""Creates a compilation database for the given keyboard build.
"""

import itertools
import json
import os
import re
import shlex
import shutil
from functools import lru_cache
from pathlib import Path
from typing import Dict, Iterator, List, Union

from milc import cli, MILC

from qmk.commands import create_make_command
from qmk.constants import QMK_FIRMWARE
from qmk.decorators import automagic_keyboard, automagic_keymap


@lru_cache(maxsize=10)
def system_libs(binary: str) -> List[Path]:
"""Find the system include directory that the given build tool uses.
"""
cli.log.debug("searching for system library directory for binary: %s", binary)
bin_path = shutil.which(binary)
return list(Path(bin_path).resolve().parent.parent.glob("*/include")) if bin_path else []


file_re = re.compile(r'printf "Compiling: ([^"]+)')
cmd_re = re.compile(r'LOG=\$\((.+?)&&')


def parse_make_n(f: Iterator[str]) -> List[Dict[str, str]]:
"""parse the output of `make -n <target>`

This function makes many assumptions about the format of your build log.
This happens to work right now for qmk.
"""

state = 'start'
this_file = None
records = []
for line in f:
if state == 'start':
m = file_re.search(line)
if m:
this_file = m.group(1)
state = 'cmd'

if state == 'cmd':
assert this_file
m = cmd_re.search(line)
if m:
# we have a hit!
this_cmd = m.group(1)
args = shlex.split(this_cmd)
args += ['-I%s' % s for s in system_libs(args[0])]
new_cmd = ' '.join(shlex.quote(s) for s in args if s != '-mno-thumb-interwork')
records.append({"directory": str(QMK_FIRMWARE.resolve()), "command": new_cmd, "file": this_file})
state = 'start'

return records


@cli.argument('-kb', '--keyboard', help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.')
@cli.argument('-km', '--keymap', help='The keymap to build a firmware for. Ignored when a configurator export is supplied.')
@cli.subcommand('Create a compilation database.')
@automagic_keyboard
@automagic_keymap
def generate_compilation_database(cli: MILC) -> Union[bool, int]:
"""Creates a compilation database for the given keyboard build.

Does a make clean, then a make -n for this target and uses the dry-run output to create
a compilation database (compile_commands.json). This file can help some IDEs and
IDE-like editors work better. For more information about this:

https://clang.llvm.org/docs/JSONCompilationDatabase.html
"""
command = None
# check both config domains: the magic decorator fills in `generate_compilation_database` but the user is
# more likely to have set `compile` in their config file.
current_keyboard = cli.config.generate_compilation_database.keyboard or cli.config.user.keyboard
current_keymap = cli.config.generate_compilation_database.keymap or cli.config.user.keymap

if current_keyboard and current_keymap:
# Generate the make command for a specific keyboard/keymap.
command = create_make_command(current_keyboard, current_keymap, dry_run=True)
elif not current_keyboard:
cli.log.error('Could not determine keyboard!')
elif not current_keymap:
cli.log.error('Could not determine keymap!')

if not command:
cli.log.error('You must supply both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.')
cli.echo('usage: qmk compiledb [-kb KEYBOARD] [-km KEYMAP]')
return False

# remove any environment variable overrides which could trip us up
env = os.environ.copy()
env.pop("MAKEFLAGS", None)

# re-use same executable as the main make invocation (might be gmake)
clean_command = [command[0], 'clean']
cli.log.info('Making clean with {fg_cyan}%s', ' '.join(clean_command))
cli.run(clean_command, capture_output=False, check=True, env=env)

cli.log.info('Gathering build instructions from {fg_cyan}%s', ' '.join(command))

result = cli.run(command, capture_output=True, check=True, env=env)
db = parse_make_n(result.stdout.splitlines())
if not db:
cli.log.error("Failed to parse output from make output:\n%s", result.stdout)
return False

cli.log.info("Found %s compile commands", len(db))

dbpath = QMK_FIRMWARE / 'compile_commands.json'

cli.log.info(f"Writing build database to {dbpath}")
dbpath.write_text(json.dumps(db, indent=4))

return True
14 changes: 10 additions & 4 deletions lib/python/qmk/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,17 @@ def _find_make():
return make_cmd


def create_make_target(target, parallel=1, **env_vars):
def create_make_target(target, dry_run=False, parallel=1, **env_vars):
"""Create a make command

Args:

target
Usually a make rule, such as 'clean' or 'all'.

dry_run
make -n -- don't actually build

parallel
The number of make jobs to run in parallel

Expand All @@ -52,10 +55,10 @@ def create_make_target(target, parallel=1, **env_vars):
for key, value in env_vars.items():
env.append(f'{key}={value}')

return [make_cmd, *get_make_parallel_args(parallel), *env, target]
return [make_cmd, *(['-n'] if dry_run else []), *get_make_parallel_args(parallel), *env, target]


def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):
def create_make_command(keyboard, keymap, target=None, dry_run=False, parallel=1, **env_vars):
"""Create a make compile command

Args:
Expand All @@ -69,6 +72,9 @@ def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):
target
Usually a bootloader.

dry_run
make -n -- don't actually build

parallel
The number of make jobs to run in parallel

Expand All @@ -84,7 +90,7 @@ def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):
if target:
make_args.append(target)

return create_make_target(':'.join(make_args), parallel, **env_vars)
return create_make_target(':'.join(make_args), dry_run=dry_run, parallel=parallel, **env_vars)


def get_git_version(current_time, repo_dir='.', check_dir='.'):
Expand Down