aboutsummaryrefslogtreecommitdiff
path: root/lib/python
diff options
context:
space:
mode:
Diffstat (limited to 'lib/python')
-rw-r--r--lib/python/qmk/cli/__init__.py4
-rwxr-xr-x[-rw-r--r--]lib/python/qmk/cli/cformat.py139
-rwxr-xr-xlib/python/qmk/cli/compile.py2
-rw-r--r--lib/python/qmk/cli/console.py5
-rwxr-xr-xlib/python/qmk/cli/doctor/__init__.py5
-rw-r--r--lib/python/qmk/cli/doctor/check.py (renamed from lib/python/qmk/os_helpers/__init__.py)22
-rw-r--r--lib/python/qmk/cli/doctor/linux.py (renamed from lib/python/qmk/os_helpers/linux/__init__.py)44
-rw-r--r--lib/python/qmk/cli/doctor/macos.py13
-rwxr-xr-xlib/python/qmk/cli/doctor/main.py (renamed from lib/python/qmk/cli/doctor.py)76
-rw-r--r--lib/python/qmk/cli/doctor/windows.py14
-rwxr-xr-x[-rw-r--r--]lib/python/qmk/cli/fileformat.py24
-rw-r--r--lib/python/qmk/cli/flash.py2
-rw-r--r--lib/python/qmk/cli/format/c.py137
-rwxr-xr-xlib/python/qmk/cli/format/json.py5
-rwxr-xr-xlib/python/qmk/cli/format/python.py26
-rw-r--r--lib/python/qmk/cli/format/text.py27
-rwxr-xr-xlib/python/qmk/cli/generate/config_h.py143
-rwxr-xr-xlib/python/qmk/cli/generate/info_json.py40
-rwxr-xr-xlib/python/qmk/cli/generate/keyboard_h.py7
-rwxr-xr-xlib/python/qmk/cli/generate/rules_mk.py41
-rw-r--r--lib/python/qmk/cli/generate/version_h.py28
-rwxr-xr-xlib/python/qmk/cli/info.py55
-rwxr-xr-xlib/python/qmk/cli/kle2json.py2
-rwxr-xr-xlib/python/qmk/cli/multibuild.py6
-rw-r--r--lib/python/qmk/cli/new/keyboard.py141
-rwxr-xr-xlib/python/qmk/cli/pyformat.py32
-rw-r--r--lib/python/qmk/commands.py165
-rw-r--r--lib/python/qmk/constants.py5
-rw-r--r--lib/python/qmk/info.py284
-rwxr-xr-xlib/python/qmk/json_encoders.py3
-rw-r--r--lib/python/qmk/json_schema.py45
-rw-r--r--lib/python/qmk/tests/minimal_info.json2
-rw-r--r--lib/python/qmk/tests/test_cli_commands.py22
33 files changed, 1156 insertions, 410 deletions
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
index de71a5d1e..b22f1c0d2 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -40,7 +40,10 @@ subcommands = [
40 'qmk.cli.doctor', 40 'qmk.cli.doctor',
41 'qmk.cli.fileformat', 41 'qmk.cli.fileformat',
42 'qmk.cli.flash', 42 'qmk.cli.flash',
43 'qmk.cli.format.c',
43 'qmk.cli.format.json', 44 'qmk.cli.format.json',
45 'qmk.cli.format.python',
46 'qmk.cli.format.text',
44 'qmk.cli.generate.api', 47 'qmk.cli.generate.api',
45 'qmk.cli.generate.config_h', 48 'qmk.cli.generate.config_h',
46 'qmk.cli.generate.dfu_header', 49 'qmk.cli.generate.dfu_header',
@@ -50,6 +53,7 @@ subcommands = [
50 'qmk.cli.generate.layouts', 53 'qmk.cli.generate.layouts',
51 'qmk.cli.generate.rgb_breathe_table', 54 'qmk.cli.generate.rgb_breathe_table',
52 'qmk.cli.generate.rules_mk', 55 'qmk.cli.generate.rules_mk',
56 'qmk.cli.generate.version_h',
53 'qmk.cli.hello', 57 'qmk.cli.hello',
54 'qmk.cli.info', 58 'qmk.cli.info',
55 'qmk.cli.json2c', 59 'qmk.cli.json2c',
diff --git a/lib/python/qmk/cli/cformat.py b/lib/python/qmk/cli/cformat.py
index efeb45967..9d0ecaeba 100644..100755
--- a/lib/python/qmk/cli/cformat.py
+++ b/lib/python/qmk/cli/cformat.py
@@ -1,137 +1,28 @@
1"""Format C code according to QMK's style. 1"""Point people to the new command name.
2""" 2"""
3from os import path 3import sys
4from shutil import which 4from pathlib import Path
5from subprocess import CalledProcessError, DEVNULL, Popen, PIPE
6 5
7from argcomplete.completers import FilesCompleter
8from milc import cli 6from milc import cli
9 7
10from qmk.path import normpath
11from qmk.c_parse import c_source_files
12
13c_file_suffixes = ('c', 'h', 'cpp')
14core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms')
15ignored = ('tmk_core/protocol/usb_hid', 'quantum/template', 'platforms/chibios')
16
17
18def find_clang_format():
19 """Returns the path to clang-format.
20 """
21 for clang_version in range(20, 6, -1):
22 binary = f'clang-format-{clang_version}'
23
24 if which(binary):
25 return binary
26
27 return 'clang-format'
28
29
30def find_diffs(files):
31 """Run clang-format and diff it against a file.
32 """
33 found_diffs = False
34
35 for file in files:
36 cli.log.debug('Checking for changes in %s', file)
37 clang_format = Popen([find_clang_format(), file], stdout=PIPE, stderr=PIPE, universal_newlines=True)
38 diff = cli.run(['diff', '-u', f'--label=a/{file}', f'--label=b/{file}', str(file), '-'], stdin=clang_format.stdout, capture_output=True)
39
40 if diff.returncode != 0:
41 print(diff.stdout)
42 found_diffs = True
43
44 return found_diffs
45
46
47def cformat_run(files):
48 """Spawn clang-format subprocess with proper arguments
49 """
50 # Determine which version of clang-format to use
51 clang_format = [find_clang_format(), '-i']
52
53 try:
54 cli.run([*clang_format, *map(str, files)], check=True, capture_output=False, stdin=DEVNULL)
55 cli.log.info('Successfully formatted the C code.')
56 return True
57
58 except CalledProcessError as e:
59 cli.log.error('Error formatting C code!')
60 cli.log.debug('%s exited with returncode %s', e.cmd, e.returncode)
61 cli.log.debug('STDOUT:')
62 cli.log.debug(e.stdout)
63 cli.log.debug('STDERR:')
64 cli.log.debug(e.stderr)
65 return False
66
67
68def filter_files(files, core_only=False):
69 """Yield only files to be formatted and skip the rest
70 """
71 if core_only:
72 # Filter non-core files
73 for index, file in enumerate(files):
74 # The following statement checks each file to see if the file path is
75 # - in the core directories
76 # - not in the ignored directories
77 if not any(i in str(file) for i in core_dirs) or any(i in str(file) for i in ignored):
78 files[index] = None
79 cli.log.debug("Skipping non-core file %s, as '--core-only' is used.", file)
80
81 for file in files:
82 if file and file.name.split('.')[-1] in c_file_suffixes:
83 yield file
84 else:
85 cli.log.debug('Skipping file %s', file)
86
87 8
88@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.") 9@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.")
89@cli.argument('-b', '--base-branch', default='origin/master', help='Branch to compare to diffs to.') 10@cli.argument('-b', '--base-branch', default='origin/master', help='Branch to compare to diffs to.')
90@cli.argument('-a', '--all-files', arg_only=True, action='store_true', help='Format all core files.') 11@cli.argument('-a', '--all-files', arg_only=True, action='store_true', help='Format all core files.')
91@cli.argument('--core-only', arg_only=True, action='store_true', help='Format core files only.') 12@cli.argument('--core-only', arg_only=True, action='store_true', help='Format core files only.')
92@cli.argument('files', nargs='*', arg_only=True, type=normpath, completer=FilesCompleter('.c'), help='Filename(s) to format.') 13@cli.argument('files', nargs='*', arg_only=True, help='Filename(s) to format.')
93@cli.subcommand("Format C code according to QMK's style.", hidden=False if cli.config.user.developer else True) 14@cli.subcommand('Pointer to the new command name: qmk format-c.', hidden=True)
94def cformat(cli): 15def cformat(cli):
95 """Format C code according to QMK's style. 16 """Pointer to the new command name: qmk format-c.
96 """ 17 """
97 # Find the list of files to format 18 cli.log.warning('"qmk cformat" has been renamed to "qmk format-c". Please use the new command in the future.')
98 if cli.args.files: 19 argv = [sys.executable, *sys.argv]
99 files = list(filter_files(cli.args.files, cli.args.core_only)) 20 argv[argv.index('cformat')] = 'format-c'
100 21 script_path = Path(argv[1])
101 if not files: 22 script_path_exe = Path(f'{argv[1]}.exe')
102 cli.log.error('No C files in filelist: %s', ', '.join(map(str, cli.args.files)))
103 exit(0)
104
105 if cli.args.all_files:
106 cli.log.warning('Filenames passed with -a, only formatting: %s', ','.join(map(str, files)))
107
108 elif cli.args.all_files:
109 all_files = c_source_files(core_dirs)
110 files = list(filter_files(all_files, True))
111
112 else:
113 git_diff_cmd = ['git', 'diff', '--name-only', cli.args.base_branch, *core_dirs]
114 git_diff = cli.run(git_diff_cmd, stdin=DEVNULL)
115
116 if git_diff.returncode != 0:
117 cli.log.error("Error running %s", git_diff_cmd)
118 print(git_diff.stderr)
119 return git_diff.returncode
120
121 files = []
122
123 for file in git_diff.stdout.strip().split('\n'):
124 if not any([file.startswith(ignore) for ignore in ignored]):
125 if path.exists(file) and file.split('.')[-1] in c_file_suffixes:
126 files.append(file)
127 23
128 # Sanity check 24 if not script_path.exists() and script_path_exe.exists():
129 if not files: 25 # For reasons I don't understand ".exe" is stripped from the script name on windows.
130 cli.log.error('No changed files detected. Use "qmk cformat -a" to format all core files') 26 argv[1] = str(script_path_exe)
131 return False
132 27
133 # Run clang-format on the files we've found 28 return cli.run(argv, capture_output=False).returncode
134 if cli.args.dry_run:
135 return not find_diffs(files)
136 else:
137 return cformat_run(files)
diff --git a/lib/python/qmk/cli/compile.py b/lib/python/qmk/cli/compile.py
index 7a45e7721..acbd77864 100755
--- a/lib/python/qmk/cli/compile.py
+++ b/lib/python/qmk/cli/compile.py
@@ -18,7 +18,7 @@ from qmk.keymap import keymap_completer
18@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.') 18@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.')
19@cli.argument('-km', '--keymap', completer=keymap_completer, help='The keymap to build a firmware for. Ignored when a configurator export is supplied.') 19@cli.argument('-km', '--keymap', completer=keymap_completer, help='The keymap to build a firmware for. Ignored when a configurator export is supplied.')
20@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.") 20@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.")
21@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs to run.") 21@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.")
22@cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.") 22@cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.")
23@cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.") 23@cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.")
24@cli.subcommand('Compile a QMK Firmware.') 24@cli.subcommand('Compile a QMK Firmware.')
diff --git a/lib/python/qmk/cli/console.py b/lib/python/qmk/cli/console.py
index 45ff0c8be..3c508160e 100644
--- a/lib/python/qmk/cli/console.py
+++ b/lib/python/qmk/cli/console.py
@@ -48,10 +48,11 @@ KNOWN_BOOTLOADERS = {
48 ('239A', '000C'): 'caterina: Adafruit Feather 32U4', 48 ('239A', '000C'): 'caterina: Adafruit Feather 32U4',
49 ('239A', '000D'): 'caterina: Adafruit ItsyBitsy 32U4 3v', 49 ('239A', '000D'): 'caterina: Adafruit ItsyBitsy 32U4 3v',
50 ('239A', '000E'): 'caterina: Adafruit ItsyBitsy 32U4 5v', 50 ('239A', '000E'): 'caterina: Adafruit ItsyBitsy 32U4 5v',
51 ('239A', '000E'): 'caterina: Adafruit ItsyBitsy 32U4 5v',
52 ('2A03', '0036'): 'caterina: Arduino Leonardo', 51 ('2A03', '0036'): 'caterina: Arduino Leonardo',
53 ('2A03', '0037'): 'caterina: Arduino Micro', 52 ('2A03', '0037'): 'caterina: Arduino Micro',
54 ('314B', '0106'): 'apm32-dfu: APM32 DFU ISP Mode' 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'
55} 56}
56 57
57 58
diff --git a/lib/python/qmk/cli/doctor/__init__.py b/lib/python/qmk/cli/doctor/__init__.py
new file mode 100755
index 000000000..272e04202
--- /dev/null
+++ b/lib/python/qmk/cli/doctor/__init__.py
@@ -0,0 +1,5 @@
1"""QMK Doctor
2
3Check out the user's QMK environment and make sure it's ready to compile.
4"""
5from .main import doctor
diff --git a/lib/python/qmk/os_helpers/__init__.py b/lib/python/qmk/cli/doctor/check.py
index 3e98db3c3..0807f4151 100644
--- a/lib/python/qmk/os_helpers/__init__.py
+++ b/lib/python/qmk/cli/doctor/check.py
@@ -1,4 +1,4 @@
1"""OS-agnostic helper functions 1"""Check for specific programs.
2""" 2"""
3from enum import Enum 3from enum import Enum
4import re 4import re
@@ -30,7 +30,7 @@ ESSENTIAL_BINARIES = {
30} 30}
31 31
32 32
33def parse_gcc_version(version): 33def _parse_gcc_version(version):
34 m = re.match(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?", version) 34 m = re.match(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?", version)
35 35
36 return { 36 return {
@@ -40,7 +40,7 @@ def parse_gcc_version(version):
40 } 40 }
41 41
42 42
43def check_arm_gcc_version(): 43def _check_arm_gcc_version():
44 """Returns True if the arm-none-eabi-gcc version is not known to cause problems. 44 """Returns True if the arm-none-eabi-gcc version is not known to cause problems.
45 """ 45 """
46 if 'output' in ESSENTIAL_BINARIES['arm-none-eabi-gcc']: 46 if 'output' in ESSENTIAL_BINARIES['arm-none-eabi-gcc']:
@@ -50,7 +50,7 @@ def check_arm_gcc_version():
50 return CheckStatus.OK # Right now all known arm versions are ok 50 return CheckStatus.OK # Right now all known arm versions are ok
51 51
52 52
53def check_avr_gcc_version(): 53def _check_avr_gcc_version():
54 """Returns True if the avr-gcc version is not known to cause problems. 54 """Returns True if the avr-gcc version is not known to cause problems.
55 """ 55 """
56 rc = CheckStatus.ERROR 56 rc = CheckStatus.ERROR
@@ -60,7 +60,7 @@ def check_avr_gcc_version():
60 cli.log.info('Found avr-gcc version %s', version_number) 60 cli.log.info('Found avr-gcc version %s', version_number)
61 rc = CheckStatus.OK 61 rc = CheckStatus.OK
62 62
63 parsed_version = parse_gcc_version(version_number) 63 parsed_version = _parse_gcc_version(version_number)
64 if parsed_version['major'] > 8: 64 if parsed_version['major'] > 8:
65 cli.log.warning('{fg_yellow}We do not recommend avr-gcc newer than 8. Downgrading to 8.x is recommended.') 65 cli.log.warning('{fg_yellow}We do not recommend avr-gcc newer than 8. Downgrading to 8.x is recommended.')
66 rc = CheckStatus.WARNING 66 rc = CheckStatus.WARNING
@@ -68,7 +68,7 @@ def check_avr_gcc_version():
68 return rc 68 return rc
69 69
70 70
71def check_avrdude_version(): 71def _check_avrdude_version():
72 if 'output' in ESSENTIAL_BINARIES['avrdude']: 72 if 'output' in ESSENTIAL_BINARIES['avrdude']:
73 last_line = ESSENTIAL_BINARIES['avrdude']['output'].split('\n')[-2] 73 last_line = ESSENTIAL_BINARIES['avrdude']['output'].split('\n')[-2]
74 version_number = last_line.split()[2][:-1] 74 version_number = last_line.split()[2][:-1]
@@ -77,7 +77,7 @@ def check_avrdude_version():
77 return CheckStatus.OK 77 return CheckStatus.OK
78 78
79 79
80def check_dfu_util_version(): 80def _check_dfu_util_version():
81 if 'output' in ESSENTIAL_BINARIES['dfu-util']: 81 if 'output' in ESSENTIAL_BINARIES['dfu-util']:
82 first_line = ESSENTIAL_BINARIES['dfu-util']['output'].split('\n')[0] 82 first_line = ESSENTIAL_BINARIES['dfu-util']['output'].split('\n')[0]
83 version_number = first_line.split()[1] 83 version_number = first_line.split()[1]
@@ -86,7 +86,7 @@ def check_dfu_util_version():
86 return CheckStatus.OK 86 return CheckStatus.OK
87 87
88 88
89def check_dfu_programmer_version(): 89def _check_dfu_programmer_version():
90 if 'output' in ESSENTIAL_BINARIES['dfu-programmer']: 90 if 'output' in ESSENTIAL_BINARIES['dfu-programmer']:
91 first_line = ESSENTIAL_BINARIES['dfu-programmer']['output'].split('\n')[0] 91 first_line = ESSENTIAL_BINARIES['dfu-programmer']['output'].split('\n')[0]
92 version_number = first_line.split()[1] 92 version_number = first_line.split()[1]
@@ -111,7 +111,7 @@ def check_binary_versions():
111 """Check the versions of ESSENTIAL_BINARIES 111 """Check the versions of ESSENTIAL_BINARIES
112 """ 112 """
113 versions = [] 113 versions = []
114 for check in (check_arm_gcc_version, check_avr_gcc_version, check_avrdude_version, check_dfu_util_version, check_dfu_programmer_version): 114 for check in (_check_arm_gcc_version, _check_avr_gcc_version, _check_avrdude_version, _check_dfu_util_version, _check_dfu_programmer_version):
115 versions.append(check()) 115 versions.append(check())
116 return versions 116 return versions
117 117
@@ -159,6 +159,6 @@ def check_git_repo():
159 This is a decent enough indicator that the qmk_firmware directory is a 159 This is a decent enough indicator that the qmk_firmware directory is a
160 proper Git repository, rather than a .zip download from GitHub. 160 proper Git repository, rather than a .zip download from GitHub.
161 """ 161 """
162 dot_git_dir = QMK_FIRMWARE / '.git' 162 dot_git = QMK_FIRMWARE / '.git'
163 163
164 return CheckStatus.OK if dot_git_dir.is_dir() else CheckStatus.WARNING 164 return CheckStatus.OK if dot_git.exists() else CheckStatus.WARNING
diff --git a/lib/python/qmk/os_helpers/linux/__init__.py b/lib/python/qmk/cli/doctor/linux.py
index 008654ab0..6ce00f6ef 100644
--- a/lib/python/qmk/os_helpers/linux/__init__.py
+++ b/lib/python/qmk/cli/doctor/linux.py
@@ -1,11 +1,13 @@
1"""OS-specific functions for: Linux 1"""OS-specific functions for: Linux
2""" 2"""
3from pathlib import Path 3import platform
4import shutil 4import shutil
5from pathlib import Path
5 6
6from milc import cli 7from milc import cli
8
7from qmk.constants import QMK_FIRMWARE 9from qmk.constants import QMK_FIRMWARE
8from qmk.os_helpers import CheckStatus 10from .check import CheckStatus
9 11
10 12
11def _udev_rule(vid, pid=None, *args): 13def _udev_rule(vid, pid=None, *args):
@@ -39,7 +41,12 @@ def check_udev_rules():
39 """Make sure the udev rules look good. 41 """Make sure the udev rules look good.
40 """ 42 """
41 rc = CheckStatus.OK 43 rc = CheckStatus.OK
42 udev_dir = Path("/etc/udev/rules.d/") 44 udev_dirs = [
45 Path("/usr/lib/udev/rules.d/"),
46 Path("/usr/local/lib/udev/rules.d/"),
47 Path("/run/udev/rules.d/"),
48 Path("/etc/udev/rules.d/"),
49 ]
43 desired_rules = { 50 desired_rules = {
44 'atmel-dfu': { 51 'atmel-dfu': {
45 _udev_rule("03eb", "2fef"), # ATmega16U2 52 _udev_rule("03eb", "2fef"), # ATmega16U2
@@ -75,6 +82,10 @@ def check_udev_rules():
75 # dog hunter AG 82 # dog hunter AG
76 _udev_rule("2a03", "0036", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Leonardo 83 _udev_rule("2a03", "0036", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Leonardo
77 _udev_rule("2a03", "0037", 'ENV{ID_MM_DEVICE_IGNORE}="1"') # Micro 84 _udev_rule("2a03", "0037", 'ENV{ID_MM_DEVICE_IGNORE}="1"') # Micro
85 },
86 'hid-bootloader': {
87 _udev_rule("03eb", "2067"), # QMK HID
88 _udev_rule("16c0", "0478") # PJRC halfkay
78 } 89 }
79 } 90 }
80 91
@@ -88,8 +99,8 @@ def check_udev_rules():
88 'tmk': {_deprecated_udev_rule("feed")} 99 'tmk': {_deprecated_udev_rule("feed")}
89 } 100 }
90 101
91 if udev_dir.exists(): 102 if any(udev_dir.exists() for udev_dir in udev_dirs):
92 udev_rules = [rule_file for rule_file in udev_dir.glob('*.rules')] 103 udev_rules = [rule_file for udev_dir in udev_dirs for rule_file in udev_dir.glob('*.rules')]
93 current_rules = set() 104 current_rules = set()
94 105
95 # Collect all rules from the config files 106 # Collect all rules from the config files
@@ -115,7 +126,8 @@ def check_udev_rules():
115 cli.log.warning("{fg_yellow}Missing or outdated udev rules for '%s' boards. Run 'sudo cp %s/util/udev/50-qmk.rules /etc/udev/rules.d/'.", bootloader, QMK_FIRMWARE) 126 cli.log.warning("{fg_yellow}Missing or outdated udev rules for '%s' boards. Run 'sudo cp %s/util/udev/50-qmk.rules /etc/udev/rules.d/'.", bootloader, QMK_FIRMWARE)
116 127
117 else: 128 else:
118 cli.log.warning("{fg_yellow}'%s' does not exist. Skipping udev rule checking...", udev_dir) 129 cli.log.warning("{fg_yellow}Can't find udev rules, skipping udev rule checking...")
130 cli.log.debug("Checked directories: %s", ', '.join(str(udev_dir) for udev_dir in udev_dirs))
119 131
120 return rc 132 return rc
121 133
@@ -138,3 +150,23 @@ def check_modem_manager():
138 """(TODO): Add check for non-systemd systems 150 """(TODO): Add check for non-systemd systems
139 """ 151 """
140 return False 152 return False
153
154
155def os_test_linux():
156 """Run the Linux specific tests.
157 """
158 # Don't bother with udev on WSL, for now
159 if 'microsoft' in platform.uname().release.lower():
160 cli.log.info("Detected {fg_cyan}Linux (WSL){fg_reset}.")
161
162 # https://github.com/microsoft/WSL/issues/4197
163 if QMK_FIRMWARE.as_posix().startswith("/mnt"):
164 cli.log.warning("I/O performance on /mnt may be extremely slow.")
165 return CheckStatus.WARNING
166
167 return CheckStatus.OK
168 else:
169 cli.log.info("Detected {fg_cyan}Linux{fg_reset}.")
170 from .linux import check_udev_rules
171
172 return check_udev_rules()
diff --git a/lib/python/qmk/cli/doctor/macos.py b/lib/python/qmk/cli/doctor/macos.py
new file mode 100644
index 000000000..00fb27285
--- /dev/null
+++ b/lib/python/qmk/cli/doctor/macos.py
@@ -0,0 +1,13 @@
1import platform
2
3from milc import cli
4
5from .check import CheckStatus
6
7
8def os_test_macos():
9 """Run the Mac specific tests.
10 """
11 cli.log.info("Detected {fg_cyan}macOS %s{fg_reset}.", platform.mac_ver()[0])
12
13 return CheckStatus.OK
diff --git a/lib/python/qmk/cli/doctor.py b/lib/python/qmk/cli/doctor/main.py
index 327bc9cb3..6a31ccdfd 100755
--- a/lib/python/qmk/cli/doctor.py
+++ b/lib/python/qmk/cli/doctor/main.py
@@ -7,9 +7,11 @@ from subprocess import DEVNULL
7 7
8from milc import cli 8from milc import cli
9from milc.questions import yesno 9from milc.questions import yesno
10
10from qmk import submodules 11from qmk import submodules
11from qmk.constants import QMK_FIRMWARE 12from qmk.constants import QMK_FIRMWARE, QMK_FIRMWARE_UPSTREAM
12from qmk.os_helpers import CheckStatus, check_binaries, check_binary_versions, check_submodules, check_git_repo 13from .check import CheckStatus, check_binaries, check_binary_versions, check_submodules
14from qmk.commands import git_check_repo, git_get_branch, git_is_dirty, git_get_remotes, git_check_deviation, in_virtualenv
13 15
14 16
15def os_tests(): 17def os_tests():
@@ -18,51 +20,48 @@ def os_tests():
18 platform_id = platform.platform().lower() 20 platform_id = platform.platform().lower()
19 21
20 if 'darwin' in platform_id or 'macos' in platform_id: 22 if 'darwin' in platform_id or 'macos' in platform_id:
23 from .macos import os_test_macos
21 return os_test_macos() 24 return os_test_macos()
22 elif 'linux' in platform_id: 25 elif 'linux' in platform_id:
26 from .linux import os_test_linux
23 return os_test_linux() 27 return os_test_linux()
24 elif 'windows' in platform_id: 28 elif 'windows' in platform_id:
29 from .windows import os_test_windows
25 return os_test_windows() 30 return os_test_windows()
26 else: 31 else:
27 cli.log.warning('Unsupported OS detected: %s', platform_id) 32 cli.log.warning('Unsupported OS detected: %s', platform_id)
28 return CheckStatus.WARNING 33 return CheckStatus.WARNING
29 34
30 35
31def os_test_linux(): 36def git_tests():
32 """Run the Linux specific tests. 37 """Run Git-related checks
33 """ 38 """
34 # Don't bother with udev on WSL, for now 39 status = CheckStatus.OK
35 if 'microsoft' in platform.uname().release.lower():
36 cli.log.info("Detected {fg_cyan}Linux (WSL){fg_reset}.")
37
38 # https://github.com/microsoft/WSL/issues/4197
39 if QMK_FIRMWARE.as_posix().startswith("/mnt"):
40 cli.log.warning("I/O performance on /mnt may be extremely slow.")
41 return CheckStatus.WARNING
42 40
43 return CheckStatus.OK 41 # Make sure our QMK home is a Git repo
42 git_ok = git_check_repo()
43 if not git_ok:
44 cli.log.warning("{fg_yellow}QMK home does not appear to be a Git repository! (no .git folder)")
45 status = CheckStatus.WARNING
44 else: 46 else:
45 cli.log.info("Detected {fg_cyan}Linux{fg_reset}.") 47 git_branch = git_get_branch()
46 from qmk.os_helpers.linux import check_udev_rules 48 if git_branch:
47 49 cli.log.info('Git branch: %s', git_branch)
48 return check_udev_rules() 50 git_dirty = git_is_dirty()
49 51 if git_dirty:
50 52 cli.log.warning('{fg_yellow}Git has unstashed/uncommitted changes.')
51def os_test_macos(): 53 status = CheckStatus.WARNING
52 """Run the Mac specific tests. 54 git_remotes = git_get_remotes()
53 """ 55 if 'upstream' not in git_remotes.keys() or QMK_FIRMWARE_UPSTREAM not in git_remotes['upstream'].get('url', ''):
54 cli.log.info("Detected {fg_cyan}macOS %s{fg_reset}.", platform.mac_ver()[0]) 56 cli.log.warning('{fg_yellow}The official repository does not seem to be configured as git remote "upstream".')
55 57 status = CheckStatus.WARNING
56 return CheckStatus.OK 58 else:
57 59 git_deviation = git_check_deviation(git_branch)
58 60 if git_branch in ['master', 'develop'] and git_deviation:
59def os_test_windows(): 61 cli.log.warning('{fg_yellow}The local "%s" branch contains commits not found in the upstream branch.', git_branch)
60 """Run the Windows specific tests. 62 status = CheckStatus.WARNING
61 """ 63
62 win32_ver = platform.win32_ver() 64 return status
63 cli.log.info("Detected {fg_cyan}Windows %s (%s){fg_reset}.", win32_ver[0], win32_ver[1])
64
65 return CheckStatus.OK
66 65
67 66
68@cli.argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.') 67@cli.argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.')
@@ -82,12 +81,11 @@ def doctor(cli):
82 81
83 status = os_tests() 82 status = os_tests()
84 83
85 # Make sure our QMK home is a Git repo 84 status = git_tests()
86 git_ok = check_git_repo()
87 85
88 if git_ok == CheckStatus.WARNING: 86 venv = in_virtualenv()
89 cli.log.warning("QMK home does not appear to be a Git repository! (no .git folder)") 87 if venv:
90 status = CheckStatus.WARNING 88 cli.log.info('CLI installed in virtualenv.')
91 89
92 # Make sure the basic CLI tools we need are available and can be executed. 90 # Make sure the basic CLI tools we need are available and can be executed.
93 bin_ok = check_binaries() 91 bin_ok = check_binaries()
diff --git a/lib/python/qmk/cli/doctor/windows.py b/lib/python/qmk/cli/doctor/windows.py
new file mode 100644
index 000000000..381ab36fd
--- /dev/null
+++ b/lib/python/qmk/cli/doctor/windows.py
@@ -0,0 +1,14 @@
1import platform
2
3from milc import cli
4
5from .check import CheckStatus
6
7
8def os_test_windows():
9 """Run the Windows specific tests.
10 """
11 win32_ver = platform.win32_ver()
12 cli.log.info("Detected {fg_cyan}Windows %s (%s){fg_reset}.", win32_ver[0], win32_ver[1])
13
14 return CheckStatus.OK
diff --git a/lib/python/qmk/cli/fileformat.py b/lib/python/qmk/cli/fileformat.py
index 112d8d59d..cee4ba1ac 100644..100755
--- a/lib/python/qmk/cli/fileformat.py
+++ b/lib/python/qmk/cli/fileformat.py
@@ -1,13 +1,23 @@
1"""Format files according to QMK's style. 1"""Point people to the new command name.
2""" 2"""
3from milc import cli 3import sys
4from pathlib import Path
4 5
5import subprocess 6from milc import cli
6 7
7 8
8@cli.subcommand("Format files according to QMK's style.", hidden=True) 9@cli.subcommand('Pointer to the new command name: qmk format-text.', hidden=True)
9def fileformat(cli): 10def fileformat(cli):
10 """Run several general formatting commands. 11 """Pointer to the new command name: qmk format-text.
11 """ 12 """
12 dos2unix = subprocess.run(['bash', '-c', 'git ls-files -z | xargs -0 dos2unix'], stdout=subprocess.DEVNULL) 13 cli.log.warning('"qmk fileformat" has been renamed to "qmk format-text". Please use the new command in the future.')
13 return dos2unix.returncode 14 argv = [sys.executable, *sys.argv]
15 argv[argv.index('fileformat')] = 'format-text'
16 script_path = Path(argv[1])
17 script_path_exe = Path(f'{argv[1]}.exe')
18
19 if not script_path.exists() and script_path_exe.exists():
20 # For reasons I don't understand ".exe" is stripped from the script name on windows.
21 argv[1] = str(script_path_exe)
22
23 return cli.run(argv, capture_output=False).returncode
diff --git a/lib/python/qmk/cli/flash.py b/lib/python/qmk/cli/flash.py
index 1b2932a5b..c2d9e09c6 100644
--- a/lib/python/qmk/cli/flash.py
+++ b/lib/python/qmk/cli/flash.py
@@ -38,7 +38,7 @@ def print_bootloader_help():
38@cli.argument('-km', '--keymap', help='The keymap to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.') 38@cli.argument('-km', '--keymap', help='The keymap to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.')
39@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='The keyboard to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.') 39@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='The keyboard to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.')
40@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.") 40@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.")
41@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs to run.") 41@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.")
42@cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.") 42@cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.")
43@cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.") 43@cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.")
44@cli.subcommand('QMK Flash.') 44@cli.subcommand('QMK Flash.')
diff --git a/lib/python/qmk/cli/format/c.py b/lib/python/qmk/cli/format/c.py
new file mode 100644
index 000000000..b7263e19f
--- /dev/null
+++ b/lib/python/qmk/cli/format/c.py
@@ -0,0 +1,137 @@
1"""Format C code according to QMK's style.
2"""
3from os import path
4from shutil import which
5from subprocess import CalledProcessError, DEVNULL, Popen, PIPE
6
7from argcomplete.completers import FilesCompleter
8from milc import cli
9
10from qmk.path import normpath
11from qmk.c_parse import c_source_files
12
13c_file_suffixes = ('c', 'h', 'cpp')
14core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms')
15ignored = ('tmk_core/protocol/usb_hid', 'quantum/template', 'platforms/chibios')
16
17
18def find_clang_format():
19 """Returns the path to clang-format.
20 """
21 for clang_version in range(20, 6, -1):
22 binary = f'clang-format-{clang_version}'
23
24 if which(binary):
25 return binary
26
27 return 'clang-format'
28
29
30def find_diffs(files):
31 """Run clang-format and diff it against a file.
32 """
33 found_diffs = False
34
35 for file in files:
36 cli.log.debug('Checking for changes in %s', file)
37 clang_format = Popen([find_clang_format(), file], stdout=PIPE, stderr=PIPE, universal_newlines=True)
38 diff = cli.run(['diff', '-u', f'--label=a/{file}', f'--label=b/{file}', str(file), '-'], stdin=clang_format.stdout, capture_output=True)
39
40 if diff.returncode != 0:
41 print(diff.stdout)
42 found_diffs = True
43
44 return found_diffs
45
46
47def cformat_run(files):
48 """Spawn clang-format subprocess with proper arguments
49 """
50 # Determine which version of clang-format to use
51 clang_format = [find_clang_format(), '-i']
52
53 try:
54 cli.run([*clang_format, *map(str, files)], check=True, capture_output=False, stdin=DEVNULL)
55 cli.log.info('Successfully formatted the C code.')
56 return True
57
58 except CalledProcessError as e:
59 cli.log.error('Error formatting C code!')
60 cli.log.debug('%s exited with returncode %s', e.cmd, e.returncode)
61 cli.log.debug('STDOUT:')
62 cli.log.debug(e.stdout)
63 cli.log.debug('STDERR:')
64 cli.log.debug(e.stderr)
65 return False
66
67
68def filter_files(files, core_only=False):
69 """Yield only files to be formatted and skip the rest
70 """
71 if core_only:
72 # Filter non-core files
73 for index, file in enumerate(files):
74 # The following statement checks each file to see if the file path is
75 # - in the core directories
76 # - not in the ignored directories
77 if not any(i in str(file) for i in core_dirs) or any(i in str(file) for i in ignored):
78 files[index] = None
79 cli.log.debug("Skipping non-core file %s, as '--core-only' is used.", file)
80
81 for file in files:
82 if file and file.name.split('.')[-1] in c_file_suffixes:
83 yield file
84 else:
85 cli.log.debug('Skipping file %s', file)
86
87
88@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.")
89@cli.argument('-b', '--base-branch', default='origin/master', help='Branch to compare to diffs to.')
90@cli.argument('-a', '--all-files', arg_only=True, action='store_true', help='Format all core files.')
91@cli.argument('--core-only', arg_only=True, action='store_true', help='Format core files only.')
92@cli.argument('files', nargs='*', arg_only=True, type=normpath, completer=FilesCompleter('.c'), help='Filename(s) to format.')
93@cli.subcommand("Format C code according to QMK's style.", hidden=False if cli.config.user.developer else True)
94def format_c(cli):
95 """Format C code according to QMK's style.
96 """
97 # Find the list of files to format
98 if cli.args.files:
99 files = list(filter_files(cli.args.files, cli.args.core_only))
100
101 if not files:
102 cli.log.error('No C files in filelist: %s', ', '.join(map(str, cli.args.files)))
103 exit(0)
104
105 if cli.args.all_files:
106 cli.log.warning('Filenames passed with -a, only formatting: %s', ','.join(map(str, files)))
107
108 elif cli.args.all_files:
109 all_files = c_source_files(core_dirs)
110 files = list(filter_files(all_files, True))
111
112 else:
113 git_diff_cmd = ['git', 'diff', '--name-only', cli.args.base_branch, *core_dirs]
114 git_diff = cli.run(git_diff_cmd, stdin=DEVNULL)
115
116 if git_diff.returncode != 0:
117 cli.log.error("Error running %s", git_diff_cmd)
118 print(git_diff.stderr)
119 return git_diff.returncode
120
121 files = []
122
123 for file in git_diff.stdout.strip().split('\n'):
124 if not any([file.startswith(ignore) for ignore in ignored]):
125 if path.exists(file) and file.split('.')[-1] in c_file_suffixes:
126 files.append(file)
127
128 # Sanity check
129 if not files:
130 cli.log.error('No changed files detected. Use "qmk format-c -a" to format all core files')
131 return False
132
133 # Run clang-format on the files we've found
134 if cli.args.dry_run:
135 return not find_diffs(files)
136 else:
137 return cformat_run(files)
diff --git a/lib/python/qmk/cli/format/json.py b/lib/python/qmk/cli/format/json.py
index 1358c70e7..19d504491 100755
--- a/lib/python/qmk/cli/format/json.py
+++ b/lib/python/qmk/cli/format/json.py
@@ -8,7 +8,7 @@ from jsonschema import ValidationError
8from milc import cli 8from milc import cli
9 9
10from qmk.info import info_json 10from qmk.info import info_json
11from qmk.json_schema import json_load, keyboard_validate 11from qmk.json_schema import json_load, validate
12from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder 12from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder
13from qmk.path import normpath 13from qmk.path import normpath
14 14
@@ -23,14 +23,13 @@ def format_json(cli):
23 23
24 if cli.args.format == 'auto': 24 if cli.args.format == 'auto':
25 try: 25 try:
26 keyboard_validate(json_file) 26 validate(json_file, 'qmk.keyboard.v1')
27 json_encoder = InfoJSONEncoder 27 json_encoder = InfoJSONEncoder
28 28
29 except ValidationError as e: 29 except ValidationError as e:
30 cli.log.warning('File %s did not validate as a keyboard:\n\t%s', cli.args.json_file, e) 30 cli.log.warning('File %s did not validate as a keyboard:\n\t%s', cli.args.json_file, e)
31 cli.log.info('Treating %s as a keymap file.', cli.args.json_file) 31 cli.log.info('Treating %s as a keymap file.', cli.args.json_file)
32 json_encoder = KeymapJSONEncoder 32 json_encoder = KeymapJSONEncoder
33
34 elif cli.args.format == 'keyboard': 33 elif cli.args.format == 'keyboard':
35 json_encoder = InfoJSONEncoder 34 json_encoder = InfoJSONEncoder
36 elif cli.args.format == 'keymap': 35 elif cli.args.format == 'keymap':
diff --git a/lib/python/qmk/cli/format/python.py b/lib/python/qmk/cli/format/python.py
new file mode 100755
index 000000000..00612f97e
--- /dev/null
+++ b/lib/python/qmk/cli/format/python.py
@@ -0,0 +1,26 @@
1"""Format python code according to QMK's style.
2"""
3from subprocess import CalledProcessError, DEVNULL
4
5from milc import cli
6
7
8@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually format.")
9@cli.subcommand("Format python code according to QMK's style.", hidden=False if cli.config.user.developer else True)
10def format_python(cli):
11 """Format python code according to QMK's style.
12 """
13 edit = '--diff' if cli.args.dry_run else '--in-place'
14 yapf_cmd = ['yapf', '-vv', '--recursive', edit, 'bin/qmk', 'lib/python']
15 try:
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.')
18 return True
19
20 except CalledProcessError:
21 if cli.args.dry_run:
22 cli.log.error('Python code in `bin/qmk` and `lib/python` incorrectly formatted!')
23 else:
24 cli.log.error('Error formatting python code!')
25
26 return False
diff --git a/lib/python/qmk/cli/format/text.py b/lib/python/qmk/cli/format/text.py
new file mode 100644
index 000000000..e7e07b729
--- /dev/null
+++ b/lib/python/qmk/cli/format/text.py
@@ -0,0 +1,27 @@
1"""Ensure text files have the proper line endings.
2"""
3from subprocess import CalledProcessError
4
5from milc import cli
6
7
8@cli.subcommand("Ensure text files have the proper line endings.", hidden=True)
9def format_text(cli):
10 """Ensure text files have the proper line endings.
11 """
12 try:
13 file_list_cmd = cli.run(['git', 'ls-files', '-z'], check=True)
14 except CalledProcessError as e:
15 cli.log.error('Could not get file list: %s', e)
16 exit(1)
17 except Exception as e:
18 cli.log.error('Unhandled exception: %s: %s', e.__class__.__name__, e)
19 cli.log.exception(e)
20 exit(1)
21
22 dos2unix = cli.run(['xargs', '-0', 'dos2unix'], stdin=None, input=file_list_cmd.stdout)
23
24 if dos2unix.returncode != 0:
25 print(dos2unix.stderr)
26
27 return dos2unix.returncode
diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py
index 54cd5b96a..ca7e14fe6 100755
--- a/lib/python/qmk/cli/generate/config_h.py
+++ b/lib/python/qmk/cli/generate/config_h.py
@@ -5,14 +5,14 @@ from pathlib import Path
5from dotty_dict import dotty 5from dotty_dict import dotty
6from milc import cli 6from milc import cli
7 7
8from qmk.decorators import automagic_keyboard, automagic_keymap
9from qmk.info import info_json 8from qmk.info import info_json
10from qmk.json_schema import json_load 9from qmk.json_schema import json_load, validate
11from qmk.keyboard import keyboard_completer, keyboard_folder 10from qmk.keyboard import keyboard_completer, keyboard_folder
12from qmk.path import is_keyboard, normpath 11from qmk.keymap import locate_keymap
12from qmk.path import normpath
13 13
14 14
15def direct_pins(direct_pins): 15def direct_pins(direct_pins, postfix):
16 """Return the config.h lines that set the direct pins. 16 """Return the config.h lines that set the direct pins.
17 """ 17 """
18 rows = [] 18 rows = []
@@ -24,81 +24,60 @@ def direct_pins(direct_pins):
24 col_count = len(direct_pins[0]) 24 col_count = len(direct_pins[0])
25 row_count = len(direct_pins) 25 row_count = len(direct_pins)
26 26
27 return """ 27 return f"""
28#ifndef MATRIX_COLS 28#ifndef MATRIX_COLS{postfix}
29# define MATRIX_COLS %s 29# define MATRIX_COLS{postfix} {col_count}
30#endif // MATRIX_COLS 30#endif // MATRIX_COLS{postfix}
31 31
32#ifndef MATRIX_ROWS 32#ifndef MATRIX_ROWS{postfix}
33# define MATRIX_ROWS %s 33# define MATRIX_ROWS{postfix} {row_count}
34#endif // MATRIX_ROWS 34#endif // MATRIX_ROWS{postfix}
35 35
36#ifndef DIRECT_PINS 36#ifndef DIRECT_PINS{postfix}
37# define DIRECT_PINS {%s} 37# define DIRECT_PINS{postfix} {{ {", ".join(rows)} }}
38#endif // DIRECT_PINS 38#endif // DIRECT_PINS{postfix}
39""" % (col_count, row_count, ','.join(rows)) 39"""
40 40
41 41
42def pin_array(define, pins): 42def pin_array(define, pins, postfix):
43 """Return the config.h lines that set a pin array. 43 """Return the config.h lines that set a pin array.
44 """ 44 """
45 pin_num = len(pins) 45 pin_num = len(pins)
46 pin_array = ', '.join(map(str, [pin or 'NO_PIN' for pin in pins])) 46 pin_array = ', '.join(map(str, [pin or 'NO_PIN' for pin in pins]))
47 47
48 return f""" 48 return f"""
49#ifndef {define}S 49#ifndef {define}S{postfix}
50# define {define}S {pin_num} 50# define {define}S{postfix} {pin_num}
51#endif // {define}S 51#endif // {define}S{postfix}
52 52
53#ifndef {define}_PINS 53#ifndef {define}_PINS{postfix}
54# define {define}_PINS {{ {pin_array} }} 54# define {define}_PINS{postfix} {{ {pin_array} }}
55#endif // {define}_PINS 55#endif // {define}_PINS{postfix}
56""" 56"""
57 57
58 58
59def matrix_pins(matrix_pins): 59def matrix_pins(matrix_pins, postfix=''):
60 """Add the matrix config to the config.h. 60 """Add the matrix config to the config.h.
61 """ 61 """
62 pins = [] 62 pins = []
63 63
64 if 'direct' in matrix_pins: 64 if 'direct' in matrix_pins:
65 pins.append(direct_pins(matrix_pins['direct'])) 65 pins.append(direct_pins(matrix_pins['direct'], postfix))
66 66
67 if 'cols' in matrix_pins: 67 if 'cols' in matrix_pins:
68 pins.append(pin_array('MATRIX_COL', matrix_pins['cols'])) 68 pins.append(pin_array('MATRIX_COL', matrix_pins['cols'], postfix))
69 69
70 if 'rows' in matrix_pins: 70 if 'rows' in matrix_pins:
71 pins.append(pin_array('MATRIX_ROW', matrix_pins['rows'])) 71 pins.append(pin_array('MATRIX_ROW', matrix_pins['rows'], postfix))
72 72
73 return '\n'.join(pins) 73 return '\n'.join(pins)
74 74
75 75
76@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to') 76def generate_config_items(kb_info_json, config_h_lines):
77@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") 77 """Iterate through the info_config map to generate basic config values.
78@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate config.h for.')
79@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
80@automagic_keyboard
81@automagic_keymap
82def generate_config_h(cli):
83 """Generates the info_config.h file.
84 """ 78 """
85 # Determine our keyboard(s)
86 if not cli.config.generate_config_h.keyboard:
87 cli.log.error('Missing parameter: --keyboard')
88 cli.subcommands['info'].print_help()
89 return False
90
91 if not is_keyboard(cli.config.generate_config_h.keyboard):
92 cli.log.error('Invalid keyboard: "%s"', cli.config.generate_config_h.keyboard)
93 return False
94
95 # Build the info_config.h file.
96 kb_info_json = dotty(info_json(cli.config.generate_config_h.keyboard))
97 info_config_map = json_load(Path('data/mappings/info_config.json')) 79 info_config_map = json_load(Path('data/mappings/info_config.json'))
98 80
99 config_h_lines = ['/* This file was generated by `qmk generate-config-h`. Do not edit or copy.' ' */', '', '#pragma once']
100
101 # Iterate through the info_config map to generate basic things
102 for config_key, info_dict in info_config_map.items(): 81 for config_key, info_dict in info_config_map.items():
103 info_key = info_dict['info_key'] 82 info_key = info_dict['info_key']
104 key_type = info_dict.get('value_type', 'str') 83 key_type = info_dict.get('value_type', 'str')
@@ -135,9 +114,75 @@ def generate_config_h(cli):
135 config_h_lines.append(f'# define {config_key} {config_value}') 114 config_h_lines.append(f'# define {config_key} {config_value}')
136 config_h_lines.append(f'#endif // {config_key}') 115 config_h_lines.append(f'#endif // {config_key}')
137 116
117
118def generate_split_config(kb_info_json, config_h_lines):
119 """Generate the config.h lines for split boards."""
120 if 'primary' in kb_info_json['split']:
121 if kb_info_json['split']['primary'] in ('left', 'right'):
122 config_h_lines.append('')
123 config_h_lines.append('#ifndef MASTER_LEFT')
124 config_h_lines.append('# ifndef MASTER_RIGHT')
125 if kb_info_json['split']['primary'] == 'left':
126 config_h_lines.append('# define MASTER_LEFT')
127 elif kb_info_json['split']['primary'] == 'right':
128 config_h_lines.append('# define MASTER_RIGHT')
129 config_h_lines.append('# endif // MASTER_RIGHT')
130 config_h_lines.append('#endif // MASTER_LEFT')
131 elif kb_info_json['split']['primary'] == 'pin':
132 config_h_lines.append('')
133 config_h_lines.append('#ifndef SPLIT_HAND_PIN')
134 config_h_lines.append('# define SPLIT_HAND_PIN')
135 config_h_lines.append('#endif // SPLIT_HAND_PIN')
136 elif kb_info_json['split']['primary'] == 'matrix_grid':
137 config_h_lines.append('')
138 config_h_lines.append('#ifndef SPLIT_HAND_MATRIX_GRID')
139 config_h_lines.append('# define SPLIT_HAND_MATRIX_GRID {%s}' % (','.join(kb_info_json["split"]["matrix_grid"],)))
140 config_h_lines.append('#endif // SPLIT_HAND_MATRIX_GRID')
141 elif kb_info_json['split']['primary'] == 'eeprom':
142 config_h_lines.append('')
143 config_h_lines.append('#ifndef EE_HANDS')
144 config_h_lines.append('# define EE_HANDS')
145 config_h_lines.append('#endif // EE_HANDS')
146
147 if 'protocol' in kb_info_json['split'].get('transport', {}):
148 if kb_info_json['split']['transport']['protocol'] == 'i2c':
149 config_h_lines.append('')
150 config_h_lines.append('#ifndef USE_I2C')
151 config_h_lines.append('# define USE_I2C')
152 config_h_lines.append('#endif // USE_I2C')
153
154 if 'right' in kb_info_json['split'].get('matrix_pins', {}):
155 config_h_lines.append(matrix_pins(kb_info_json['split']['matrix_pins']['right'], '_RIGHT'))
156
157
158@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
159@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
160@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate config.h for.')
161@cli.argument('-km', '--keymap', arg_only=True, help='Keymap to generate config.h for.')
162@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
163def generate_config_h(cli):
164 """Generates the info_config.h file.
165 """
166 # Determine our keyboard/keymap
167 if cli.args.keymap:
168 km = locate_keymap(cli.args.keyboard, cli.args.keymap)
169 km_json = json_load(km)
170 validate(km_json, 'qmk.keymap.v1')
171 kb_info_json = dotty(km_json.get('config', {}))
172 else:
173 kb_info_json = dotty(info_json(cli.args.keyboard))
174
175 # Build the info_config.h file.
176 config_h_lines = ['/* This file was generated by `qmk generate-config-h`. Do not edit or copy.' ' */', '', '#pragma once']
177
178 generate_config_items(kb_info_json, config_h_lines)
179
138 if 'matrix_pins' in kb_info_json: 180 if 'matrix_pins' in kb_info_json:
139 config_h_lines.append(matrix_pins(kb_info_json['matrix_pins'])) 181 config_h_lines.append(matrix_pins(kb_info_json['matrix_pins']))
140 182
183 if 'split' in kb_info_json:
184 generate_split_config(kb_info_json, config_h_lines)
185
141 # Show the results 186 # Show the results
142 config_h = '\n'.join(config_h_lines) 187 config_h = '\n'.join(config_h_lines)
143 188
diff --git a/lib/python/qmk/cli/generate/info_json.py b/lib/python/qmk/cli/generate/info_json.py
index 8931b68b6..284d1a851 100755
--- a/lib/python/qmk/cli/generate/info_json.py
+++ b/lib/python/qmk/cli/generate/info_json.py
@@ -4,15 +4,17 @@ Compile an info.json for a particular keyboard and pretty-print it.
4""" 4"""
5import json 5import json
6 6
7from jsonschema import Draft7Validator, validators 7from argcomplete.completers import FilesCompleter
8from jsonschema import Draft7Validator, RefResolver, validators
8from milc import cli 9from milc import cli
10from pathlib import Path
9 11
10from qmk.decorators import automagic_keyboard, automagic_keymap 12from qmk.decorators import automagic_keyboard, automagic_keymap
11from qmk.info import info_json 13from qmk.info import info_json
12from qmk.json_encoders import InfoJSONEncoder 14from qmk.json_encoders import InfoJSONEncoder
13from qmk.json_schema import load_jsonschema 15from qmk.json_schema import compile_schema_store
14from qmk.keyboard import keyboard_completer, keyboard_folder 16from qmk.keyboard import keyboard_completer, keyboard_folder
15from qmk.path import is_keyboard 17from qmk.path import is_keyboard, normpath
16 18
17 19
18def pruning_validator(validator_class): 20def pruning_validator(validator_class):
@@ -34,15 +36,19 @@ def pruning_validator(validator_class):
34def strip_info_json(kb_info_json): 36def strip_info_json(kb_info_json):
35 """Remove the API-only properties from the info.json. 37 """Remove the API-only properties from the info.json.
36 """ 38 """
39 schema_store = compile_schema_store()
37 pruning_draft_7_validator = pruning_validator(Draft7Validator) 40 pruning_draft_7_validator = pruning_validator(Draft7Validator)
38 schema = load_jsonschema('keyboard') 41 schema = schema_store['qmk.keyboard.v1']
39 validator = pruning_draft_7_validator(schema).validate 42 resolver = RefResolver.from_schema(schema_store['qmk.keyboard.v1'], store=schema_store)
43 validator = pruning_draft_7_validator(schema, resolver=resolver).validate
40 44
41 return validator(kb_info_json) 45 return validator(kb_info_json)
42 46
43 47
44@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to show info for.') 48@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to show info for.')
45@cli.argument('-km', '--keymap', help='Show the layers for a JSON keymap too.') 49@cli.argument('-km', '--keymap', help='Show the layers for a JSON keymap too.')
50@cli.argument('-o', '--output', arg_only=True, completer=FilesCompleter, help='Write the output the specified file, overwriting if necessary.')
51@cli.argument('-ow', '--overwrite', arg_only=True, action='store_true', help='Overwrite the existing info.json. (Overrides the location of --output)')
46@cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True) 52@cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True)
47@automagic_keyboard 53@automagic_keyboard
48@automagic_keymap 54@automagic_keymap
@@ -59,9 +65,29 @@ def generate_info_json(cli):
59 cli.log.error('Invalid keyboard: "%s"', cli.config.generate_info_json.keyboard) 65 cli.log.error('Invalid keyboard: "%s"', cli.config.generate_info_json.keyboard)
60 return False 66 return False
61 67
68 if cli.args.overwrite:
69 output_path = (Path('keyboards') / cli.config.generate_info_json.keyboard / 'info.json').resolve()
70
71 if cli.args.output:
72 cli.log.warning('Overwriting user supplied --output with %s', output_path)
73
74 cli.args.output = output_path
75
62 # Build the info.json file 76 # Build the info.json file
63 kb_info_json = info_json(cli.config.generate_info_json.keyboard) 77 kb_info_json = info_json(cli.config.generate_info_json.keyboard)
64 strip_info_json(kb_info_json) 78 strip_info_json(kb_info_json)
79 info_json_text = json.dumps(kb_info_json, indent=4, cls=InfoJSONEncoder)
80
81 if cli.args.output:
82 # Write to a file
83 output_path = normpath(cli.args.output)
84
85 if output_path.exists():
86 cli.log.warning('Overwriting output file %s', output_path)
87
88 output_path.write_text(info_json_text + '\n')
89 cli.log.info('Wrote info.json to %s.', output_path)
65 90
66 # Display the results 91 else:
67 print(json.dumps(kb_info_json, indent=2, cls=InfoJSONEncoder)) 92 # Display the results
93 print(info_json_text)
diff --git a/lib/python/qmk/cli/generate/keyboard_h.py b/lib/python/qmk/cli/generate/keyboard_h.py
index 22500dbc9..c9d7f549b 100755
--- a/lib/python/qmk/cli/generate/keyboard_h.py
+++ b/lib/python/qmk/cli/generate/keyboard_h.py
@@ -2,7 +2,6 @@
2""" 2"""
3from milc import cli 3from milc import cli
4 4
5from qmk.decorators import automagic_keyboard, automagic_keymap
6from qmk.info import info_json 5from qmk.info import info_json
7from qmk.keyboard import keyboard_completer, keyboard_folder 6from qmk.keyboard import keyboard_completer, keyboard_folder
8from qmk.path import normpath 7from qmk.path import normpath
@@ -29,14 +28,12 @@ def would_populate_layout_h(keyboard):
29 28
30@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to') 29@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
31@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") 30@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
32@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate keyboard.h for.') 31@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate keyboard.h for.')
33@cli.subcommand('Used by the make system to generate keyboard.h from info.json', hidden=True) 32@cli.subcommand('Used by the make system to generate keyboard.h from info.json', hidden=True)
34@automagic_keyboard
35@automagic_keymap
36def generate_keyboard_h(cli): 33def generate_keyboard_h(cli):
37 """Generates the keyboard.h file. 34 """Generates the keyboard.h file.
38 """ 35 """
39 has_layout_h = would_populate_layout_h(cli.config.generate_keyboard_h.keyboard) 36 has_layout_h = would_populate_layout_h(cli.args.keyboard)
40 37
41 # Build the layouts.h file. 38 # Build the layouts.h file.
42 keyboard_h_lines = ['/* This file was generated by `qmk generate-keyboard-h`. Do not edit or copy.' ' */', '', '#pragma once', '#include "quantum.h"'] 39 keyboard_h_lines = ['/* This file was generated by `qmk generate-keyboard-h`. Do not edit or copy.' ' */', '', '#pragma once', '#include "quantum.h"']
diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py
index 41c94e16b..cdf17dfbc 100755
--- a/lib/python/qmk/cli/generate/rules_mk.py
+++ b/lib/python/qmk/cli/generate/rules_mk.py
@@ -5,11 +5,11 @@ from pathlib import Path
5from dotty_dict import dotty 5from dotty_dict import dotty
6from milc import cli 6from milc import cli
7 7
8from qmk.decorators import automagic_keyboard, automagic_keymap
9from qmk.info import info_json 8from qmk.info import info_json
10from qmk.json_schema import json_load 9from qmk.json_schema import json_load, validate
11from qmk.keyboard import keyboard_completer, keyboard_folder 10from qmk.keyboard import keyboard_completer, keyboard_folder
12from qmk.path import is_keyboard, normpath 11from qmk.keymap import locate_keymap
12from qmk.path import normpath
13 13
14 14
15def process_mapping_rule(kb_info_json, rules_key, info_dict): 15def process_mapping_rule(kb_info_json, rules_key, info_dict):
@@ -39,23 +39,21 @@ def process_mapping_rule(kb_info_json, rules_key, info_dict):
39@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to') 39@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
40@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") 40@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
41@cli.argument('-e', '--escape', arg_only=True, action='store_true', help="Escape spaces in quiet mode") 41@cli.argument('-e', '--escape', arg_only=True, action='store_true', help="Escape spaces in quiet mode")
42@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate config.h for.') 42@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate rules.mk for.')
43@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True) 43@cli.argument('-km', '--keymap', arg_only=True, help='Keymap to generate rules.mk for.')
44@automagic_keyboard 44@cli.subcommand('Used by the make system to generate rules.mk from info.json', hidden=True)
45@automagic_keymap
46def generate_rules_mk(cli): 45def generate_rules_mk(cli):
47 """Generates a rules.mk file from info.json. 46 """Generates a rules.mk file from info.json.
48 """ 47 """
49 if not cli.config.generate_rules_mk.keyboard: 48 # Determine our keyboard/keymap
50 cli.log.error('Missing parameter: --keyboard') 49 if cli.args.keymap:
51 cli.subcommands['info'].print_help() 50 km = locate_keymap(cli.args.keyboard, cli.args.keymap)
52 return False 51 km_json = json_load(km)
53 52 validate(km_json, 'qmk.keymap.v1')
54 if not is_keyboard(cli.config.generate_rules_mk.keyboard): 53 kb_info_json = dotty(km_json.get('config', {}))
55 cli.log.error('Invalid keyboard: "%s"', cli.config.generate_rules_mk.keyboard) 54 else:
56 return False 55 kb_info_json = dotty(info_json(cli.args.keyboard))
57 56
58 kb_info_json = dotty(info_json(cli.config.generate_rules_mk.keyboard))
59 info_rules_map = json_load(Path('data/mappings/info_rules.json')) 57 info_rules_map = json_load(Path('data/mappings/info_rules.json'))
60 rules_mk_lines = ['# This file was generated by `qmk generate-rules-mk`. Do not edit or copy.', ''] 58 rules_mk_lines = ['# This file was generated by `qmk generate-rules-mk`. Do not edit or copy.', '']
61 59
@@ -76,6 +74,17 @@ def generate_rules_mk(cli):
76 enabled = 'yes' if enabled else 'no' 74 enabled = 'yes' if enabled else 'no'
77 rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}') 75 rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}')
78 76
77 # Set SPLIT_TRANSPORT, if needed
78 if kb_info_json.get('split', {}).get('transport', {}).get('protocol') == 'custom':
79 rules_mk_lines.append('SPLIT_TRANSPORT ?= custom')
80
81 # Set CUSTOM_MATRIX, if needed
82 if kb_info_json.get('matrix_pins', {}).get('custom'):
83 if kb_info_json.get('matrix_pins', {}).get('custom_lite'):
84 rules_mk_lines.append('CUSTOM_MATRIX ?= lite')
85 else:
86 rules_mk_lines.append('CUSTOM_MATRIX ?= yes')
87
79 # Show the results 88 # Show the results
80 rules_mk = '\n'.join(rules_mk_lines) + '\n' 89 rules_mk = '\n'.join(rules_mk_lines) + '\n'
81 90
diff --git a/lib/python/qmk/cli/generate/version_h.py b/lib/python/qmk/cli/generate/version_h.py
new file mode 100644
index 000000000..b8e52588c
--- /dev/null
+++ b/lib/python/qmk/cli/generate/version_h.py
@@ -0,0 +1,28 @@
1"""Used by the make system to generate version.h for use in code.
2"""
3from milc import cli
4
5from qmk.commands import create_version_h
6from qmk.path import normpath
7
8
9@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
10@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
11@cli.argument('--skip-git', arg_only=True, action='store_true', help='Skip Git operations')
12@cli.argument('--skip-all', arg_only=True, action='store_true', help='Use placeholder values for all defines (implies --skip-git)')
13@cli.subcommand('Used by the make system to generate version.h for use in code', hidden=True)
14def generate_version_h(cli):
15 """Generates the version.h file.
16 """
17 if cli.args.skip_all:
18 cli.args.skip_git = True
19
20 version_h = create_version_h(cli.args.skip_git, cli.args.skip_all)
21
22 if cli.args.output:
23 cli.args.output.write_text(version_h)
24
25 if not cli.args.quiet:
26 cli.log.info('Wrote version.h to %s.', cli.args.output)
27 else:
28 print(version_h)
diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py
index 0d08d242c..3131d4b53 100755
--- a/lib/python/qmk/cli/info.py
+++ b/lib/python/qmk/cli/info.py
@@ -24,19 +24,15 @@ def show_keymap(kb_info_json, title_caps=True):
24 keymap_path = locate_keymap(cli.config.info.keyboard, cli.config.info.keymap) 24 keymap_path = locate_keymap(cli.config.info.keyboard, cli.config.info.keymap)
25 25
26 if keymap_path and keymap_path.suffix == '.json': 26 if keymap_path and keymap_path.suffix == '.json':
27 if title_caps:
28 cli.echo('{fg_blue}Keymap "%s"{fg_reset}:', cli.config.info.keymap)
29 else:
30 cli.echo('{fg_blue}keymap_%s{fg_reset}:', cli.config.info.keymap)
31
32 keymap_data = json.load(keymap_path.open(encoding='utf-8')) 27 keymap_data = json.load(keymap_path.open(encoding='utf-8'))
33 layout_name = keymap_data['layout'] 28 layout_name = keymap_data['layout']
29 layout_name = kb_info_json.get('layout_aliases', {}).get(layout_name, layout_name) # Resolve alias names
34 30
35 for layer_num, layer in enumerate(keymap_data['layers']): 31 for layer_num, layer in enumerate(keymap_data['layers']):
36 if title_caps: 32 if title_caps:
37 cli.echo('{fg_cyan}Layer %s{fg_reset}:', layer_num) 33 cli.echo('{fg_cyan}Keymap %s Layer %s{fg_reset}:', cli.config.info.keymap, layer_num)
38 else: 34 else:
39 cli.echo('{fg_cyan}layer_%s{fg_reset}:', layer_num) 35 cli.echo('{fg_cyan}keymap.%s.layer.%s{fg_reset}:', cli.config.info.keymap, layer_num)
40 36
41 print(render_layout(kb_info_json['layouts'][layout_name]['layout'], cli.config.info.ascii, layer)) 37 print(render_layout(kb_info_json['layouts'][layout_name]['layout'], cli.config.info.ascii, layer))
42 38
@@ -45,7 +41,7 @@ def show_layouts(kb_info_json, title_caps=True):
45 """Render the layouts with info.json labels. 41 """Render the layouts with info.json labels.
46 """ 42 """
47 for layout_name, layout_art in render_layouts(kb_info_json, cli.config.info.ascii).items(): 43 for layout_name, layout_art in render_layouts(kb_info_json, cli.config.info.ascii).items():
48 title = layout_name.title() if title_caps else layout_name 44 title = f'Layout {layout_name.title()}' if title_caps else f'layouts.{layout_name}'
49 cli.echo('{fg_cyan}%s{fg_reset}:', title) 45 cli.echo('{fg_cyan}%s{fg_reset}:', title)
50 print(layout_art) # Avoid passing dirty data to cli.echo() 46 print(layout_art) # Avoid passing dirty data to cli.echo()
51 47
@@ -87,23 +83,12 @@ def print_friendly_output(kb_info_json):
87 cli.echo('{fg_blue}Maintainer{fg_reset}: %s', kb_info_json['maintainer']) 83 cli.echo('{fg_blue}Maintainer{fg_reset}: %s', kb_info_json['maintainer'])
88 cli.echo('{fg_blue}Keyboard Folder{fg_reset}: %s', kb_info_json.get('keyboard_folder', 'Unknown')) 84 cli.echo('{fg_blue}Keyboard Folder{fg_reset}: %s', kb_info_json.get('keyboard_folder', 'Unknown'))
89 cli.echo('{fg_blue}Layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys()))) 85 cli.echo('{fg_blue}Layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys())))
90 if 'width' in kb_info_json and 'height' in kb_info_json:
91 cli.echo('{fg_blue}Size{fg_reset}: %s x %s' % (kb_info_json['width'], kb_info_json['height']))
92 cli.echo('{fg_blue}Processor{fg_reset}: %s', kb_info_json.get('processor', 'Unknown')) 86 cli.echo('{fg_blue}Processor{fg_reset}: %s', kb_info_json.get('processor', 'Unknown'))
93 cli.echo('{fg_blue}Bootloader{fg_reset}: %s', kb_info_json.get('bootloader', 'Unknown')) 87 cli.echo('{fg_blue}Bootloader{fg_reset}: %s', kb_info_json.get('bootloader', 'Unknown'))
94 if 'layout_aliases' in kb_info_json: 88 if 'layout_aliases' in kb_info_json:
95 aliases = [f'{key}={value}' for key, value in kb_info_json['layout_aliases'].items()] 89 aliases = [f'{key}={value}' for key, value in kb_info_json['layout_aliases'].items()]
96 cli.echo('{fg_blue}Layout aliases:{fg_reset} %s' % (', '.join(aliases),)) 90 cli.echo('{fg_blue}Layout aliases:{fg_reset} %s' % (', '.join(aliases),))
97 91
98 if cli.config.info.layouts:
99 show_layouts(kb_info_json, True)
100
101 if cli.config.info.matrix:
102 show_matrix(kb_info_json, True)
103
104 if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file':
105 show_keymap(kb_info_json, True)
106
107 92
108def print_text_output(kb_info_json): 93def print_text_output(kb_info_json):
109 """Print the info.json in a plain text format. 94 """Print the info.json in a plain text format.
@@ -124,6 +109,24 @@ def print_text_output(kb_info_json):
124 show_keymap(kb_info_json, False) 109 show_keymap(kb_info_json, False)
125 110
126 111
112def print_dotted_output(kb_info_json, prefix=''):
113 """Print the info.json in a plain text format with dot-joined keys.
114 """
115 for key in sorted(kb_info_json):
116 new_prefix = f'{prefix}.{key}' if prefix else key
117
118 if key in ['parse_errors', 'parse_warnings']:
119 continue
120 elif key == 'layouts' and prefix == '':
121 cli.echo('{fg_blue}layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys())))
122 elif isinstance(kb_info_json[key], dict):
123 print_dotted_output(kb_info_json[key], new_prefix)
124 elif isinstance(kb_info_json[key], list):
125 cli.echo('{fg_blue}%s{fg_reset}: %s', new_prefix, ', '.join(map(str, sorted(kb_info_json[key]))))
126 else:
127 cli.echo('{fg_blue}%s{fg_reset}: %s', new_prefix, kb_info_json[key])
128
129
127def print_parsed_rules_mk(keyboard_name): 130def print_parsed_rules_mk(keyboard_name):
128 rules = rules_mk(keyboard_name) 131 rules = rules_mk(keyboard_name)
129 for k in sorted(rules.keys()): 132 for k in sorted(rules.keys()):
@@ -164,10 +167,22 @@ def info(cli):
164 # Output in the requested format 167 # Output in the requested format
165 if cli.args.format == 'json': 168 if cli.args.format == 'json':
166 print(json.dumps(kb_info_json, cls=InfoJSONEncoder)) 169 print(json.dumps(kb_info_json, cls=InfoJSONEncoder))
170 return True
167 elif cli.args.format == 'text': 171 elif cli.args.format == 'text':
168 print_text_output(kb_info_json) 172 print_dotted_output(kb_info_json)
173 title_caps = False
169 elif cli.args.format == 'friendly': 174 elif cli.args.format == 'friendly':
170 print_friendly_output(kb_info_json) 175 print_friendly_output(kb_info_json)
176 title_caps = True
171 else: 177 else:
172 cli.log.error('Unknown format: %s', cli.args.format) 178 cli.log.error('Unknown format: %s', cli.args.format)
173 return False 179 return False
180
181 if cli.config.info.layouts:
182 show_layouts(kb_info_json, title_caps)
183
184 if cli.config.info.matrix:
185 show_matrix(kb_info_json, title_caps)
186
187 if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file':
188 show_keymap(kb_info_json, title_caps)
diff --git a/lib/python/qmk/cli/kle2json.py b/lib/python/qmk/cli/kle2json.py
index acb75ef4f..bbfddf426 100755
--- a/lib/python/qmk/cli/kle2json.py
+++ b/lib/python/qmk/cli/kle2json.py
@@ -44,8 +44,6 @@ def kle2json(cli):
44 'keyboard_name': kle.name, 44 'keyboard_name': kle.name,
45 'url': '', 45 'url': '',
46 'maintainer': 'qmk', 46 'maintainer': 'qmk',
47 'width': kle.columns,
48 'height': kle.rows,
49 'layouts': { 47 'layouts': {
50 'LAYOUT': { 48 'LAYOUT': {
51 'layout': kle2qmk(kle) 49 'layout': kle2qmk(kle)
diff --git a/lib/python/qmk/cli/multibuild.py b/lib/python/qmk/cli/multibuild.py
index bdb0b493c..85ed0fa1e 100755
--- a/lib/python/qmk/cli/multibuild.py
+++ b/lib/python/qmk/cli/multibuild.py
@@ -10,7 +10,7 @@ from subprocess import DEVNULL
10from milc import cli 10from milc import cli
11 11
12from qmk.constants import QMK_FIRMWARE 12from qmk.constants import QMK_FIRMWARE
13from qmk.commands import _find_make 13from qmk.commands import _find_make, get_make_parallel_args
14import qmk.keyboard 14import qmk.keyboard
15import qmk.keymap 15import qmk.keymap
16 16
@@ -28,7 +28,7 @@ def _is_split(keyboard_name):
28 return True if 'SPLIT_KEYBOARD' in rules_mk and rules_mk['SPLIT_KEYBOARD'].lower() == 'yes' else False 28 return True if 'SPLIT_KEYBOARD' in rules_mk and rules_mk['SPLIT_KEYBOARD'].lower() == 'yes' else False
29 29
30 30
31@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs to run.") 31@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.")
32@cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.") 32@cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.")
33@cli.argument('-f', '--filter', arg_only=True, action='append', default=[], help="Filter the list of keyboards based on the supplied value in rules.mk. Supported format is 'SPLIT_KEYBOARD=yes'. May be passed multiple times.") 33@cli.argument('-f', '--filter', arg_only=True, action='append', default=[], help="Filter the list of keyboards based on the supplied value in rules.mk. Supported format is 'SPLIT_KEYBOARD=yes'. May be passed multiple times.")
34@cli.argument('-km', '--keymap', type=str, default='default', help="The keymap name to build. Default is 'default'.") 34@cli.argument('-km', '--keymap', type=str, default='default', help="The keymap name to build. Default is 'default'.")
@@ -80,7 +80,7 @@ all: {keyboard_safe}_binary
80 ) 80 )
81 # yapf: enable 81 # yapf: enable
82 82
83 cli.run([make_cmd, '-j', str(cli.args.parallel), '-f', makefile.as_posix(), 'all'], capture_output=False, stdin=DEVNULL) 83 cli.run([make_cmd, *get_make_parallel_args(cli.args.parallel), '-f', makefile.as_posix(), 'all'], capture_output=False, stdin=DEVNULL)
84 84
85 # Check for failures 85 # Check for failures
86 failures = [f for f in builddir.glob(f'failed.log.{os.getpid()}.*')] 86 failures = [f for f in builddir.glob(f'failed.log.{os.getpid()}.*')]
diff --git a/lib/python/qmk/cli/new/keyboard.py b/lib/python/qmk/cli/new/keyboard.py
index ae4445ca4..9e4232679 100644
--- a/lib/python/qmk/cli/new/keyboard.py
+++ b/lib/python/qmk/cli/new/keyboard.py
@@ -1,11 +1,142 @@
1"""This script automates the creation of keyboards. 1"""This script automates the creation of new keyboard directories using a starter template.
2""" 2"""
3from datetime import date
4import fileinput
5from pathlib import Path
6import re
7import shutil
8
9from qmk.commands import git_get_username
10import qmk.path
3from milc import cli 11from milc import cli
12from milc.questions import choice, question
13
14KEYBOARD_TYPES = ['avr', 'ps2avrgb']
15
16
17def keyboard_name(name):
18 """Callable for argparse validation.
19 """
20 if not validate_keyboard_name(name):
21 raise ValueError
22 return name
4 23
5 24
6@cli.subcommand('Creates a new keyboard') 25def validate_keyboard_name(name):
26 """Returns True if the given keyboard name contains only lowercase a-z, 0-9 and underscore characters.
27 """
28 regex = re.compile(r'^[a-z0-9][a-z0-9/_]+$')
29 return bool(regex.match(name))
30
31
32@cli.argument('-kb', '--keyboard', help='Specify the name for the new keyboard directory', arg_only=True, type=keyboard_name)
33@cli.argument('-t', '--type', help='Specify the keyboard type', arg_only=True, choices=KEYBOARD_TYPES)
34@cli.argument('-u', '--username', help='Specify your username (default from Git config)', arg_only=True)
35@cli.subcommand('Creates a new keyboard directory')
7def new_keyboard(cli): 36def new_keyboard(cli):
8 """Creates a new keyboard 37 """Creates a new keyboard.
9 """ 38 """
10 # TODO: replace this bodge to the existing script 39 cli.log.info('{style_bright}Generating a new QMK keyboard directory{style_normal}')
11 cli.run(['util/new_keyboard.sh'], stdin=None, capture_output=False) 40 cli.echo('')
41
42 # Get keyboard name
43 new_keyboard_name = None
44 while not new_keyboard_name:
45 new_keyboard_name = cli.args.keyboard if cli.args.keyboard else question('Keyboard Name:')
46 if not validate_keyboard_name(new_keyboard_name):
47 cli.log.error('Keyboard names must contain only {fg_cyan}lowercase a-z{fg_reset}, {fg_cyan}0-9{fg_reset}, and {fg_cyan}_{fg_reset}! Please choose a different name.')
48
49 # Exit if passed by arg
50 if cli.args.keyboard:
51 return False
52
53 new_keyboard_name = None
54 continue
55
56 keyboard_path = qmk.path.keyboard(new_keyboard_name)
57 if keyboard_path.exists():
58 cli.log.error(f'Keyboard {{fg_cyan}}{new_keyboard_name}{{fg_reset}} already exists! Please choose a different name.')
59
60 # Exit if passed by arg
61 if cli.args.keyboard:
62 return False
63
64 new_keyboard_name = None
65
66 # Get keyboard type
67 keyboard_type = cli.args.type if cli.args.type else choice('Keyboard Type:', KEYBOARD_TYPES, default=0)
68
69 # Get username
70 user_name = None
71 while not user_name:
72 user_name = question('Your Name:', default=find_user_name())
73
74 if not user_name:
75 cli.log.error('You didn\'t provide a username, and we couldn\'t find one set in your QMK or Git configs. Please try again.')
76
77 # Exit if passed by arg
78 if cli.args.username:
79 return False
80
81 # Copy all the files
82 copy_templates(keyboard_type, keyboard_path)
83
84 # Replace all the placeholders
85 keyboard_basename = keyboard_path.name
86 replacements = [
87 ('%YEAR%', str(date.today().year)),
88 ('%KEYBOARD%', keyboard_basename),
89 ('%YOUR_NAME%', user_name),
90 ]
91 filenames = [
92 keyboard_path / 'config.h',
93 keyboard_path / 'info.json',
94 keyboard_path / 'readme.md',
95 keyboard_path / f'{keyboard_basename}.c',
96 keyboard_path / f'{keyboard_basename}.h',
97 keyboard_path / 'keymaps/default/readme.md',
98 keyboard_path / 'keymaps/default/keymap.c',
99 ]
100 replace_placeholders(replacements, filenames)
101
102 cli.echo('')
103 cli.log.info(f'{{fg_green}}Created a new keyboard called {{fg_cyan}}{new_keyboard_name}{{fg_green}}.{{fg_reset}}')
104 cli.log.info(f'To start working on things, `cd` into {{fg_cyan}}{keyboard_path}{{fg_reset}},')
105 cli.log.info('or open the directory in your preferred text editor.')
106
107
108def find_user_name():
109 if cli.args.username:
110 return cli.args.username
111 elif cli.config.user.name:
112 return cli.config.user.name
113 else:
114 return git_get_username()
115
116
117def copy_templates(keyboard_type, keyboard_path):
118 """Copies the template files from quantum/template to the new keyboard directory.
119 """
120 template_base_path = Path('quantum/template')
121 keyboard_basename = keyboard_path.name
122
123 cli.log.info('Copying base template files...')
124 shutil.copytree(template_base_path / 'base', keyboard_path)
125
126 cli.log.info(f'Copying {{fg_cyan}}{keyboard_type}{{fg_reset}} template files...')
127 shutil.copytree(template_base_path / keyboard_type, keyboard_path, dirs_exist_ok=True)
128
129 cli.log.info(f'Renaming {{fg_cyan}}keyboard.[ch]{{fg_reset}} to {{fg_cyan}}{keyboard_basename}.[ch]{{fg_reset}}...')
130 shutil.move(keyboard_path / 'keyboard.c', keyboard_path / f'{keyboard_basename}.c')
131 shutil.move(keyboard_path / 'keyboard.h', keyboard_path / f'{keyboard_basename}.h')
132
133
134def replace_placeholders(replacements, filenames):
135 """Replaces the given placeholders in each template file.
136 """
137 for replacement in replacements:
138 cli.log.info(f'Replacing {{fg_cyan}}{replacement[0]}{{fg_reset}} with {{fg_cyan}}{replacement[1]}{{fg_reset}}...')
139
140 with fileinput.input(files=filenames, inplace=True) as file:
141 for line in file:
142 print(line.replace(replacement[0], replacement[1]), end='')
diff --git a/lib/python/qmk/cli/pyformat.py b/lib/python/qmk/cli/pyformat.py
index abe5f6de1..c624f74ae 100755
--- a/lib/python/qmk/cli/pyformat.py
+++ b/lib/python/qmk/cli/pyformat.py
@@ -1,26 +1,24 @@
1"""Format python code according to QMK's style. 1"""Point people to the new command name.
2""" 2"""
3from subprocess import CalledProcessError, DEVNULL 3import sys
4from pathlib import Path
4 5
5from milc import cli 6from milc import cli
6 7
7 8
8@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.") 9@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually format.")
9@cli.subcommand("Format python code according to QMK's style.", hidden=False if cli.config.user.developer else True) 10@cli.subcommand('Pointer to the new command name: qmk format-python.', hidden=False if cli.config.user.developer else True)
10def pyformat(cli): 11def pyformat(cli):
11 """Format python code according to QMK's style. 12 """Pointer to the new command name: qmk format-python.
12 """ 13 """
13 edit = '--diff' if cli.args.dry_run else '--in-place' 14 cli.log.warning('"qmk pyformat" has been renamed to "qmk format-python". Please use the new command in the future.')
14 yapf_cmd = ['yapf', '-vv', '--recursive', edit, 'bin/qmk', 'lib/python'] 15 argv = [sys.executable, *sys.argv]
15 try: 16 argv[argv.index('pyformat')] = 'format-python'
16 cli.run(yapf_cmd, check=True, capture_output=False, stdin=DEVNULL) 17 script_path = Path(argv[1])
17 cli.log.info('Python code in `bin/qmk` and `lib/python` is correctly formatted.') 18 script_path_exe = Path(f'{argv[1]}.exe')
18 return True
19 19
20 except CalledProcessError: 20 if not script_path.exists() and script_path_exe.exists():
21 if cli.args.dry_run: 21 # For reasons I don't understand ".exe" is stripped from the script name on windows.
22 cli.log.error('Python code in `bin/qmk` and `lib/python` incorrectly formatted!') 22 argv[1] = str(script_path_exe)
23 else:
24 cli.log.error('Error formatting python code!')
25 23
26 return False 24 return cli.run(argv, capture_output=False).returncode
diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py
index 3a35c1103..421453d83 100644
--- a/lib/python/qmk/commands.py
+++ b/lib/python/qmk/commands.py
@@ -2,6 +2,7 @@
2""" 2"""
3import json 3import json
4import os 4import os
5import sys
5import shutil 6import shutil
6from pathlib import Path 7from pathlib import Path
7from subprocess import DEVNULL 8from subprocess import DEVNULL
@@ -10,7 +11,7 @@ from time import strftime
10from milc import cli 11from milc import cli
11 12
12import qmk.keymap 13import qmk.keymap
13from qmk.constants import KEYBOARD_OUTPUT_PREFIX 14from qmk.constants import QMK_FIRMWARE, KEYBOARD_OUTPUT_PREFIX
14from qmk.json_schema import json_load 15from qmk.json_schema import json_load
15 16
16time_fmt = '%Y-%m-%d-%H:%M:%S' 17time_fmt = '%Y-%m-%d-%H:%M:%S'
@@ -51,7 +52,7 @@ def create_make_target(target, parallel=1, **env_vars):
51 for key, value in env_vars.items(): 52 for key, value in env_vars.items():
52 env.append(f'{key}={value}') 53 env.append(f'{key}={value}')
53 54
54 return [make_cmd, '-j', str(parallel), *env, target] 55 return [make_cmd, *get_make_parallel_args(parallel), *env, target]
55 56
56 57
57def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars): 58def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):
@@ -86,11 +87,17 @@ def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):
86 return create_make_target(':'.join(make_args), parallel, **env_vars) 87 return create_make_target(':'.join(make_args), parallel, **env_vars)
87 88
88 89
89def get_git_version(repo_dir='.', check_dir='.'): 90def get_git_version(current_time, repo_dir='.', check_dir='.'):
90 """Returns the current git version for a repo, or the current time. 91 """Returns the current git version for a repo, or the current time.
91 """ 92 """
92 git_describe_cmd = ['git', 'describe', '--abbrev=6', '--dirty', '--always', '--tags'] 93 git_describe_cmd = ['git', 'describe', '--abbrev=6', '--dirty', '--always', '--tags']
93 94
95 if repo_dir != '.':
96 repo_dir = Path('lib') / repo_dir
97
98 if check_dir != '.':
99 check_dir = repo_dir / check_dir
100
94 if Path(check_dir).exists(): 101 if Path(check_dir).exists():
95 git_describe = cli.run(git_describe_cmd, stdin=DEVNULL, cwd=repo_dir) 102 git_describe = cli.run(git_describe_cmd, stdin=DEVNULL, cwd=repo_dir)
96 103
@@ -100,23 +107,58 @@ def get_git_version(repo_dir='.', check_dir='.'):
100 else: 107 else:
101 cli.log.warn(f'"{" ".join(git_describe_cmd)}" returned error code {git_describe.returncode}') 108 cli.log.warn(f'"{" ".join(git_describe_cmd)}" returned error code {git_describe.returncode}')
102 print(git_describe.stderr) 109 print(git_describe.stderr)
103 return strftime(time_fmt) 110 return current_time
111
112 return current_time
113
114
115def get_make_parallel_args(parallel=1):
116 """Returns the arguments for running the specified number of parallel jobs.
117 """
118 parallel_args = []
119
120 if int(parallel) <= 0:
121 # 0 or -1 means -j without argument (unlimited jobs)
122 parallel_args.append('--jobs')
123 else:
124 parallel_args.append('--jobs=' + str(parallel))
125
126 if int(parallel) != 1:
127 # If more than 1 job is used, synchronize parallel output by target
128 parallel_args.append('--output-sync=target')
104 129
105 return strftime(time_fmt) 130 return parallel_args
106 131
107 132
108def write_version_h(git_version, build_date, chibios_version, chibios_contrib_version): 133def create_version_h(skip_git=False, skip_all=False):
109 """Generate and write quantum/version.h 134 """Generate version.h contents
110 """ 135 """
111 version_h = [ 136 if skip_all:
112 f'#define QMK_VERSION "{git_version}"', 137 current_time = "1970-01-01-00:00:00"
113 f'#define QMK_BUILDDATE "{build_date}"', 138 else:
114 f'#define CHIBIOS_VERSION "{chibios_version}"', 139 current_time = strftime(time_fmt)
115 f'#define CHIBIOS_CONTRIB_VERSION "{chibios_contrib_version}"', 140
116 ] 141 if skip_git:
142 git_version = "NA"
143 chibios_version = "NA"
144 chibios_contrib_version = "NA"
145 else:
146 git_version = get_git_version(current_time)
147 chibios_version = get_git_version(current_time, "chibios", "os")
148 chibios_contrib_version = get_git_version(current_time, "chibios-contrib", "os")
149
150 version_h_lines = f"""/* This file was automatically generated. Do not edit or copy.
151 */
152
153#pragma once
154
155#define QMK_VERSION "{git_version}"
156#define QMK_BUILDDATE "{current_time}"
157#define CHIBIOS_VERSION "{chibios_version}"
158#define CHIBIOS_CONTRIB_VERSION "{chibios_contrib_version}"
159"""
117 160
118 version_h_file = Path('quantum/version.h') 161 return version_h_lines
119 version_h_file.write_text('\n'.join(version_h))
120 162
121 163
122def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_vars): 164def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_vars):
@@ -149,13 +191,8 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
149 keymap_dir.mkdir(exist_ok=True, parents=True) 191 keymap_dir.mkdir(exist_ok=True, parents=True)
150 keymap_c.write_text(c_text) 192 keymap_c.write_text(c_text)
151 193
152 # Write the version.h file 194 version_h = Path('quantum/version.h')
153 git_version = get_git_version() 195 version_h.write_text(create_version_h())
154 build_date = strftime('%Y-%m-%d-%H:%M:%S')
155 chibios_version = get_git_version("lib/chibios", "lib/chibios/os")
156 chibios_contrib_version = get_git_version("lib/chibios-contrib", "lib/chibios-contrib/os")
157
158 write_version_h(git_version, build_date, chibios_version, chibios_contrib_version)
159 196
160 # Return a command that can be run to make the keymap and flash if given 197 # Return a command that can be run to make the keymap and flash if given
161 verbose = 'true' if cli.config.general.verbose else 'false' 198 verbose = 'true' if cli.config.general.verbose else 'false'
@@ -166,8 +203,7 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
166 make_command.append('-s') 203 make_command.append('-s')
167 204
168 make_command.extend([ 205 make_command.extend([
169 '-j', 206 *get_make_parallel_args(parallel),
170 str(parallel),
171 '-r', 207 '-r',
172 '-R', 208 '-R',
173 '-f', 209 '-f',
@@ -181,10 +217,6 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
181 make_command.append(f'{key}={value}') 217 make_command.append(f'{key}={value}')
182 218
183 make_command.extend([ 219 make_command.extend([
184 f'GIT_VERSION={git_version}',
185 f'BUILD_DATE={build_date}',
186 f'CHIBIOS_VERSION={chibios_version}',
187 f'CHIBIOS_CONTRIB_VERSION={chibios_contrib_version}',
188 f'KEYBOARD={user_keymap["keyboard"]}', 220 f'KEYBOARD={user_keymap["keyboard"]}',
189 f'KEYMAP={user_keymap["keymap"]}', 221 f'KEYMAP={user_keymap["keymap"]}',
190 f'KEYBOARD_FILESAFE={keyboard_filesafe}', 222 f'KEYBOARD_FILESAFE={keyboard_filesafe}',
@@ -223,3 +255,80 @@ def parse_configurator_json(configurator_file):
223 user_keymap['layout'] = aliases[orig_keyboard]['layouts'][user_keymap['layout']] 255 user_keymap['layout'] = aliases[orig_keyboard]['layouts'][user_keymap['layout']]
224 256
225 return user_keymap 257 return user_keymap
258
259
260def git_get_username():
261 """Retrieves user's username from Git config, if set.
262 """
263 git_username = cli.run(['git', 'config', '--get', 'user.name'])
264
265 if git_username.returncode == 0 and git_username.stdout:
266 return git_username.stdout.strip()
267
268
269def git_check_repo():
270 """Checks that the .git directory exists inside QMK_HOME.
271
272 This is a decent enough indicator that the qmk_firmware directory is a
273 proper Git repository, rather than a .zip download from GitHub.
274 """
275 dot_git_dir = QMK_FIRMWARE / '.git'
276
277 return dot_git_dir.is_dir()
278
279
280def git_get_branch():
281 """Returns the current branch for a repo, or None.
282 """
283 git_branch = cli.run(['git', 'branch', '--show-current'])
284 if not git_branch.returncode != 0 or not git_branch.stdout:
285 # Workaround for Git pre-2.22
286 git_branch = cli.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'])
287
288 if git_branch.returncode == 0:
289 return git_branch.stdout.strip()
290
291
292def git_is_dirty():
293 """Returns 1 if repo is dirty, or 0 if clean
294 """
295 git_diff_staged_cmd = ['git', 'diff', '--quiet']
296 git_diff_unstaged_cmd = [*git_diff_staged_cmd, '--cached']
297
298 unstaged = cli.run(git_diff_staged_cmd)
299 staged = cli.run(git_diff_unstaged_cmd)
300
301 return unstaged.returncode != 0 or staged.returncode != 0
302
303
304def git_get_remotes():
305 """Returns the current remotes for a repo.
306 """
307 remotes = {}
308
309 git_remote_show_cmd = ['git', 'remote', 'show']
310 git_remote_get_cmd = ['git', 'remote', 'get-url']
311
312 git_remote_show = cli.run(git_remote_show_cmd)
313 if git_remote_show.returncode == 0:
314 for name in git_remote_show.stdout.splitlines():
315 git_remote_name = cli.run([*git_remote_get_cmd, name])
316 remotes[name.strip()] = {"url": git_remote_name.stdout.strip()}
317
318 return remotes
319
320
321def git_check_deviation(active_branch):
322 """Return True if branch has custom commits
323 """
324 cli.run(['git', 'fetch', 'upstream', active_branch])
325 deviations = cli.run(['git', '--no-pager', 'log', f'upstream/{active_branch}...{active_branch}'])
326 return bool(deviations.returncode)
327
328
329def in_virtualenv():
330 """Check if running inside a virtualenv.
331 Based on https://stackoverflow.com/a/1883251
332 """
333 active_prefix = getattr(sys, "base_prefix", None) or getattr(sys, "real_prefix", None) or sys.prefix
334 return active_prefix != sys.prefix
diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py
index 49e5e0eb4..71a6c91c7 100644
--- a/lib/python/qmk/constants.py
+++ b/lib/python/qmk/constants.py
@@ -6,11 +6,14 @@ from pathlib import Path
6# The root of the qmk_firmware tree. 6# The root of the qmk_firmware tree.
7QMK_FIRMWARE = Path.cwd() 7QMK_FIRMWARE = Path.cwd()
8 8
9# Upstream repo url
10QMK_FIRMWARE_UPSTREAM = 'qmk/qmk_firmware'
11
9# This is the number of directories under `qmk_firmware/keyboards` that will be traversed. This is currently a limitation of our make system. 12# This is the number of directories under `qmk_firmware/keyboards` that will be traversed. This is currently a limitation of our make system.
10MAX_KEYBOARD_SUBFOLDERS = 5 13MAX_KEYBOARD_SUBFOLDERS = 5
11 14
12# Supported processor types 15# Supported processor types
13CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'MK66F18', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L433', 'STM32L443' 16CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'MK66F18', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F407', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L412', 'STM32L422', 'STM32L433', 'STM32L443'
14LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None 17LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None
15VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85' 18VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85'
16 19
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index 5fc14dc85..7f3aabdc3 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -9,7 +9,7 @@ from milc import cli
9 9
10from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS 10from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS
11from qmk.c_parse import find_layouts 11from qmk.c_parse import find_layouts
12from qmk.json_schema import deep_update, json_load, keyboard_validate, keyboard_api_validate 12from qmk.json_schema import deep_update, json_load, validate
13from qmk.keyboard import config_h, rules_mk 13from qmk.keyboard import config_h, rules_mk
14from qmk.keymap import list_keymaps 14from qmk.keymap import list_keymaps
15from qmk.makefile import parse_rules_mk_file 15from qmk.makefile import parse_rules_mk_file
@@ -49,7 +49,7 @@ def info_json(keyboard):
49 info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'} 49 info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'}
50 50
51 # Populate layout data 51 # Populate layout data
52 layouts, aliases = _find_all_layouts(info_data, keyboard) 52 layouts, aliases = _search_keyboard_h(keyboard)
53 53
54 if aliases: 54 if aliases:
55 info_data['layout_aliases'] = aliases 55 info_data['layout_aliases'] = aliases
@@ -61,12 +61,15 @@ def info_json(keyboard):
61 61
62 # Merge in the data from info.json, config.h, and rules.mk 62 # Merge in the data from info.json, config.h, and rules.mk
63 info_data = merge_info_jsons(keyboard, info_data) 63 info_data = merge_info_jsons(keyboard, info_data)
64 info_data = _extract_config_h(info_data)
65 info_data = _extract_rules_mk(info_data) 64 info_data = _extract_rules_mk(info_data)
65 info_data = _extract_config_h(info_data)
66
67 # Ensure that we have matrix row and column counts
68 info_data = _matrix_size(info_data)
66 69
67 # Validate against the jsonschema 70 # Validate against the jsonschema
68 try: 71 try:
69 keyboard_api_validate(info_data) 72 validate(info_data, 'qmk.api.keyboard.v1')
70 73
71 except jsonschema.ValidationError as e: 74 except jsonschema.ValidationError as e:
72 json_path = '.'.join([str(p) for p in e.absolute_path]) 75 json_path = '.'.join([str(p) for p in e.absolute_path])
@@ -75,6 +78,9 @@ def info_json(keyboard):
75 78
76 # Make sure we have at least one layout 79 # Make sure we have at least one layout
77 if not info_data.get('layouts'): 80 if not info_data.get('layouts'):
81 _find_missing_layouts(info_data, keyboard)
82
83 if not info_data.get('layouts'):
78 _log_error(info_data, 'No LAYOUTs defined! Need at least one layout defined in the keyboard.h or info.json.') 84 _log_error(info_data, 'No LAYOUTs defined! Need at least one layout defined in the keyboard.h or info.json.')
79 85
80 # Filter out any non-existing community layouts 86 # Filter out any non-existing community layouts
@@ -90,6 +96,9 @@ def info_json(keyboard):
90 if layout_name not in info_data.get('layouts', {}) and layout_name not in info_data.get('layout_aliases', {}): 96 if layout_name not in info_data.get('layouts', {}) and layout_name not in info_data.get('layout_aliases', {}):
91 _log_error(info_data, 'Claims to support community layout %s but no %s() macro found' % (layout, layout_name)) 97 _log_error(info_data, 'Claims to support community layout %s but no %s() macro found' % (layout, layout_name))
92 98
99 # Check that the reported matrix size is consistent with the actual matrix size
100 _check_matrix(info_data)
101
93 return info_data 102 return info_data
94 103
95 104
@@ -143,10 +152,7 @@ def _pin_name(pin):
143 elif pin == 'NO_PIN': 152 elif pin == 'NO_PIN':
144 return None 153 return None
145 154
146 elif pin[0] in 'ABCDEFGHIJK' and pin[1].isdigit(): 155 return pin
147 return pin
148
149 raise ValueError(f'Invalid pin: {pin}')
150 156
151 157
152def _extract_pins(pins): 158def _extract_pins(pins):
@@ -155,10 +161,9 @@ def _extract_pins(pins):
155 return [_pin_name(pin) for pin in pins.split(',')] 161 return [_pin_name(pin) for pin in pins.split(',')]
156 162
157 163
158def _extract_direct_matrix(info_data, direct_pins): 164def _extract_direct_matrix(direct_pins):
159 """ 165 """
160 """ 166 """
161 info_data['matrix_pins'] = {}
162 direct_pin_array = [] 167 direct_pin_array = []
163 168
164 while direct_pins[-1] != '}': 169 while direct_pins[-1] != '}':
@@ -182,12 +187,157 @@ def _extract_direct_matrix(info_data, direct_pins):
182 return direct_pin_array 187 return direct_pin_array
183 188
184 189
190def _extract_audio(info_data, config_c):
191 """Populate data about the audio configuration
192 """
193 audio_pins = []
194
195 for pin in 'B5', 'B6', 'B7', 'C4', 'C5', 'C6':
196 if config_c.get(f'{pin}_AUDIO'):
197 audio_pins.append(pin)
198
199 if audio_pins:
200 info_data['audio'] = {'pins': audio_pins}
201
202
203def _extract_split_main(info_data, config_c):
204 """Populate data about the split configuration
205 """
206 # Figure out how the main half is determined
207 if config_c.get('SPLIT_HAND_PIN') is True:
208 if 'split' not in info_data:
209 info_data['split'] = {}
210
211 if 'main' in info_data['split']:
212 _log_warning(info_data, 'Split main hand is specified in both config.h (SPLIT_HAND_PIN) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main'])
213
214 info_data['split']['main'] = 'pin'
215
216 if config_c.get('SPLIT_HAND_MATRIX_GRID'):
217 if 'split' not in info_data:
218 info_data['split'] = {}
219
220 if 'main' in info_data['split']:
221 _log_warning(info_data, 'Split main hand is specified in both config.h (SPLIT_HAND_MATRIX_GRID) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main'])
222
223 info_data['split']['main'] = 'matrix_grid'
224 info_data['split']['matrix_grid'] = _extract_pins(config_c['SPLIT_HAND_MATRIX_GRID'])
225
226 if config_c.get('EE_HANDS') is True:
227 if 'split' not in info_data:
228 info_data['split'] = {}
229
230 if 'main' in info_data['split']:
231 _log_warning(info_data, 'Split main hand is specified in both config.h (EE_HANDS) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main'])
232
233 info_data['split']['main'] = 'eeprom'
234
235 if config_c.get('MASTER_RIGHT') is True:
236 if 'split' not in info_data:
237 info_data['split'] = {}
238
239 if 'main' in info_data['split']:
240 _log_warning(info_data, 'Split main hand is specified in both config.h (MASTER_RIGHT) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main'])
241
242 info_data['split']['main'] = 'right'
243
244 if config_c.get('MASTER_LEFT') is True:
245 if 'split' not in info_data:
246 info_data['split'] = {}
247
248 if 'main' in info_data['split']:
249 _log_warning(info_data, 'Split main hand is specified in both config.h (MASTER_LEFT) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main'])
250
251 info_data['split']['main'] = 'left'
252
253
254def _extract_split_transport(info_data, config_c):
255 # Figure out the transport method
256 if config_c.get('USE_I2C') is True:
257 if 'split' not in info_data:
258 info_data['split'] = {}
259
260 if 'transport' not in info_data['split']:
261 info_data['split']['transport'] = {}
262
263 if 'protocol' in info_data['split']['transport']:
264 _log_warning(info_data, 'Split transport is specified in both config.h (USE_I2C) and info.json (split.transport.protocol) (Value: %s), the config.h value wins.' % info_data['split']['transport'])
265
266 info_data['split']['transport']['protocol'] = 'i2c'
267
268 elif 'protocol' not in info_data.get('split', {}).get('transport', {}):
269 if 'split' not in info_data:
270 info_data['split'] = {}
271
272 if 'transport' not in info_data['split']:
273 info_data['split']['transport'] = {}
274
275 info_data['split']['transport']['protocol'] = 'serial'
276
277
278def _extract_split_right_pins(info_data, config_c):
279 # Figure out the right half matrix pins
280 row_pins = config_c.get('MATRIX_ROW_PINS_RIGHT', '').replace('{', '').replace('}', '').strip()
281 col_pins = config_c.get('MATRIX_COL_PINS_RIGHT', '').replace('{', '').replace('}', '').strip()
282 unused_pin_text = config_c.get('UNUSED_PINS_RIGHT')
283 unused_pins = unused_pin_text.replace('{', '').replace('}', '').strip() if isinstance(unused_pin_text, str) else None
284 direct_pins = config_c.get('DIRECT_PINS_RIGHT', '').replace(' ', '')[1:-1]
285
286 if row_pins and col_pins:
287 if info_data.get('split', {}).get('matrix_pins', {}).get('right') in info_data:
288 _log_warning(info_data, 'Right hand matrix data is specified in both info.json and config.h, the config.h values win.')
289
290 if 'split' not in info_data:
291 info_data['split'] = {}
292
293 if 'matrix_pins' not in info_data['split']:
294 info_data['split']['matrix_pins'] = {}
295
296 if 'right' not in info_data['split']['matrix_pins']:
297 info_data['split']['matrix_pins']['right'] = {}
298
299 info_data['split']['matrix_pins']['right'] = {
300 'cols': _extract_pins(col_pins),
301 'rows': _extract_pins(row_pins),
302 }
303
304 if direct_pins:
305 if info_data.get('split', {}).get('matrix_pins', {}).get('right', {}):
306 _log_warning(info_data, 'Right hand matrix data is specified in both info.json and config.h, the config.h values win.')
307
308 if 'split' not in info_data:
309 info_data['split'] = {}
310
311 if 'matrix_pins' not in info_data['split']:
312 info_data['split']['matrix_pins'] = {}
313
314 if 'right' not in info_data['split']['matrix_pins']:
315 info_data['split']['matrix_pins']['right'] = {}
316
317 info_data['split']['matrix_pins']['right']['direct'] = _extract_direct_matrix(direct_pins)
318
319 if unused_pins:
320 if 'split' not in info_data:
321 info_data['split'] = {}
322
323 if 'matrix_pins' not in info_data['split']:
324 info_data['split']['matrix_pins'] = {}
325
326 if 'right' not in info_data['split']['matrix_pins']:
327 info_data['split']['matrix_pins']['right'] = {}
328
329 info_data['split']['matrix_pins']['right']['unused'] = _extract_pins(unused_pins)
330
331
185def _extract_matrix_info(info_data, config_c): 332def _extract_matrix_info(info_data, config_c):
186 """Populate the matrix information. 333 """Populate the matrix information.
187 """ 334 """
188 row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip() 335 row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip()
189 col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip() 336 col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip()
337 unused_pin_text = config_c.get('UNUSED_PINS')
338 unused_pins = unused_pin_text.replace('{', '').replace('}', '').strip() if isinstance(unused_pin_text, str) else None
190 direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1] 339 direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1]
340 info_snippet = {}
191 341
192 if 'MATRIX_ROWS' in config_c and 'MATRIX_COLS' in config_c: 342 if 'MATRIX_ROWS' in config_c and 'MATRIX_COLS' in config_c:
193 if 'matrix_size' in info_data: 343 if 'matrix_size' in info_data:
@@ -199,19 +349,35 @@ def _extract_matrix_info(info_data, config_c):
199 } 349 }
200 350
201 if row_pins and col_pins: 351 if row_pins and col_pins:
202 if 'matrix_pins' in info_data: 352 if 'matrix_pins' in info_data and 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']:
203 _log_warning(info_data, 'Matrix pins are specified in both info.json and config.h, the config.h values win.') 353 _log_warning(info_data, 'Matrix pins are specified in both info.json and config.h, the config.h values win.')
204 354
205 info_data['matrix_pins'] = { 355 info_snippet['cols'] = _extract_pins(col_pins)
206 'cols': _extract_pins(col_pins), 356 info_snippet['rows'] = _extract_pins(row_pins)
207 'rows': _extract_pins(row_pins),
208 }
209 357
210 if direct_pins: 358 if direct_pins:
211 if 'matrix_pins' in info_data: 359 if 'matrix_pins' in info_data and 'direct' in info_data['matrix_pins']:
212 _log_warning(info_data, 'Direct pins are specified in both info.json and config.h, the config.h values win.') 360 _log_warning(info_data, 'Direct pins are specified in both info.json and config.h, the config.h values win.')
213 361
214 info_data['matrix_pins']['direct'] = _extract_direct_matrix(info_data, direct_pins) 362 info_snippet['direct'] = _extract_direct_matrix(direct_pins)
363
364 if unused_pins:
365 if 'matrix_pins' not in info_data:
366 info_data['matrix_pins'] = {}
367
368 info_snippet['unused'] = _extract_pins(unused_pins)
369
370 if config_c.get('CUSTOM_MATRIX', 'no') != 'no':
371 if 'matrix_pins' in info_data and 'custom' in info_data['matrix_pins']:
372 _log_warning(info_data, 'Custom Matrix is specified in both info.json and config.h, the config.h values win.')
373
374 info_snippet['custom'] = True
375
376 if config_c['CUSTOM_MATRIX'] == 'lite':
377 info_snippet['custom_lite'] = True
378
379 if info_snippet:
380 info_data['matrix_pins'] = info_snippet
215 381
216 return info_data 382 return info_data
217 383
@@ -269,6 +435,10 @@ def _extract_config_h(info_data):
269 435
270 # Pull data that easily can't be mapped in json 436 # Pull data that easily can't be mapped in json
271 _extract_matrix_info(info_data, config_c) 437 _extract_matrix_info(info_data, config_c)
438 _extract_audio(info_data, config_c)
439 _extract_split_main(info_data, config_c)
440 _extract_split_transport(info_data, config_c)
441 _extract_split_right_pins(info_data, config_c)
272 442
273 return info_data 443 return info_data
274 444
@@ -341,12 +511,53 @@ def _extract_rules_mk(info_data):
341 return info_data 511 return info_data
342 512
343 513
344def _search_keyboard_h(path): 514def _matrix_size(info_data):
515 """Add info_data['matrix_size'] if it doesn't exist.
516 """
517 if 'matrix_size' not in info_data and 'matrix_pins' in info_data:
518 info_data['matrix_size'] = {}
519
520 if 'direct' in info_data['matrix_pins']:
521 info_data['matrix_size']['cols'] = len(info_data['matrix_pins']['direct'][0])
522 info_data['matrix_size']['rows'] = len(info_data['matrix_pins']['direct'])
523 elif 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']:
524 info_data['matrix_size']['cols'] = len(info_data['matrix_pins']['cols'])
525 info_data['matrix_size']['rows'] = len(info_data['matrix_pins']['rows'])
526
527 return info_data
528
529
530def _check_matrix(info_data):
531 """Check the matrix to ensure that row/column count is consistent.
532 """
533 if 'matrix_pins' in info_data and 'matrix_size' in info_data:
534 actual_col_count = info_data['matrix_size'].get('cols', 0)
535 actual_row_count = info_data['matrix_size'].get('rows', 0)
536 col_count = row_count = 0
537
538 if 'direct' in info_data['matrix_pins']:
539 col_count = len(info_data['matrix_pins']['direct'][0])
540 row_count = len(info_data['matrix_pins']['direct'])
541 elif 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']:
542 col_count = len(info_data['matrix_pins']['cols'])
543 row_count = len(info_data['matrix_pins']['rows'])
544
545 if col_count != actual_col_count and col_count != (actual_col_count / 2):
546 # FIXME: once we can we should detect if split is enabled to do the actual_col_count/2 check.
547 _log_error(info_data, f'MATRIX_COLS is inconsistent with the size of MATRIX_COL_PINS: {col_count} != {actual_col_count}')
548
549 if row_count != actual_row_count and row_count != (actual_row_count / 2):
550 # FIXME: once we can we should detect if split is enabled to do the actual_row_count/2 check.
551 _log_error(info_data, f'MATRIX_ROWS is inconsistent with the size of MATRIX_ROW_PINS: {row_count} != {actual_row_count}')
552
553
554def _search_keyboard_h(keyboard):
555 keyboard = Path(keyboard)
345 current_path = Path('keyboards/') 556 current_path = Path('keyboards/')
346 aliases = {} 557 aliases = {}
347 layouts = {} 558 layouts = {}
348 559
349 for directory in path.parts: 560 for directory in keyboard.parts:
350 current_path = current_path / directory 561 current_path = current_path / directory
351 keyboard_h = '%s.h' % (directory,) 562 keyboard_h = '%s.h' % (directory,)
352 keyboard_h_path = current_path / keyboard_h 563 keyboard_h_path = current_path / keyboard_h
@@ -361,27 +572,28 @@ def _search_keyboard_h(path):
361 return layouts, aliases 572 return layouts, aliases
362 573
363 574
364def _find_all_layouts(info_data, keyboard): 575def _find_missing_layouts(info_data, keyboard):
365 """Looks for layout macros associated with this keyboard. 576 """Looks for layout macros when they aren't found other places.
366 """
367 layouts, aliases = _search_keyboard_h(Path(keyboard))
368 577
369 if not layouts: 578 If we don't find any layouts from info.json or keyboard.h we widen our search. This is error prone which is why we want to encourage people to follow the standard above.
370 # If we don't find any layouts from info.json or keyboard.h we widen our search. This is error prone which is why we want to encourage people to follow the standard above. 579 """
371 info_data['parse_warnings'].append('%s: Falling back to searching for KEYMAP/LAYOUT macros.' % (keyboard)) 580 _log_warning(info_data, '%s: Falling back to searching for KEYMAP/LAYOUT macros.' % (keyboard))
372 581
373 for file in glob('keyboards/%s/*.h' % keyboard): 582 for file in glob('keyboards/%s/*.h' % keyboard):
374 if file.endswith('.h'): 583 these_layouts, these_aliases = find_layouts(file)
375 these_layouts, these_aliases = find_layouts(file)
376 584
377 if these_layouts: 585 if these_layouts:
378 layouts.update(these_layouts) 586 for layout_name, layout_json in these_layouts.items():
587 if not layout_name.startswith('LAYOUT_kc'):
588 layout_json['c_macro'] = True
589 info_data['layouts'][layout_name] = layout_json
379 590
380 for alias, alias_text in these_aliases.items(): 591 for alias, alias_text in these_aliases.items():
381 if alias_text in layouts: 592 if alias_text in these_layouts:
382 aliases[alias] = alias_text 593 if 'layout_aliases' not in info_data:
594 info_data['layout_aliases'] = {}
383 595
384 return layouts, aliases 596 info_data['layout_aliases'][alias] = alias_text
385 597
386 598
387def _log_error(info_data, message): 599def _log_error(info_data, message):
@@ -460,7 +672,7 @@ def merge_info_jsons(keyboard, info_data):
460 continue 672 continue
461 673
462 try: 674 try:
463 keyboard_validate(new_info_data) 675 validate(new_info_data, 'qmk.keyboard.v1')
464 except jsonschema.ValidationError as e: 676 except jsonschema.ValidationError as e:
465 json_path = '.'.join([str(p) for p in e.absolute_path]) 677 json_path = '.'.join([str(p) for p in e.absolute_path])
466 cli.log.error('Not including data from file: %s', info_file) 678 cli.log.error('Not including data from file: %s', info_file)
diff --git a/lib/python/qmk/json_encoders.py b/lib/python/qmk/json_encoders.py
index 9f3da022b..72e91973a 100755
--- a/lib/python/qmk/json_encoders.py
+++ b/lib/python/qmk/json_encoders.py
@@ -102,9 +102,6 @@ class InfoJSONEncoder(QMKJSONEncoder):
102 elif key == 'maintainer': 102 elif key == 'maintainer':
103 return '12maintainer' 103 return '12maintainer'
104 104
105 elif key in ('height', 'width'):
106 return '40' + str(key)
107
108 elif key == 'community_layouts': 105 elif key == 'community_layouts':
109 return '97community_layouts' 106 return '97community_layouts'
110 107
diff --git a/lib/python/qmk/json_schema.py b/lib/python/qmk/json_schema.py
index f3992ee71..ffc7c6bcd 100644
--- a/lib/python/qmk/json_schema.py
+++ b/lib/python/qmk/json_schema.py
@@ -2,6 +2,7 @@
2""" 2"""
3import json 3import json
4from collections.abc import Mapping 4from collections.abc import Mapping
5from functools import lru_cache
5from pathlib import Path 6from pathlib import Path
6 7
7import hjson 8import hjson
@@ -25,11 +26,13 @@ def json_load(json_file):
25 exit(1) 26 exit(1)
26 27
27 28
29@lru_cache(maxsize=0)
28def load_jsonschema(schema_name): 30def load_jsonschema(schema_name):
29 """Read a jsonschema file from disk. 31 """Read a jsonschema file from disk.
30
31 FIXME(skullydazed/anyone): Refactor to make this a public function.
32 """ 32 """
33 if Path(schema_name).exists():
34 return json_load(schema_name)
35
33 schema_path = Path(f'data/schemas/{schema_name}.jsonschema') 36 schema_path = Path(f'data/schemas/{schema_name}.jsonschema')
34 37
35 if not schema_path.exists(): 38 if not schema_path.exists():
@@ -38,28 +41,42 @@ def load_jsonschema(schema_name):
38 return json_load(schema_path) 41 return json_load(schema_path)
39 42
40 43
41def keyboard_validate(data): 44@lru_cache(maxsize=0)
42 """Validates data against the keyboard jsonschema. 45def compile_schema_store():
46 """Compile all our schemas into a schema store.
43 """ 47 """
44 schema = load_jsonschema('keyboard') 48 schema_store = {}
45 validator = jsonschema.Draft7Validator(schema).validate
46 49
47 return validator(data) 50 for schema_file in Path('data/schemas').glob('*.jsonschema'):
51 schema_data = load_jsonschema(schema_file)
52 if not isinstance(schema_data, dict):
53 cli.log.debug('Skipping schema file %s', schema_file)
54 continue
55 schema_store[schema_data['$id']] = schema_data
56
57 return schema_store
58
59
60@lru_cache(maxsize=0)
61def create_validator(schema):
62 """Creates a validator for the given schema id.
63 """
64 schema_store = compile_schema_store()
65 resolver = jsonschema.RefResolver.from_schema(schema_store['qmk.keyboard.v1'], store=schema_store)
66
67 return jsonschema.Draft7Validator(schema_store[schema], resolver=resolver).validate
48 68
49 69
50def keyboard_api_validate(data): 70def validate(data, schema):
51 """Validates data against the api_keyboard jsonschema. 71 """Validates data against a schema.
52 """ 72 """
53 base = load_jsonschema('keyboard') 73 validator = create_validator(schema)
54 relative = load_jsonschema('api_keyboard')
55 resolver = jsonschema.RefResolver.from_schema(base)
56 validator = jsonschema.Draft7Validator(relative, resolver=resolver).validate
57 74
58 return validator(data) 75 return validator(data)
59 76
60 77
61def deep_update(origdict, newdict): 78def deep_update(origdict, newdict):
62 """Update a dictionary in place, recursing to do a deep copy. 79 """Update a dictionary in place, recursing to do a depth-first deep copy.
63 """ 80 """
64 for key, value in newdict.items(): 81 for key, value in newdict.items():
65 if isinstance(value, Mapping): 82 if isinstance(value, Mapping):
diff --git a/lib/python/qmk/tests/minimal_info.json b/lib/python/qmk/tests/minimal_info.json
index b91c23bd3..11ef12fef 100644
--- a/lib/python/qmk/tests/minimal_info.json
+++ b/lib/python/qmk/tests/minimal_info.json
@@ -1,8 +1,6 @@
1{ 1{
2 "keyboard_name": "tester", 2 "keyboard_name": "tester",
3 "maintainer": "qmk", 3 "maintainer": "qmk",
4 "height": 5,
5 "width": 15,
6 "layouts": { 4 "layouts": {
7 "LAYOUT": { 5 "LAYOUT": {
8 "layout": [ 6 "layout": [
diff --git a/lib/python/qmk/tests/test_cli_commands.py b/lib/python/qmk/tests/test_cli_commands.py
index afdbc8142..b39fe5e46 100644
--- a/lib/python/qmk/tests/test_cli_commands.py
+++ b/lib/python/qmk/tests/test_cli_commands.py
@@ -31,13 +31,13 @@ def check_returncode(result, expected=[0]):
31 assert result.returncode in expected 31 assert result.returncode in expected
32 32
33 33
34def test_cformat(): 34def test_format_c():
35 result = check_subcommand('cformat', '-n', 'quantum/matrix.c') 35 result = check_subcommand('format-c', '-n', 'quantum/matrix.c')
36 check_returncode(result) 36 check_returncode(result)
37 37
38 38
39def test_cformat_all(): 39def test_format_c_all():
40 result = check_subcommand('cformat', '-n', '-a') 40 result = check_subcommand('format-c', '-n', '-a')
41 check_returncode(result, [0, 1]) 41 check_returncode(result, [0, 1])
42 42
43 43
@@ -80,8 +80,8 @@ def test_hello():
80 assert 'Hello,' in result.stdout 80 assert 'Hello,' in result.stdout
81 81
82 82
83def test_pyformat(): 83def test_format_python():
84 result = check_subcommand('pyformat', '--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 `bin/qmk` and `lib/python` is correctly formatted.' in result.stdout
87 87
@@ -258,6 +258,12 @@ def test_generate_rules_mk():
258 assert 'MCU ?= atmega32u4' in result.stdout 258 assert 'MCU ?= atmega32u4' in result.stdout
259 259
260 260
261def test_generate_version_h():
262 result = check_subcommand('generate-version-h')
263 check_returncode(result)
264 assert '#define QMK_VERSION' in result.stdout
265
266
261def test_generate_layouts(): 267def test_generate_layouts():
262 result = check_subcommand('generate-layouts', '-kb', 'handwired/pytest/basic') 268 result = check_subcommand('generate-layouts', '-kb', 'handwired/pytest/basic')
263 check_returncode(result) 269 check_returncode(result)
@@ -267,7 +273,7 @@ def test_generate_layouts():
267def test_format_json_keyboard(): 273def test_format_json_keyboard():
268 result = check_subcommand('format-json', '--format', 'keyboard', 'lib/python/qmk/tests/minimal_info.json') 274 result = check_subcommand('format-json', '--format', 'keyboard', 'lib/python/qmk/tests/minimal_info.json')
269 check_returncode(result) 275 check_returncode(result)
270 assert result.stdout == '{\n "keyboard_name": "tester",\n "maintainer": "qmk",\n "height": 5,\n "width": 15,\n "layouts": {\n "LAYOUT": {\n "layout": [\n { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }\n ]\n }\n }\n}\n' 276 assert result.stdout == '{\n "keyboard_name": "tester",\n "maintainer": "qmk",\n "layouts": {\n "LAYOUT": {\n "layout": [\n { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }\n ]\n }\n }\n}\n'
271 277
272 278
273def test_format_json_keymap(): 279def test_format_json_keymap():
@@ -279,7 +285,7 @@ def test_format_json_keymap():
279def test_format_json_keyboard_auto(): 285def test_format_json_keyboard_auto():
280 result = check_subcommand('format-json', '--format', 'auto', 'lib/python/qmk/tests/minimal_info.json') 286 result = check_subcommand('format-json', '--format', 'auto', 'lib/python/qmk/tests/minimal_info.json')
281 check_returncode(result) 287 check_returncode(result)
282 assert result.stdout == '{\n "keyboard_name": "tester",\n "maintainer": "qmk",\n "height": 5,\n "width": 15,\n "layouts": {\n "LAYOUT": {\n "layout": [\n { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }\n ]\n }\n }\n}\n' 288 assert result.stdout == '{\n "keyboard_name": "tester",\n "maintainer": "qmk",\n "layouts": {\n "LAYOUT": {\n "layout": [\n { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }\n ]\n }\n }\n}\n'
283 289
284 290
285def test_format_json_keymap_auto(): 291def test_format_json_keymap_auto():