diff options
Diffstat (limited to 'lib/python')
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 | """ |
3 | from os import path | 3 | import sys |
4 | from shutil import which | 4 | from pathlib import Path |
5 | from subprocess import CalledProcessError, DEVNULL, Popen, PIPE | ||
6 | 5 | ||
7 | from argcomplete.completers import FilesCompleter | ||
8 | from milc import cli | 6 | from milc import cli |
9 | 7 | ||
10 | from qmk.path import normpath | ||
11 | from qmk.c_parse import c_source_files | ||
12 | |||
13 | c_file_suffixes = ('c', 'h', 'cpp') | ||
14 | core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms') | ||
15 | ignored = ('tmk_core/protocol/usb_hid', 'quantum/template', 'platforms/chibios') | ||
16 | |||
17 | |||
18 | def 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 | |||
30 | def 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 | |||
47 | def 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 | |||
68 | def 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) |
94 | def cformat(cli): | 15 | def 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 | |||
3 | Check out the user's QMK environment and make sure it's ready to compile. | ||
4 | """ | ||
5 | from .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 | """ |
3 | from enum import Enum | 3 | from enum import Enum |
4 | import re | 4 | import re |
@@ -30,7 +30,7 @@ ESSENTIAL_BINARIES = { | |||
30 | } | 30 | } |
31 | 31 | ||
32 | 32 | ||
33 | def parse_gcc_version(version): | 33 | def _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 | ||
43 | def check_arm_gcc_version(): | 43 | def _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 | ||
53 | def check_avr_gcc_version(): | 53 | def _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 | ||
71 | def check_avrdude_version(): | 71 | def _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 | ||
80 | def check_dfu_util_version(): | 80 | def _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 | ||
89 | def check_dfu_programmer_version(): | 89 | def _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 | """ |
3 | from pathlib import Path | 3 | import platform |
4 | import shutil | 4 | import shutil |
5 | from pathlib import Path | ||
5 | 6 | ||
6 | from milc import cli | 7 | from milc import cli |
8 | |||
7 | from qmk.constants import QMK_FIRMWARE | 9 | from qmk.constants import QMK_FIRMWARE |
8 | from qmk.os_helpers import CheckStatus | 10 | from .check import CheckStatus |
9 | 11 | ||
10 | 12 | ||
11 | def _udev_rule(vid, pid=None, *args): | 13 | def _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 | |||
155 | def 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 @@ | |||
1 | import platform | ||
2 | |||
3 | from milc import cli | ||
4 | |||
5 | from .check import CheckStatus | ||
6 | |||
7 | |||
8 | def 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 | ||
8 | from milc import cli | 8 | from milc import cli |
9 | from milc.questions import yesno | 9 | from milc.questions import yesno |
10 | |||
10 | from qmk import submodules | 11 | from qmk import submodules |
11 | from qmk.constants import QMK_FIRMWARE | 12 | from qmk.constants import QMK_FIRMWARE, QMK_FIRMWARE_UPSTREAM |
12 | from qmk.os_helpers import CheckStatus, check_binaries, check_binary_versions, check_submodules, check_git_repo | 13 | from .check import CheckStatus, check_binaries, check_binary_versions, check_submodules |
14 | from qmk.commands import git_check_repo, git_get_branch, git_is_dirty, git_get_remotes, git_check_deviation, in_virtualenv | ||
13 | 15 | ||
14 | 16 | ||
15 | def os_tests(): | 17 | def 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 | ||
31 | def os_test_linux(): | 36 | def 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.') | |
51 | def 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: | |
59 | def 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 @@ | |||
1 | import platform | ||
2 | |||
3 | from milc import cli | ||
4 | |||
5 | from .check import CheckStatus | ||
6 | |||
7 | |||
8 | def 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 | """ |
3 | from milc import cli | 3 | import sys |
4 | from pathlib import Path | ||
4 | 5 | ||
5 | import subprocess | 6 | from 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) |
9 | def fileformat(cli): | 10 | def 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 | """ | ||
3 | from os import path | ||
4 | from shutil import which | ||
5 | from subprocess import CalledProcessError, DEVNULL, Popen, PIPE | ||
6 | |||
7 | from argcomplete.completers import FilesCompleter | ||
8 | from milc import cli | ||
9 | |||
10 | from qmk.path import normpath | ||
11 | from qmk.c_parse import c_source_files | ||
12 | |||
13 | c_file_suffixes = ('c', 'h', 'cpp') | ||
14 | core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms') | ||
15 | ignored = ('tmk_core/protocol/usb_hid', 'quantum/template', 'platforms/chibios') | ||
16 | |||
17 | |||
18 | def 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 | |||
30 | def 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 | |||
47 | def 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 | |||
68 | def 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) | ||
94 | def 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 | |||
8 | from milc import cli | 8 | from milc import cli |
9 | 9 | ||
10 | from qmk.info import info_json | 10 | from qmk.info import info_json |
11 | from qmk.json_schema import json_load, keyboard_validate | 11 | from qmk.json_schema import json_load, validate |
12 | from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder | 12 | from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder |
13 | from qmk.path import normpath | 13 | from 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 | """ | ||
3 | from subprocess import CalledProcessError, DEVNULL | ||
4 | |||
5 | from 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) | ||
10 | def 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 | """ | ||
3 | from subprocess import CalledProcessError | ||
4 | |||
5 | from milc import cli | ||
6 | |||
7 | |||
8 | @cli.subcommand("Ensure text files have the proper line endings.", hidden=True) | ||
9 | def 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 | |||
5 | from dotty_dict import dotty | 5 | from dotty_dict import dotty |
6 | from milc import cli | 6 | from milc import cli |
7 | 7 | ||
8 | from qmk.decorators import automagic_keyboard, automagic_keymap | ||
9 | from qmk.info import info_json | 8 | from qmk.info import info_json |
10 | from qmk.json_schema import json_load | 9 | from qmk.json_schema import json_load, validate |
11 | from qmk.keyboard import keyboard_completer, keyboard_folder | 10 | from qmk.keyboard import keyboard_completer, keyboard_folder |
12 | from qmk.path import is_keyboard, normpath | 11 | from qmk.keymap import locate_keymap |
12 | from qmk.path import normpath | ||
13 | 13 | ||
14 | 14 | ||
15 | def direct_pins(direct_pins): | 15 | def 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 | ||
42 | def pin_array(define, pins): | 42 | def 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 | ||
59 | def matrix_pins(matrix_pins): | 59 | def 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') | 76 | def 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 | ||
82 | def 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 | |||
118 | def 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) | ||
163 | def 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 | """ |
5 | import json | 5 | import json |
6 | 6 | ||
7 | from jsonschema import Draft7Validator, validators | 7 | from argcomplete.completers import FilesCompleter |
8 | from jsonschema import Draft7Validator, RefResolver, validators | ||
8 | from milc import cli | 9 | from milc import cli |
10 | from pathlib import Path | ||
9 | 11 | ||
10 | from qmk.decorators import automagic_keyboard, automagic_keymap | 12 | from qmk.decorators import automagic_keyboard, automagic_keymap |
11 | from qmk.info import info_json | 13 | from qmk.info import info_json |
12 | from qmk.json_encoders import InfoJSONEncoder | 14 | from qmk.json_encoders import InfoJSONEncoder |
13 | from qmk.json_schema import load_jsonschema | 15 | from qmk.json_schema import compile_schema_store |
14 | from qmk.keyboard import keyboard_completer, keyboard_folder | 16 | from qmk.keyboard import keyboard_completer, keyboard_folder |
15 | from qmk.path import is_keyboard | 17 | from qmk.path import is_keyboard, normpath |
16 | 18 | ||
17 | 19 | ||
18 | def pruning_validator(validator_class): | 20 | def pruning_validator(validator_class): |
@@ -34,15 +36,19 @@ def pruning_validator(validator_class): | |||
34 | def strip_info_json(kb_info_json): | 36 | def 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 | """ |
3 | from milc import cli | 3 | from milc import cli |
4 | 4 | ||
5 | from qmk.decorators import automagic_keyboard, automagic_keymap | ||
6 | from qmk.info import info_json | 5 | from qmk.info import info_json |
7 | from qmk.keyboard import keyboard_completer, keyboard_folder | 6 | from qmk.keyboard import keyboard_completer, keyboard_folder |
8 | from qmk.path import normpath | 7 | from 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 | ||
36 | def generate_keyboard_h(cli): | 33 | def 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 | |||
5 | from dotty_dict import dotty | 5 | from dotty_dict import dotty |
6 | from milc import cli | 6 | from milc import cli |
7 | 7 | ||
8 | from qmk.decorators import automagic_keyboard, automagic_keymap | ||
9 | from qmk.info import info_json | 8 | from qmk.info import info_json |
10 | from qmk.json_schema import json_load | 9 | from qmk.json_schema import json_load, validate |
11 | from qmk.keyboard import keyboard_completer, keyboard_folder | 10 | from qmk.keyboard import keyboard_completer, keyboard_folder |
12 | from qmk.path import is_keyboard, normpath | 11 | from qmk.keymap import locate_keymap |
12 | from qmk.path import normpath | ||
13 | 13 | ||
14 | 14 | ||
15 | def process_mapping_rule(kb_info_json, rules_key, info_dict): | 15 | def 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 | ||
46 | def generate_rules_mk(cli): | 45 | def 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 | """ | ||
3 | from milc import cli | ||
4 | |||
5 | from qmk.commands import create_version_h | ||
6 | from 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) | ||
14 | def 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 | ||
108 | def print_text_output(kb_info_json): | 93 | def 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 | ||
112 | def 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 | |||
127 | def print_parsed_rules_mk(keyboard_name): | 130 | def 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 | |||
10 | from milc import cli | 10 | from milc import cli |
11 | 11 | ||
12 | from qmk.constants import QMK_FIRMWARE | 12 | from qmk.constants import QMK_FIRMWARE |
13 | from qmk.commands import _find_make | 13 | from qmk.commands import _find_make, get_make_parallel_args |
14 | import qmk.keyboard | 14 | import qmk.keyboard |
15 | import qmk.keymap | 15 | import 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 | """ |
3 | from datetime import date | ||
4 | import fileinput | ||
5 | from pathlib import Path | ||
6 | import re | ||
7 | import shutil | ||
8 | |||
9 | from qmk.commands import git_get_username | ||
10 | import qmk.path | ||
3 | from milc import cli | 11 | from milc import cli |
12 | from milc.questions import choice, question | ||
13 | |||
14 | KEYBOARD_TYPES = ['avr', 'ps2avrgb'] | ||
15 | |||
16 | |||
17 | def 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') | 25 | def 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') | ||
7 | def new_keyboard(cli): | 36 | def 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 | |||
108 | def 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 | |||
117 | def 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 | |||
134 | def 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 | """ |
3 | from subprocess import CalledProcessError, DEVNULL | 3 | import sys |
4 | from pathlib import Path | ||
4 | 5 | ||
5 | from milc import cli | 6 | from 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) |
10 | def pyformat(cli): | 11 | def 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 | """ |
3 | import json | 3 | import json |
4 | import os | 4 | import os |
5 | import sys | ||
5 | import shutil | 6 | import shutil |
6 | from pathlib import Path | 7 | from pathlib import Path |
7 | from subprocess import DEVNULL | 8 | from subprocess import DEVNULL |
@@ -10,7 +11,7 @@ from time import strftime | |||
10 | from milc import cli | 11 | from milc import cli |
11 | 12 | ||
12 | import qmk.keymap | 13 | import qmk.keymap |
13 | from qmk.constants import KEYBOARD_OUTPUT_PREFIX | 14 | from qmk.constants import QMK_FIRMWARE, KEYBOARD_OUTPUT_PREFIX |
14 | from qmk.json_schema import json_load | 15 | from qmk.json_schema import json_load |
15 | 16 | ||
16 | time_fmt = '%Y-%m-%d-%H:%M:%S' | 17 | time_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 | ||
57 | def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars): | 58 | def 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 | ||
89 | def get_git_version(repo_dir='.', check_dir='.'): | 90 | def 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 | |||
115 | def 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 | ||
108 | def write_version_h(git_version, build_date, chibios_version, chibios_contrib_version): | 133 | def 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 | ||
122 | def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_vars): | 164 | def 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 | |||
260 | def 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 | |||
269 | def 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 | |||
280 | def 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 | |||
292 | def 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 | |||
304 | def 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 | |||
321 | def 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 | |||
329 | def 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. |
7 | QMK_FIRMWARE = Path.cwd() | 7 | QMK_FIRMWARE = Path.cwd() |
8 | 8 | ||
9 | # Upstream repo url | ||
10 | QMK_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. |
10 | MAX_KEYBOARD_SUBFOLDERS = 5 | 13 | MAX_KEYBOARD_SUBFOLDERS = 5 |
11 | 14 | ||
12 | # Supported processor types | 15 | # Supported processor types |
13 | CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'MK66F18', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L433', 'STM32L443' | 16 | CHIBIOS_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' |
14 | LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None | 17 | LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None |
15 | VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85' | 18 | VUSB_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 | ||
10 | from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS | 10 | from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS |
11 | from qmk.c_parse import find_layouts | 11 | from qmk.c_parse import find_layouts |
12 | from qmk.json_schema import deep_update, json_load, keyboard_validate, keyboard_api_validate | 12 | from qmk.json_schema import deep_update, json_load, validate |
13 | from qmk.keyboard import config_h, rules_mk | 13 | from qmk.keyboard import config_h, rules_mk |
14 | from qmk.keymap import list_keymaps | 14 | from qmk.keymap import list_keymaps |
15 | from qmk.makefile import parse_rules_mk_file | 15 | from 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 | ||
152 | def _extract_pins(pins): | 158 | def _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 | ||
158 | def _extract_direct_matrix(info_data, direct_pins): | 164 | def _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 | ||
190 | def _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 | |||
203 | def _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 | |||
254 | def _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 | |||
278 | def _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 | |||
185 | def _extract_matrix_info(info_data, config_c): | 332 | def _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 | ||
344 | def _search_keyboard_h(path): | 514 | def _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 | |||
530 | def _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 | |||
554 | def _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 | ||
364 | def _find_all_layouts(info_data, keyboard): | 575 | def _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 | ||
387 | def _log_error(info_data, message): | 599 | def _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 | """ |
3 | import json | 3 | import json |
4 | from collections.abc import Mapping | 4 | from collections.abc import Mapping |
5 | from functools import lru_cache | ||
5 | from pathlib import Path | 6 | from pathlib import Path |
6 | 7 | ||
7 | import hjson | 8 | import 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) | ||
28 | def load_jsonschema(schema_name): | 30 | def 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 | ||
41 | def keyboard_validate(data): | 44 | @lru_cache(maxsize=0) |
42 | """Validates data against the keyboard jsonschema. | 45 | def 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) | ||
61 | def 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 | ||
50 | def keyboard_api_validate(data): | 70 | def 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 | ||
61 | def deep_update(origdict, newdict): | 78 | def 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 | ||
34 | def test_cformat(): | 34 | def 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 | ||
39 | def test_cformat_all(): | 39 | def 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 | ||
83 | def test_pyformat(): | 83 | def 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 | ||
261 | def 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 | |||
261 | def test_generate_layouts(): | 267 | def 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(): | |||
267 | def test_format_json_keyboard(): | 273 | def 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 | ||
273 | def test_format_json_keymap(): | 279 | def test_format_json_keymap(): |
@@ -279,7 +285,7 @@ def test_format_json_keymap(): | |||
279 | def test_format_json_keyboard_auto(): | 285 | def 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 | ||
285 | def test_format_json_keymap_auto(): | 291 | def test_format_json_keymap_auto(): |