diff options
Diffstat (limited to 'lib/python/qmk')
-rw-r--r-- | lib/python/qmk/cli/__init__.py | 2 | ||||
-rw-r--r-- | lib/python/qmk/cli/console.py | 303 | ||||
-rw-r--r-- | lib/python/qmk/cli/doctor/check.py | 1 | ||||
-rwxr-xr-x | lib/python/qmk/cli/format/python.py | 6 | ||||
-rwxr-xr-x | lib/python/qmk/cli/generate/compilation_database.py | 133 | ||||
-rw-r--r-- | lib/python/qmk/cli/lint.py | 145 | ||||
-rw-r--r-- | lib/python/qmk/cli/pytest.py | 2 | ||||
-rw-r--r-- | lib/python/qmk/commands.py | 16 | ||||
-rw-r--r-- | lib/python/qmk/tests/test_cli_commands.py | 2 |
9 files changed, 251 insertions, 359 deletions
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index f45e33240..944938824 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py | |||
@@ -35,7 +35,6 @@ subcommands = [ | |||
35 | 'qmk.cli.chibios.confmigrate', | 35 | 'qmk.cli.chibios.confmigrate', |
36 | 'qmk.cli.clean', | 36 | 'qmk.cli.clean', |
37 | 'qmk.cli.compile', | 37 | 'qmk.cli.compile', |
38 | 'qmk.cli.console', | ||
39 | 'qmk.cli.docs', | 38 | 'qmk.cli.docs', |
40 | 'qmk.cli.doctor', | 39 | 'qmk.cli.doctor', |
41 | 'qmk.cli.fileformat', | 40 | 'qmk.cli.fileformat', |
@@ -45,6 +44,7 @@ subcommands = [ | |||
45 | 'qmk.cli.format.python', | 44 | 'qmk.cli.format.python', |
46 | 'qmk.cli.format.text', | 45 | 'qmk.cli.format.text', |
47 | 'qmk.cli.generate.api', | 46 | 'qmk.cli.generate.api', |
47 | 'qmk.cli.generate.compilation_database', | ||
48 | 'qmk.cli.generate.config_h', | 48 | 'qmk.cli.generate.config_h', |
49 | 'qmk.cli.generate.dfu_header', | 49 | 'qmk.cli.generate.dfu_header', |
50 | 'qmk.cli.generate.docs', | 50 | 'qmk.cli.generate.docs', |
diff --git a/lib/python/qmk/cli/console.py b/lib/python/qmk/cli/console.py deleted file mode 100644 index 98c6bc0dc..000000000 --- a/lib/python/qmk/cli/console.py +++ /dev/null | |||
@@ -1,303 +0,0 @@ | |||
1 | """Acquire debugging information from usb hid devices | ||
2 | |||
3 | cli implementation of https://www.pjrc.com/teensy/hid_listen.html | ||
4 | """ | ||
5 | from pathlib import Path | ||
6 | from threading import Thread | ||
7 | from time import sleep, strftime | ||
8 | |||
9 | import hid | ||
10 | import usb.core | ||
11 | |||
12 | from milc import cli | ||
13 | |||
14 | LOG_COLOR = { | ||
15 | 'next': 0, | ||
16 | 'colors': [ | ||
17 | '{fg_blue}', | ||
18 | '{fg_cyan}', | ||
19 | '{fg_green}', | ||
20 | '{fg_magenta}', | ||
21 | '{fg_red}', | ||
22 | '{fg_yellow}', | ||
23 | ], | ||
24 | } | ||
25 | |||
26 | KNOWN_BOOTLOADERS = { | ||
27 | # VID , PID | ||
28 | ('03EB', '2FEF'): 'atmel-dfu: ATmega16U2', | ||
29 | ('03EB', '2FF0'): 'atmel-dfu: ATmega32U2', | ||
30 | ('03EB', '2FF3'): 'atmel-dfu: ATmega16U4', | ||
31 | ('03EB', '2FF4'): 'atmel-dfu: ATmega32U4', | ||
32 | ('03EB', '2FF9'): 'atmel-dfu: AT90USB64', | ||
33 | ('03EB', '2FFA'): 'atmel-dfu: AT90USB162', | ||
34 | ('03EB', '2FFB'): 'atmel-dfu: AT90USB128', | ||
35 | ('03EB', '6124'): 'Microchip SAM-BA', | ||
36 | ('0483', 'DF11'): 'stm32-dfu: STM32 BOOTLOADER', | ||
37 | ('16C0', '05DC'): 'usbasploader: USBaspLoader', | ||
38 | ('16C0', '05DF'): 'bootloadhid: HIDBoot', | ||
39 | ('16C0', '0478'): 'halfkay: Teensy Halfkay', | ||
40 | ('1B4F', '9203'): 'caterina: Pro Micro 3.3V', | ||
41 | ('1B4F', '9205'): 'caterina: Pro Micro 5V', | ||
42 | ('1B4F', '9207'): 'caterina: LilyPadUSB', | ||
43 | ('1C11', 'B007'): 'kiibohd: Kiibohd DFU Bootloader', | ||
44 | ('1EAF', '0003'): 'stm32duino: Maple 003', | ||
45 | ('1FFB', '0101'): 'caterina: Polou A-Star 32U4 Bootloader', | ||
46 | ('2341', '0036'): 'caterina: Arduino Leonardo', | ||
47 | ('2341', '0037'): 'caterina: Arduino Micro', | ||
48 | ('239A', '000C'): 'caterina: Adafruit Feather 32U4', | ||
49 | ('239A', '000D'): 'caterina: Adafruit ItsyBitsy 32U4 3v', | ||
50 | ('239A', '000E'): 'caterina: Adafruit ItsyBitsy 32U4 5v', | ||
51 | ('2A03', '0036'): 'caterina: Arduino Leonardo', | ||
52 | ('2A03', '0037'): 'caterina: Arduino Micro', | ||
53 | ('314B', '0106'): 'apm32-dfu: APM32 DFU ISP Mode', | ||
54 | ('03EB', '2067'): 'qmk-hid: HID Bootloader', | ||
55 | ('03EB', '2045'): 'lufa-ms: LUFA Mass Storage Bootloader' | ||
56 | } | ||
57 | |||
58 | |||
59 | class MonitorDevice(object): | ||
60 | def __init__(self, hid_device, numeric): | ||
61 | self.hid_device = hid_device | ||
62 | self.numeric = numeric | ||
63 | self.device = hid.Device(path=hid_device['path']) | ||
64 | self.current_line = '' | ||
65 | |||
66 | cli.log.info('Console Connected: %(color)s%(manufacturer_string)s %(product_string)s{style_reset_all} (%(color)s%(vendor_id)04X:%(product_id)04X:%(index)d{style_reset_all})', hid_device) | ||
67 | |||
68 | def read(self, size, encoding='ascii', timeout=1): | ||
69 | """Read size bytes from the device. | ||
70 | """ | ||
71 | return self.device.read(size, timeout).decode(encoding) | ||
72 | |||
73 | def read_line(self): | ||
74 | """Read from the device's console until we get a \n. | ||
75 | """ | ||
76 | while '\n' not in self.current_line: | ||
77 | self.current_line += self.read(32).replace('\x00', '') | ||
78 | |||
79 | lines = self.current_line.split('\n', 1) | ||
80 | self.current_line = lines[1] | ||
81 | |||
82 | return lines[0] | ||
83 | |||
84 | def run_forever(self): | ||
85 | while True: | ||
86 | try: | ||
87 | message = {**self.hid_device, 'text': self.read_line()} | ||
88 | identifier = (int2hex(message['vendor_id']), int2hex(message['product_id'])) if self.numeric else (message['manufacturer_string'], message['product_string']) | ||
89 | message['identifier'] = ':'.join(identifier) | ||
90 | message['ts'] = '{style_dim}{fg_green}%s{style_reset_all} ' % (strftime(cli.config.general.datetime_fmt),) if cli.args.timestamp else '' | ||
91 | |||
92 | cli.echo('%(ts)s%(color)s%(identifier)s:%(index)d{style_reset_all}: %(text)s' % message) | ||
93 | |||
94 | except hid.HIDException: | ||
95 | break | ||
96 | |||
97 | |||
98 | class FindDevices(object): | ||
99 | def __init__(self, vid, pid, index, numeric): | ||
100 | self.vid = vid | ||
101 | self.pid = pid | ||
102 | self.index = index | ||
103 | self.numeric = numeric | ||
104 | |||
105 | def run_forever(self): | ||
106 | """Process messages from our queue in a loop. | ||
107 | """ | ||
108 | live_devices = {} | ||
109 | live_bootloaders = {} | ||
110 | |||
111 | while True: | ||
112 | try: | ||
113 | for device in list(live_devices): | ||
114 | if not live_devices[device]['thread'].is_alive(): | ||
115 | cli.log.info('Console Disconnected: %(color)s%(manufacturer_string)s %(product_string)s{style_reset_all} (%(color)s%(vendor_id)04X:%(product_id)04X:%(index)d{style_reset_all})', live_devices[device]) | ||
116 | del live_devices[device] | ||
117 | |||
118 | for device in self.find_devices(): | ||
119 | if device['path'] not in live_devices: | ||
120 | device['color'] = LOG_COLOR['colors'][LOG_COLOR['next']] | ||
121 | LOG_COLOR['next'] = (LOG_COLOR['next'] + 1) % len(LOG_COLOR['colors']) | ||
122 | live_devices[device['path']] = device | ||
123 | |||
124 | try: | ||
125 | monitor = MonitorDevice(device, self.numeric) | ||
126 | device['thread'] = Thread(target=monitor.run_forever, daemon=True) | ||
127 | |||
128 | device['thread'].start() | ||
129 | except Exception as e: | ||
130 | device['e'] = e | ||
131 | device['e_name'] = e.__class__.__name__ | ||
132 | cli.log.error("Could not connect to %(color)s%(manufacturer_string)s %(product_string)s{style_reset_all} (%(color)s:%(vendor_id)04X:%(product_id)04X:%(index)d): %(e_name)s: %(e)s", device) | ||
133 | if cli.config.general.verbose: | ||
134 | cli.log.exception(e) | ||
135 | del live_devices[device['path']] | ||
136 | |||
137 | if cli.args.bootloaders: | ||
138 | for device in self.find_bootloaders(): | ||
139 | if device.address in live_bootloaders: | ||
140 | live_bootloaders[device.address]._qmk_found = True | ||
141 | else: | ||
142 | name = KNOWN_BOOTLOADERS[(int2hex(device.idVendor), int2hex(device.idProduct))] | ||
143 | cli.log.info('Bootloader Connected: {style_bright}{fg_magenta}%s', name) | ||
144 | device._qmk_found = True | ||
145 | live_bootloaders[device.address] = device | ||
146 | |||
147 | for device in list(live_bootloaders): | ||
148 | if live_bootloaders[device]._qmk_found: | ||
149 | live_bootloaders[device]._qmk_found = False | ||
150 | else: | ||
151 | name = KNOWN_BOOTLOADERS[(int2hex(live_bootloaders[device].idVendor), int2hex(live_bootloaders[device].idProduct))] | ||
152 | cli.log.info('Bootloader Disconnected: {style_bright}{fg_magenta}%s', name) | ||
153 | del live_bootloaders[device] | ||
154 | |||
155 | sleep(.1) | ||
156 | |||
157 | except KeyboardInterrupt: | ||
158 | break | ||
159 | |||
160 | def is_bootloader(self, hid_device): | ||
161 | """Returns true if the device in question matches a known bootloader vid/pid. | ||
162 | """ | ||
163 | return (int2hex(hid_device.idVendor), int2hex(hid_device.idProduct)) in KNOWN_BOOTLOADERS | ||
164 | |||
165 | def is_console_hid(self, hid_device): | ||
166 | """Returns true when the usage page indicates it's a teensy-style console. | ||
167 | """ | ||
168 | return hid_device['usage_page'] == 0xFF31 and hid_device['usage'] == 0x0074 | ||
169 | |||
170 | def is_filtered_device(self, hid_device): | ||
171 | """Returns True if the device should be included in the list of available consoles. | ||
172 | """ | ||
173 | return int2hex(hid_device['vendor_id']) == self.vid and int2hex(hid_device['product_id']) == self.pid | ||
174 | |||
175 | def find_devices_by_report(self, hid_devices): | ||
176 | """Returns a list of available teensy-style consoles by doing a brute-force search. | ||
177 | |||
178 | Some versions of linux don't report usage and usage_page. In that case we fallback to reading the report (possibly inaccurately) ourselves. | ||
179 | """ | ||
180 | devices = [] | ||
181 | |||
182 | for device in hid_devices: | ||
183 | path = device['path'].decode('utf-8') | ||
184 | |||
185 | if path.startswith('/dev/hidraw'): | ||
186 | number = path[11:] | ||
187 | report = Path(f'/sys/class/hidraw/hidraw{number}/device/report_descriptor') | ||
188 | |||
189 | if report.exists(): | ||
190 | rp = report.read_bytes() | ||
191 | |||
192 | if rp[1] == 0x31 and rp[3] == 0x09: | ||
193 | devices.append(device) | ||
194 | |||
195 | return devices | ||
196 | |||
197 | def find_bootloaders(self): | ||
198 | """Returns a list of available bootloader devices. | ||
199 | """ | ||
200 | return list(filter(self.is_bootloader, usb.core.find(find_all=True))) | ||
201 | |||
202 | def find_devices(self): | ||
203 | """Returns a list of available teensy-style consoles. | ||
204 | """ | ||
205 | hid_devices = hid.enumerate() | ||
206 | devices = list(filter(self.is_console_hid, hid_devices)) | ||
207 | |||
208 | if not devices: | ||
209 | devices = self.find_devices_by_report(hid_devices) | ||
210 | |||
211 | if self.vid and self.pid: | ||
212 | devices = list(filter(self.is_filtered_device, devices)) | ||
213 | |||
214 | # Add index numbers | ||
215 | device_index = {} | ||
216 | for device in devices: | ||
217 | id = ':'.join((int2hex(device['vendor_id']), int2hex(device['product_id']))) | ||
218 | |||
219 | if id not in device_index: | ||
220 | device_index[id] = 0 | ||
221 | |||
222 | device_index[id] += 1 | ||
223 | device['index'] = device_index[id] | ||
224 | |||
225 | return devices | ||
226 | |||
227 | |||
228 | def int2hex(number): | ||
229 | """Returns a string representation of the number as hex. | ||
230 | """ | ||
231 | return "%04X" % number | ||
232 | |||
233 | |||
234 | def list_devices(device_finder): | ||
235 | """Show the user a nicely formatted list of devices. | ||
236 | """ | ||
237 | devices = device_finder.find_devices() | ||
238 | |||
239 | if devices: | ||
240 | cli.log.info('Available devices:') | ||
241 | for dev in devices: | ||
242 | color = LOG_COLOR['colors'][LOG_COLOR['next']] | ||
243 | LOG_COLOR['next'] = (LOG_COLOR['next'] + 1) % len(LOG_COLOR['colors']) | ||
244 | cli.log.info("\t%s%s:%s:%d{style_reset_all}\t%s %s", color, int2hex(dev['vendor_id']), int2hex(dev['product_id']), dev['index'], dev['manufacturer_string'], dev['product_string']) | ||
245 | |||
246 | if cli.args.bootloaders: | ||
247 | bootloaders = device_finder.find_bootloaders() | ||
248 | |||
249 | if bootloaders: | ||
250 | cli.log.info('Available Bootloaders:') | ||
251 | |||
252 | for dev in bootloaders: | ||
253 | cli.log.info("\t%s:%s\t%s", int2hex(dev.idVendor), int2hex(dev.idProduct), KNOWN_BOOTLOADERS[(int2hex(dev.idVendor), int2hex(dev.idProduct))]) | ||
254 | |||
255 | |||
256 | @cli.argument('--bootloaders', arg_only=True, default=True, action='store_boolean', help='displaying bootloaders.') | ||
257 | @cli.argument('-d', '--device', help='Device to select - uses format <pid>:<vid>[:<index>].') | ||
258 | @cli.argument('-l', '--list', arg_only=True, action='store_true', help='List available hid_listen devices.') | ||
259 | @cli.argument('-n', '--numeric', arg_only=True, action='store_true', help='Show VID/PID instead of names.') | ||
260 | @cli.argument('-t', '--timestamp', arg_only=True, action='store_true', help='Print the timestamp for received messages as well.') | ||
261 | @cli.argument('-w', '--wait', type=int, default=1, help="How many seconds to wait between checks (Default: 1)") | ||
262 | @cli.subcommand('Acquire debugging information from usb hid devices.', hidden=False if cli.config.user.developer else True) | ||
263 | def console(cli): | ||
264 | """Acquire debugging information from usb hid devices | ||
265 | """ | ||
266 | vid = None | ||
267 | pid = None | ||
268 | index = 1 | ||
269 | |||
270 | if cli.config.console.device: | ||
271 | device = cli.config.console.device.split(':') | ||
272 | |||
273 | if len(device) == 2: | ||
274 | vid, pid = device | ||
275 | |||
276 | elif len(device) == 3: | ||
277 | vid, pid, index = device | ||
278 | |||
279 | if not index.isdigit(): | ||
280 | cli.log.error('Device index must be a number! Got "%s" instead.', index) | ||
281 | exit(1) | ||
282 | |||
283 | index = int(index) | ||
284 | |||
285 | if index < 1: | ||
286 | cli.log.error('Device index must be greater than 0! Got %s', index) | ||
287 | exit(1) | ||
288 | |||
289 | else: | ||
290 | cli.log.error('Invalid format for device, expected "<pid>:<vid>[:<index>]" but got "%s".', cli.config.console.device) | ||
291 | cli.print_help() | ||
292 | exit(1) | ||
293 | |||
294 | vid = vid.upper() | ||
295 | pid = pid.upper() | ||
296 | |||
297 | device_finder = FindDevices(vid, pid, index, cli.args.numeric) | ||
298 | |||
299 | if cli.args.list: | ||
300 | return list_devices(device_finder) | ||
301 | |||
302 | print('Looking for devices...', flush=True) | ||
303 | device_finder.run_forever() | ||
diff --git a/lib/python/qmk/cli/doctor/check.py b/lib/python/qmk/cli/doctor/check.py index 0807f4151..2d691b64b 100644 --- a/lib/python/qmk/cli/doctor/check.py +++ b/lib/python/qmk/cli/doctor/check.py | |||
@@ -26,7 +26,6 @@ ESSENTIAL_BINARIES = { | |||
26 | 'arm-none-eabi-gcc': { | 26 | 'arm-none-eabi-gcc': { |
27 | 'version_arg': '-dumpversion' | 27 | 'version_arg': '-dumpversion' |
28 | }, | 28 | }, |
29 | 'bin/qmk': {}, | ||
30 | } | 29 | } |
31 | 30 | ||
32 | 31 | ||
diff --git a/lib/python/qmk/cli/format/python.py b/lib/python/qmk/cli/format/python.py index 00612f97e..b32a72640 100755 --- a/lib/python/qmk/cli/format/python.py +++ b/lib/python/qmk/cli/format/python.py | |||
@@ -11,15 +11,15 @@ def format_python(cli): | |||
11 | """Format python code according to QMK's style. | 11 | """Format python code according to QMK's style. |
12 | """ | 12 | """ |
13 | edit = '--diff' if cli.args.dry_run else '--in-place' | 13 | edit = '--diff' if cli.args.dry_run else '--in-place' |
14 | yapf_cmd = ['yapf', '-vv', '--recursive', edit, 'bin/qmk', 'lib/python'] | 14 | yapf_cmd = ['yapf', '-vv', '--recursive', edit, 'lib/python'] |
15 | try: | 15 | try: |
16 | cli.run(yapf_cmd, check=True, capture_output=False, stdin=DEVNULL) | 16 | cli.run(yapf_cmd, check=True, capture_output=False, stdin=DEVNULL) |
17 | cli.log.info('Python code in `bin/qmk` and `lib/python` is correctly formatted.') | 17 | cli.log.info('Python code in `lib/python` is correctly formatted.') |
18 | return True | 18 | return True |
19 | 19 | ||
20 | except CalledProcessError: | 20 | except CalledProcessError: |
21 | if cli.args.dry_run: | 21 | if cli.args.dry_run: |
22 | cli.log.error('Python code in `bin/qmk` and `lib/python` incorrectly formatted!') | 22 | cli.log.error('Python code in `lib/python` is incorrectly formatted!') |
23 | else: | 23 | else: |
24 | cli.log.error('Error formatting python code!') | 24 | cli.log.error('Error formatting python code!') |
25 | 25 | ||
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..602635270 --- /dev/null +++ b/lib/python/qmk/cli/generate/compilation_database.py | |||
@@ -0,0 +1,133 @@ | |||
1 | """Creates a compilation database for the given keyboard build. | ||
2 | """ | ||
3 | |||
4 | import json | ||
5 | import os | ||
6 | import re | ||
7 | import shlex | ||
8 | import shutil | ||
9 | from functools import lru_cache | ||
10 | from pathlib import Path | ||
11 | from typing import Dict, Iterator, List, Union | ||
12 | |||
13 | from milc import cli, MILC | ||
14 | |||
15 | from qmk.commands import create_make_command | ||
16 | from qmk.constants import QMK_FIRMWARE | ||
17 | from qmk.decorators import automagic_keyboard, automagic_keymap | ||
18 | |||
19 | |||
20 | @lru_cache(maxsize=10) | ||
21 | def system_libs(binary: str) -> List[Path]: | ||
22 | """Find the system include directory that the given build tool uses. | ||
23 | """ | ||
24 | cli.log.debug("searching for system library directory for binary: %s", binary) | ||
25 | bin_path = shutil.which(binary) | ||
26 | |||
27 | # Actually query xxxxxx-gcc to find its include paths. | ||
28 | if binary.endswith("gcc") or binary.endswith("g++"): | ||
29 | result = cli.run([binary, '-E', '-Wp,-v', '-'], capture_output=True, check=True, input='\n') | ||
30 | paths = [] | ||
31 | for line in result.stderr.splitlines(): | ||
32 | if line.startswith(" "): | ||
33 | paths.append(Path(line.strip()).resolve()) | ||
34 | return paths | ||
35 | |||
36 | return list(Path(bin_path).resolve().parent.parent.glob("*/include")) if bin_path else [] | ||
37 | |||
38 | |||
39 | file_re = re.compile(r'printf "Compiling: ([^"]+)') | ||
40 | cmd_re = re.compile(r'LOG=\$\((.+?)&&') | ||
41 | |||
42 | |||
43 | def parse_make_n(f: Iterator[str]) -> List[Dict[str, str]]: | ||
44 | """parse the output of `make -n <target>` | ||
45 | |||
46 | This function makes many assumptions about the format of your build log. | ||
47 | This happens to work right now for qmk. | ||
48 | """ | ||
49 | |||
50 | state = 'start' | ||
51 | this_file = None | ||
52 | records = [] | ||
53 | for line in f: | ||
54 | if state == 'start': | ||
55 | m = file_re.search(line) | ||
56 | if m: | ||
57 | this_file = m.group(1) | ||
58 | state = 'cmd' | ||
59 | |||
60 | if state == 'cmd': | ||
61 | assert this_file | ||
62 | m = cmd_re.search(line) | ||
63 | if m: | ||
64 | # we have a hit! | ||
65 | this_cmd = m.group(1) | ||
66 | args = shlex.split(this_cmd) | ||
67 | for s in system_libs(args[0]): | ||
68 | args += ['-isystem', '%s' % s] | ||
69 | new_cmd = ' '.join(shlex.quote(s) for s in args if s != '-mno-thumb-interwork') | ||
70 | records.append({"directory": str(QMK_FIRMWARE.resolve()), "command": new_cmd, "file": this_file}) | ||
71 | state = 'start' | ||
72 | |||
73 | return records | ||
74 | |||
75 | |||
76 | @cli.argument('-kb', '--keyboard', help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.') | ||
77 | @cli.argument('-km', '--keymap', help='The keymap to build a firmware for. Ignored when a configurator export is supplied.') | ||
78 | @cli.subcommand('Create a compilation database.') | ||
79 | @automagic_keyboard | ||
80 | @automagic_keymap | ||
81 | def generate_compilation_database(cli: MILC) -> Union[bool, int]: | ||
82 | """Creates a compilation database for the given keyboard build. | ||
83 | |||
84 | Does a make clean, then a make -n for this target and uses the dry-run output to create | ||
85 | a compilation database (compile_commands.json). This file can help some IDEs and | ||
86 | IDE-like editors work better. For more information about this: | ||
87 | |||
88 | https://clang.llvm.org/docs/JSONCompilationDatabase.html | ||
89 | """ | ||
90 | command = None | ||
91 | # check both config domains: the magic decorator fills in `generate_compilation_database` but the user is | ||
92 | # more likely to have set `compile` in their config file. | ||
93 | current_keyboard = cli.config.generate_compilation_database.keyboard or cli.config.user.keyboard | ||
94 | current_keymap = cli.config.generate_compilation_database.keymap or cli.config.user.keymap | ||
95 | |||
96 | if current_keyboard and current_keymap: | ||
97 | # Generate the make command for a specific keyboard/keymap. | ||
98 | command = create_make_command(current_keyboard, current_keymap, dry_run=True) | ||
99 | elif not current_keyboard: | ||
100 | cli.log.error('Could not determine keyboard!') | ||
101 | elif not current_keymap: | ||
102 | cli.log.error('Could not determine keymap!') | ||
103 | |||
104 | if not command: | ||
105 | cli.log.error('You must supply both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.') | ||
106 | cli.echo('usage: qmk compiledb [-kb KEYBOARD] [-km KEYMAP]') | ||
107 | return False | ||
108 | |||
109 | # remove any environment variable overrides which could trip us up | ||
110 | env = os.environ.copy() | ||
111 | env.pop("MAKEFLAGS", None) | ||
112 | |||
113 | # re-use same executable as the main make invocation (might be gmake) | ||
114 | clean_command = [command[0], 'clean'] | ||
115 | cli.log.info('Making clean with {fg_cyan}%s', ' '.join(clean_command)) | ||
116 | cli.run(clean_command, capture_output=False, check=True, env=env) | ||
117 | |||
118 | cli.log.info('Gathering build instructions from {fg_cyan}%s', ' '.join(command)) | ||
119 | |||
120 | result = cli.run(command, capture_output=True, check=True, env=env) | ||
121 | db = parse_make_n(result.stdout.splitlines()) | ||
122 | if not db: | ||
123 | cli.log.error("Failed to parse output from make output:\n%s", result.stdout) | ||
124 | return False | ||
125 | |||
126 | cli.log.info("Found %s compile commands", len(db)) | ||
127 | |||
128 | dbpath = QMK_FIRMWARE / 'compile_commands.json' | ||
129 | |||
130 | cli.log.info(f"Writing build database to {dbpath}") | ||
131 | dbpath.write_text(json.dumps(db, indent=4)) | ||
132 | |||
133 | return True | ||
diff --git a/lib/python/qmk/cli/lint.py b/lib/python/qmk/cli/lint.py index 02b31fbc4..96593ed69 100644 --- a/lib/python/qmk/cli/lint.py +++ b/lib/python/qmk/cli/lint.py | |||
@@ -1,72 +1,129 @@ | |||
1 | """Command to look over a keyboard/keymap and check for common mistakes. | 1 | """Command to look over a keyboard/keymap and check for common mistakes. |
2 | """ | 2 | """ |
3 | from pathlib import Path | ||
4 | |||
3 | from milc import cli | 5 | from milc import cli |
4 | 6 | ||
5 | from qmk.decorators import automagic_keyboard, automagic_keymap | 7 | from qmk.decorators import automagic_keyboard, automagic_keymap |
6 | from qmk.info import info_json | 8 | from qmk.info import info_json |
7 | from qmk.keyboard import find_readme, keyboard_completer | 9 | from qmk.keyboard import keyboard_completer, list_keyboards |
8 | from qmk.keymap import locate_keymap | 10 | from qmk.keymap import locate_keymap |
9 | from qmk.path import is_keyboard, keyboard | 11 | from qmk.path import is_keyboard, keyboard |
10 | 12 | ||
11 | 13 | ||
12 | @cli.argument('--strict', action='store_true', help='Treat warnings as errors.') | 14 | def keymap_check(kb, km): |
13 | @cli.argument('-kb', '--keyboard', completer=keyboard_completer, help='The keyboard to check.') | 15 | """Perform the keymap level checks. |
14 | @cli.argument('-km', '--keymap', help='The keymap to check.') | 16 | """ |
17 | ok = True | ||
18 | keymap_path = locate_keymap(kb, km) | ||
19 | |||
20 | if not keymap_path: | ||
21 | ok = False | ||
22 | cli.log.error("%s: Can't find %s keymap.", kb, km) | ||
23 | |||
24 | return ok | ||
25 | |||
26 | |||
27 | def rules_mk_assignment_only(keyboard_path): | ||
28 | """Check the keyboard-level rules.mk to ensure it only has assignments. | ||
29 | """ | ||
30 | current_path = Path() | ||
31 | errors = [] | ||
32 | |||
33 | for path_part in keyboard_path.parts: | ||
34 | current_path = current_path / path_part | ||
35 | rules_mk = current_path / 'rules.mk' | ||
36 | |||
37 | if rules_mk.exists(): | ||
38 | continuation = None | ||
39 | |||
40 | for i, line in enumerate(rules_mk.open()): | ||
41 | line = line.strip() | ||
42 | |||
43 | if '#' in line: | ||
44 | line = line[:line.index('#')] | ||
45 | |||
46 | if continuation: | ||
47 | line = continuation + line | ||
48 | continuation = None | ||
49 | |||
50 | if line: | ||
51 | if line[-1] == '\\': | ||
52 | continuation = line[:-1] | ||
53 | continue | ||
54 | |||
55 | if line and '=' not in line: | ||
56 | errors.append(f'Non-assignment code on line +{i} {rules_mk}: {line}') | ||
57 | |||
58 | return errors | ||
59 | |||
60 | |||
61 | @cli.argument('--strict', action='store_true', help='Treat warnings as errors') | ||
62 | @cli.argument('-kb', '--keyboard', completer=keyboard_completer, help='Comma separated list of keyboards to check') | ||
63 | @cli.argument('-km', '--keymap', help='The keymap to check') | ||
64 | @cli.argument('--all-kb', action='store_true', arg_only=True, help='Check all keyboards') | ||
15 | @cli.subcommand('Check keyboard and keymap for common mistakes.') | 65 | @cli.subcommand('Check keyboard and keymap for common mistakes.') |
16 | @automagic_keyboard | 66 | @automagic_keyboard |
17 | @automagic_keymap | 67 | @automagic_keymap |
18 | def lint(cli): | 68 | def lint(cli): |
19 | """Check keyboard and keymap for common mistakes. | 69 | """Check keyboard and keymap for common mistakes. |
20 | """ | 70 | """ |
21 | if not cli.config.lint.keyboard: | 71 | failed = [] |
22 | cli.log.error('Missing required argument: --keyboard') | ||
23 | cli.print_help() | ||
24 | return False | ||
25 | 72 | ||
26 | if not is_keyboard(cli.config.lint.keyboard): | 73 | # Determine our keyboard list |
27 | cli.log.error('No such keyboard: %s', cli.config.lint.keyboard) | 74 | if cli.args.all_kb: |
28 | return False | 75 | if cli.args.keyboard: |
76 | cli.log.warning('Both --all-kb and --keyboard passed, --all-kb takes presidence.') | ||
29 | 77 | ||
30 | # Gather data about the keyboard. | 78 | keyboard_list = list_keyboards() |
31 | ok = True | 79 | elif not cli.config.lint.keyboard: |
32 | keyboard_path = keyboard(cli.config.lint.keyboard) | 80 | cli.log.error('Missing required arguments: --keyboard or --all-kb') |
33 | keyboard_info = info_json(cli.config.lint.keyboard) | 81 | cli.print_help() |
34 | readme_path = find_readme(cli.config.lint.keyboard) | 82 | return False |
35 | missing_readme_path = keyboard_path / 'readme.md' | 83 | else: |
84 | keyboard_list = cli.config.lint.keyboard.split(',') | ||
36 | 85 | ||
37 | # Check for errors in the info.json | 86 | # Lint each keyboard |
38 | if keyboard_info['parse_errors']: | 87 | for kb in keyboard_list: |
39 | ok = False | 88 | if not is_keyboard(kb): |
40 | cli.log.error('Errors found when generating info.json.') | 89 | cli.log.error('No such keyboard: %s', kb) |
90 | continue | ||
41 | 91 | ||
42 | if cli.config.lint.strict and keyboard_info['parse_warnings']: | 92 | # Gather data about the keyboard. |
43 | ok = False | 93 | ok = True |
44 | cli.log.error('Warnings found when generating info.json (Strict mode enabled.)') | 94 | keyboard_path = keyboard(kb) |
95 | keyboard_info = info_json(kb) | ||
45 | 96 | ||
46 | # Check for a readme.md and warn if it doesn't exist | 97 | # Check for errors in the info.json |
47 | if not readme_path: | 98 | if keyboard_info['parse_errors']: |
48 | ok = False | 99 | ok = False |
49 | cli.log.error('Missing %s', missing_readme_path) | 100 | cli.log.error('%s: Errors found when generating info.json.', kb) |
50 | 101 | ||
51 | # Keymap specific checks | 102 | if cli.config.lint.strict and keyboard_info['parse_warnings']: |
52 | if cli.config.lint.keymap: | 103 | ok = False |
53 | keymap_path = locate_keymap(cli.config.lint.keyboard, cli.config.lint.keymap) | 104 | cli.log.error('%s: Warnings found when generating info.json (Strict mode enabled.)', kb) |
54 | 105 | ||
55 | if not keymap_path: | 106 | # Check the rules.mk file(s) |
107 | rules_mk_assignment_errors = rules_mk_assignment_only(keyboard_path) | ||
108 | if rules_mk_assignment_errors: | ||
56 | ok = False | 109 | ok = False |
57 | cli.log.error("Can't find %s keymap for %s keyboard.", cli.config.lint.keymap, cli.config.lint.keyboard) | 110 | cli.log.error('%s: Non-assignment code found in rules.mk. Move it to post_rules.mk instead.', kb) |
58 | else: | 111 | for assignment_error in rules_mk_assignment_errors: |
59 | keymap_readme = keymap_path.parent / 'readme.md' | 112 | cli.log.error(assignment_error) |
60 | if not keymap_readme.exists(): | ||
61 | cli.log.warning('Missing %s', keymap_readme) | ||
62 | 113 | ||
63 | if cli.config.lint.strict: | 114 | # Keymap specific checks |
64 | ok = False | 115 | if cli.config.lint.keymap: |
116 | if not keymap_check(kb, cli.config.lint.keymap): | ||
117 | ok = False | ||
118 | |||
119 | # Report status | ||
120 | if not ok: | ||
121 | failed.append(kb) | ||
65 | 122 | ||
66 | # Check and report the overall status | 123 | # Check and report the overall status |
67 | if ok: | 124 | if failed: |
68 | cli.log.info('Lint check passed!') | 125 | cli.log.error('Lint check failed for: %s', ', '.join(failed)) |
69 | return True | 126 | return False |
70 | 127 | ||
71 | cli.log.error('Lint check failed!') | 128 | cli.log.info('Lint check passed!') |
72 | return False | 129 | return True |
diff --git a/lib/python/qmk/cli/pytest.py b/lib/python/qmk/cli/pytest.py index bdb336b9a..a7f01a872 100644 --- a/lib/python/qmk/cli/pytest.py +++ b/lib/python/qmk/cli/pytest.py | |||
@@ -12,6 +12,6 @@ def pytest(cli): | |||
12 | """Run several linting/testing commands. | 12 | """Run several linting/testing commands. |
13 | """ | 13 | """ |
14 | nose2 = cli.run(['nose2', '-v'], capture_output=False, stdin=DEVNULL) | 14 | nose2 = cli.run(['nose2', '-v'], capture_output=False, stdin=DEVNULL) |
15 | flake8 = cli.run(['flake8', 'lib/python', 'bin/qmk'], capture_output=False, stdin=DEVNULL) | 15 | flake8 = cli.run(['flake8', 'lib/python'], capture_output=False, stdin=DEVNULL) |
16 | 16 | ||
17 | return flake8.returncode | nose2.returncode | 17 | return flake8.returncode | nose2.returncode |
diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py index 421453d83..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 | ||
31 | def create_make_target(target, parallel=1, **env_vars): | 31 | def 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 | ||
58 | def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars): | 61 | def 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 | ||
90 | def get_git_version(current_time, repo_dir='.', check_dir='.'): | 96 | def get_git_version(current_time, repo_dir='.', check_dir='.'): |
@@ -233,7 +239,7 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va | |||
233 | f'VERBOSE={verbose}', | 239 | f'VERBOSE={verbose}', |
234 | f'COLOR={color}', | 240 | f'COLOR={color}', |
235 | 'SILENT=false', | 241 | 'SILENT=false', |
236 | f'QMK_BIN={"bin/qmk" if "DEPRECATED_BIN_QMK" in os.environ else "qmk"}', | 242 | 'QMK_BIN="qmk"', |
237 | ]) | 243 | ]) |
238 | 244 | ||
239 | return make_command | 245 | return make_command |
diff --git a/lib/python/qmk/tests/test_cli_commands.py b/lib/python/qmk/tests/test_cli_commands.py index b39fe5e46..e4eaef899 100644 --- a/lib/python/qmk/tests/test_cli_commands.py +++ b/lib/python/qmk/tests/test_cli_commands.py | |||
@@ -83,7 +83,7 @@ def test_hello(): | |||
83 | def test_format_python(): | 83 | def test_format_python(): |
84 | result = check_subcommand('format-python', '--dry-run') | 84 | result = check_subcommand('format-python', '--dry-run') |
85 | check_returncode(result) | 85 | check_returncode(result) |
86 | assert 'Python code in `bin/qmk` and `lib/python` is correctly formatted.' in result.stdout | 86 | assert 'Python code in `lib/python` is correctly formatted.' in result.stdout |
87 | 87 | ||
88 | 88 | ||
89 | def test_list_keyboards(): | 89 | def test_list_keyboards(): |