aboutsummaryrefslogtreecommitdiff
path: root/lib/python
diff options
context:
space:
mode:
Diffstat (limited to 'lib/python')
-rw-r--r--lib/python/qmk/cli/__init__.py1
-rwxr-xr-xlib/python/qmk/cli/doctor/__init__.py5
-rw-r--r--lib/python/qmk/cli/doctor/check.py (renamed from lib/python/qmk/os_helpers/__init__.py)18
-rw-r--r--lib/python/qmk/cli/doctor/linux.py (renamed from lib/python/qmk/os_helpers/linux/__init__.py)26
-rw-r--r--lib/python/qmk/cli/doctor/macos.py13
-rwxr-xr-xlib/python/qmk/cli/doctor/main.py (renamed from lib/python/qmk/cli/doctor.py)43
-rw-r--r--lib/python/qmk/cli/doctor/windows.py14
-rwxr-xr-xlib/python/qmk/cli/format/json.py5
-rw-r--r--lib/python/qmk/cli/generate/version_h.py28
-rw-r--r--lib/python/qmk/commands.py62
-rw-r--r--lib/python/qmk/constants.py2
-rw-r--r--lib/python/qmk/info.py11
-rw-r--r--lib/python/qmk/json_schema.py34
-rw-r--r--lib/python/qmk/tests/test_cli_commands.py6
14 files changed, 170 insertions, 98 deletions
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
index 7f5e0a1fa..2c3c9c421 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -49,6 +49,7 @@ subcommands = [
49 'qmk.cli.generate.layouts', 49 'qmk.cli.generate.layouts',
50 'qmk.cli.generate.rgb_breathe_table', 50 'qmk.cli.generate.rgb_breathe_table',
51 'qmk.cli.generate.rules_mk', 51 'qmk.cli.generate.rules_mk',
52 'qmk.cli.generate.version_h',
52 'qmk.cli.hello', 53 'qmk.cli.hello',
53 'qmk.cli.info', 54 'qmk.cli.info',
54 'qmk.cli.json2c', 55 'qmk.cli.json2c',
diff --git a/lib/python/qmk/cli/doctor/__init__.py b/lib/python/qmk/cli/doctor/__init__.py
new file mode 100755
index 000000000..272e04202
--- /dev/null
+++ b/lib/python/qmk/cli/doctor/__init__.py
@@ -0,0 +1,5 @@
1"""QMK Doctor
2
3Check out the user's QMK environment and make sure it's ready to compile.
4"""
5from .main import doctor
diff --git a/lib/python/qmk/os_helpers/__init__.py b/lib/python/qmk/cli/doctor/check.py
index 3e98db3c3..a0bbb2816 100644
--- a/lib/python/qmk/os_helpers/__init__.py
+++ b/lib/python/qmk/cli/doctor/check.py
@@ -1,4 +1,4 @@
1"""OS-agnostic helper functions 1"""Check for specific programs.
2""" 2"""
3from enum import Enum 3from enum import Enum
4import re 4import re
@@ -30,7 +30,7 @@ ESSENTIAL_BINARIES = {
30} 30}
31 31
32 32
33def parse_gcc_version(version): 33def _parse_gcc_version(version):
34 m = re.match(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?", version) 34 m = re.match(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?", version)
35 35
36 return { 36 return {
@@ -40,7 +40,7 @@ def parse_gcc_version(version):
40 } 40 }
41 41
42 42
43def check_arm_gcc_version(): 43def _check_arm_gcc_version():
44 """Returns True if the arm-none-eabi-gcc version is not known to cause problems. 44 """Returns True if the arm-none-eabi-gcc version is not known to cause problems.
45 """ 45 """
46 if 'output' in ESSENTIAL_BINARIES['arm-none-eabi-gcc']: 46 if 'output' in ESSENTIAL_BINARIES['arm-none-eabi-gcc']:
@@ -50,7 +50,7 @@ def check_arm_gcc_version():
50 return CheckStatus.OK # Right now all known arm versions are ok 50 return CheckStatus.OK # Right now all known arm versions are ok
51 51
52 52
53def check_avr_gcc_version(): 53def _check_avr_gcc_version():
54 """Returns True if the avr-gcc version is not known to cause problems. 54 """Returns True if the avr-gcc version is not known to cause problems.
55 """ 55 """
56 rc = CheckStatus.ERROR 56 rc = CheckStatus.ERROR
@@ -60,7 +60,7 @@ def check_avr_gcc_version():
60 cli.log.info('Found avr-gcc version %s', version_number) 60 cli.log.info('Found avr-gcc version %s', version_number)
61 rc = CheckStatus.OK 61 rc = CheckStatus.OK
62 62
63 parsed_version = parse_gcc_version(version_number) 63 parsed_version = _parse_gcc_version(version_number)
64 if parsed_version['major'] > 8: 64 if parsed_version['major'] > 8:
65 cli.log.warning('{fg_yellow}We do not recommend avr-gcc newer than 8. Downgrading to 8.x is recommended.') 65 cli.log.warning('{fg_yellow}We do not recommend avr-gcc newer than 8. Downgrading to 8.x is recommended.')
66 rc = CheckStatus.WARNING 66 rc = CheckStatus.WARNING
@@ -68,7 +68,7 @@ def check_avr_gcc_version():
68 return rc 68 return rc
69 69
70 70
71def check_avrdude_version(): 71def _check_avrdude_version():
72 if 'output' in ESSENTIAL_BINARIES['avrdude']: 72 if 'output' in ESSENTIAL_BINARIES['avrdude']:
73 last_line = ESSENTIAL_BINARIES['avrdude']['output'].split('\n')[-2] 73 last_line = ESSENTIAL_BINARIES['avrdude']['output'].split('\n')[-2]
74 version_number = last_line.split()[2][:-1] 74 version_number = last_line.split()[2][:-1]
@@ -77,7 +77,7 @@ def check_avrdude_version():
77 return CheckStatus.OK 77 return CheckStatus.OK
78 78
79 79
80def check_dfu_util_version(): 80def _check_dfu_util_version():
81 if 'output' in ESSENTIAL_BINARIES['dfu-util']: 81 if 'output' in ESSENTIAL_BINARIES['dfu-util']:
82 first_line = ESSENTIAL_BINARIES['dfu-util']['output'].split('\n')[0] 82 first_line = ESSENTIAL_BINARIES['dfu-util']['output'].split('\n')[0]
83 version_number = first_line.split()[1] 83 version_number = first_line.split()[1]
@@ -86,7 +86,7 @@ def check_dfu_util_version():
86 return CheckStatus.OK 86 return CheckStatus.OK
87 87
88 88
89def check_dfu_programmer_version(): 89def _check_dfu_programmer_version():
90 if 'output' in ESSENTIAL_BINARIES['dfu-programmer']: 90 if 'output' in ESSENTIAL_BINARIES['dfu-programmer']:
91 first_line = ESSENTIAL_BINARIES['dfu-programmer']['output'].split('\n')[0] 91 first_line = ESSENTIAL_BINARIES['dfu-programmer']['output'].split('\n')[0]
92 version_number = first_line.split()[1] 92 version_number = first_line.split()[1]
@@ -111,7 +111,7 @@ def check_binary_versions():
111 """Check the versions of ESSENTIAL_BINARIES 111 """Check the versions of ESSENTIAL_BINARIES
112 """ 112 """
113 versions = [] 113 versions = []
114 for check in (check_arm_gcc_version, check_avr_gcc_version, check_avrdude_version, check_dfu_util_version, check_dfu_programmer_version): 114 for check in (_check_arm_gcc_version, _check_avr_gcc_version, _check_avrdude_version, _check_dfu_util_version, _check_dfu_programmer_version):
115 versions.append(check()) 115 versions.append(check())
116 return versions 116 return versions
117 117
diff --git a/lib/python/qmk/os_helpers/linux/__init__.py b/lib/python/qmk/cli/doctor/linux.py
index 008654ab0..c0b77216a 100644
--- a/lib/python/qmk/os_helpers/linux/__init__.py
+++ b/lib/python/qmk/cli/doctor/linux.py
@@ -1,11 +1,13 @@
1"""OS-specific functions for: Linux 1"""OS-specific functions for: Linux
2""" 2"""
3from pathlib import Path 3import platform
4import shutil 4import shutil
5from pathlib import Path
5 6
6from milc import cli 7from milc import cli
8
7from qmk.constants import QMK_FIRMWARE 9from qmk.constants import QMK_FIRMWARE
8from qmk.os_helpers import CheckStatus 10from .check import CheckStatus
9 11
10 12
11def _udev_rule(vid, pid=None, *args): 13def _udev_rule(vid, pid=None, *args):
@@ -138,3 +140,23 @@ def check_modem_manager():
138 """(TODO): Add check for non-systemd systems 140 """(TODO): Add check for non-systemd systems
139 """ 141 """
140 return False 142 return False
143
144
145def os_test_linux():
146 """Run the Linux specific tests.
147 """
148 # Don't bother with udev on WSL, for now
149 if 'microsoft' in platform.uname().release.lower():
150 cli.log.info("Detected {fg_cyan}Linux (WSL){fg_reset}.")
151
152 # https://github.com/microsoft/WSL/issues/4197
153 if QMK_FIRMWARE.as_posix().startswith("/mnt"):
154 cli.log.warning("I/O performance on /mnt may be extremely slow.")
155 return CheckStatus.WARNING
156
157 return CheckStatus.OK
158 else:
159 cli.log.info("Detected {fg_cyan}Linux{fg_reset}.")
160 from .linux import check_udev_rules
161
162 return check_udev_rules()
diff --git a/lib/python/qmk/cli/doctor/macos.py b/lib/python/qmk/cli/doctor/macos.py
new file mode 100644
index 000000000..00fb27285
--- /dev/null
+++ b/lib/python/qmk/cli/doctor/macos.py
@@ -0,0 +1,13 @@
1import platform
2
3from milc import cli
4
5from .check import CheckStatus
6
7
8def os_test_macos():
9 """Run the Mac specific tests.
10 """
11 cli.log.info("Detected {fg_cyan}macOS %s{fg_reset}.", platform.mac_ver()[0])
12
13 return CheckStatus.OK
diff --git a/lib/python/qmk/cli/doctor.py b/lib/python/qmk/cli/doctor/main.py
index 327bc9cb3..5e93fad36 100755
--- a/lib/python/qmk/cli/doctor.py
+++ b/lib/python/qmk/cli/doctor/main.py
@@ -7,9 +7,10 @@ from subprocess import DEVNULL
7 7
8from milc import cli 8from milc import cli
9from milc.questions import yesno 9from milc.questions import yesno
10
10from qmk import submodules 11from qmk import submodules
11from qmk.constants import QMK_FIRMWARE 12from qmk.constants import QMK_FIRMWARE
12from qmk.os_helpers import CheckStatus, check_binaries, check_binary_versions, check_submodules, check_git_repo 13from .check import CheckStatus, check_binaries, check_binary_versions, check_submodules, check_git_repo
13 14
14 15
15def os_tests(): 16def os_tests():
@@ -18,53 +19,19 @@ def os_tests():
18 platform_id = platform.platform().lower() 19 platform_id = platform.platform().lower()
19 20
20 if 'darwin' in platform_id or 'macos' in platform_id: 21 if 'darwin' in platform_id or 'macos' in platform_id:
22 from .macos import os_test_macos
21 return os_test_macos() 23 return os_test_macos()
22 elif 'linux' in platform_id: 24 elif 'linux' in platform_id:
25 from .linux import os_test_linux
23 return os_test_linux() 26 return os_test_linux()
24 elif 'windows' in platform_id: 27 elif 'windows' in platform_id:
28 from .windows import os_test_windows
25 return os_test_windows() 29 return os_test_windows()
26 else: 30 else:
27 cli.log.warning('Unsupported OS detected: %s', platform_id) 31 cli.log.warning('Unsupported OS detected: %s', platform_id)
28 return CheckStatus.WARNING 32 return CheckStatus.WARNING
29 33
30 34
31def os_test_linux():
32 """Run the Linux specific tests.
33 """
34 # Don't bother with udev on WSL, for now
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
43 return CheckStatus.OK
44 else:
45 cli.log.info("Detected {fg_cyan}Linux{fg_reset}.")
46 from qmk.os_helpers.linux import check_udev_rules
47
48 return check_udev_rules()
49
50
51def os_test_macos():
52 """Run the Mac specific tests.
53 """
54 cli.log.info("Detected {fg_cyan}macOS %s{fg_reset}.", platform.mac_ver()[0])
55
56 return CheckStatus.OK
57
58
59def os_test_windows():
60 """Run the Windows specific tests.
61 """
62 win32_ver = platform.win32_ver()
63 cli.log.info("Detected {fg_cyan}Windows %s (%s){fg_reset}.", win32_ver[0], win32_ver[1])
64
65 return CheckStatus.OK
66
67
68@cli.argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.') 35@cli.argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.')
69@cli.argument('-n', '--no', action='store_true', arg_only=True, help='Answer no to all questions.') 36@cli.argument('-n', '--no', action='store_true', arg_only=True, help='Answer no to all questions.')
70@cli.subcommand('Basic QMK environment checks') 37@cli.subcommand('Basic QMK environment checks')
diff --git a/lib/python/qmk/cli/doctor/windows.py b/lib/python/qmk/cli/doctor/windows.py
new file mode 100644
index 000000000..381ab36fd
--- /dev/null
+++ b/lib/python/qmk/cli/doctor/windows.py
@@ -0,0 +1,14 @@
1import platform
2
3from milc import cli
4
5from .check import CheckStatus
6
7
8def os_test_windows():
9 """Run the Windows specific tests.
10 """
11 win32_ver = platform.win32_ver()
12 cli.log.info("Detected {fg_cyan}Windows %s (%s){fg_reset}.", win32_ver[0], win32_ver[1])
13
14 return CheckStatus.OK
diff --git a/lib/python/qmk/cli/format/json.py b/lib/python/qmk/cli/format/json.py
index 1358c70e7..19d504491 100755
--- a/lib/python/qmk/cli/format/json.py
+++ b/lib/python/qmk/cli/format/json.py
@@ -8,7 +8,7 @@ from jsonschema import ValidationError
8from milc import cli 8from milc import cli
9 9
10from qmk.info import info_json 10from qmk.info import info_json
11from qmk.json_schema import json_load, keyboard_validate 11from qmk.json_schema import json_load, validate
12from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder 12from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder
13from qmk.path import normpath 13from qmk.path import normpath
14 14
@@ -23,14 +23,13 @@ def format_json(cli):
23 23
24 if cli.args.format == 'auto': 24 if cli.args.format == 'auto':
25 try: 25 try:
26 keyboard_validate(json_file) 26 validate(json_file, 'qmk.keyboard.v1')
27 json_encoder = InfoJSONEncoder 27 json_encoder = InfoJSONEncoder
28 28
29 except ValidationError as e: 29 except ValidationError as e:
30 cli.log.warning('File %s did not validate as a keyboard:\n\t%s', cli.args.json_file, e) 30 cli.log.warning('File %s did not validate as a keyboard:\n\t%s', cli.args.json_file, e)
31 cli.log.info('Treating %s as a keymap file.', cli.args.json_file) 31 cli.log.info('Treating %s as a keymap file.', cli.args.json_file)
32 json_encoder = KeymapJSONEncoder 32 json_encoder = KeymapJSONEncoder
33
34 elif cli.args.format == 'keyboard': 33 elif cli.args.format == 'keyboard':
35 json_encoder = InfoJSONEncoder 34 json_encoder = InfoJSONEncoder
36 elif cli.args.format == 'keymap': 35 elif cli.args.format == 'keymap':
diff --git a/lib/python/qmk/cli/generate/version_h.py b/lib/python/qmk/cli/generate/version_h.py
new file mode 100644
index 000000000..b8e52588c
--- /dev/null
+++ b/lib/python/qmk/cli/generate/version_h.py
@@ -0,0 +1,28 @@
1"""Used by the make system to generate version.h for use in code.
2"""
3from milc import cli
4
5from qmk.commands import create_version_h
6from qmk.path import normpath
7
8
9@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
10@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
11@cli.argument('--skip-git', arg_only=True, action='store_true', help='Skip Git operations')
12@cli.argument('--skip-all', arg_only=True, action='store_true', help='Use placeholder values for all defines (implies --skip-git)')
13@cli.subcommand('Used by the make system to generate version.h for use in code', hidden=True)
14def generate_version_h(cli):
15 """Generates the version.h file.
16 """
17 if cli.args.skip_all:
18 cli.args.skip_git = True
19
20 version_h = create_version_h(cli.args.skip_git, cli.args.skip_all)
21
22 if cli.args.output:
23 cli.args.output.write_text(version_h)
24
25 if not cli.args.quiet:
26 cli.log.info('Wrote version.h to %s.', cli.args.output)
27 else:
28 print(version_h)
diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py
index 3a35c1103..104c87545 100644
--- a/lib/python/qmk/commands.py
+++ b/lib/python/qmk/commands.py
@@ -86,11 +86,17 @@ def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):
86 return create_make_target(':'.join(make_args), parallel, **env_vars) 86 return create_make_target(':'.join(make_args), parallel, **env_vars)
87 87
88 88
89def get_git_version(repo_dir='.', check_dir='.'): 89def get_git_version(current_time, repo_dir='.', check_dir='.'):
90 """Returns the current git version for a repo, or the current time. 90 """Returns the current git version for a repo, or the current time.
91 """ 91 """
92 git_describe_cmd = ['git', 'describe', '--abbrev=6', '--dirty', '--always', '--tags'] 92 git_describe_cmd = ['git', 'describe', '--abbrev=6', '--dirty', '--always', '--tags']
93 93
94 if repo_dir != '.':
95 repo_dir = Path('lib') / repo_dir
96
97 if check_dir != '.':
98 check_dir = repo_dir / check_dir
99
94 if Path(check_dir).exists(): 100 if Path(check_dir).exists():
95 git_describe = cli.run(git_describe_cmd, stdin=DEVNULL, cwd=repo_dir) 101 git_describe = cli.run(git_describe_cmd, stdin=DEVNULL, cwd=repo_dir)
96 102
@@ -100,23 +106,40 @@ def get_git_version(repo_dir='.', check_dir='.'):
100 else: 106 else:
101 cli.log.warn(f'"{" ".join(git_describe_cmd)}" returned error code {git_describe.returncode}') 107 cli.log.warn(f'"{" ".join(git_describe_cmd)}" returned error code {git_describe.returncode}')
102 print(git_describe.stderr) 108 print(git_describe.stderr)
103 return strftime(time_fmt) 109 return current_time
104 110
105 return strftime(time_fmt) 111 return current_time
106 112
107 113
108def write_version_h(git_version, build_date, chibios_version, chibios_contrib_version): 114def create_version_h(skip_git=False, skip_all=False):
109 """Generate and write quantum/version.h 115 """Generate version.h contents
110 """ 116 """
111 version_h = [ 117 if skip_all:
112 f'#define QMK_VERSION "{git_version}"', 118 current_time = "1970-01-01-00:00:00"
113 f'#define QMK_BUILDDATE "{build_date}"', 119 else:
114 f'#define CHIBIOS_VERSION "{chibios_version}"', 120 current_time = strftime(time_fmt)
115 f'#define CHIBIOS_CONTRIB_VERSION "{chibios_contrib_version}"', 121
116 ] 122 if skip_git:
123 git_version = "NA"
124 chibios_version = "NA"
125 chibios_contrib_version = "NA"
126 else:
127 git_version = get_git_version(current_time)
128 chibios_version = get_git_version(current_time, "chibios", "os")
129 chibios_contrib_version = get_git_version(current_time, "chibios-contrib", "os")
130
131 version_h_lines = f"""/* This file was automatically generated. Do not edit or copy.
132 */
133
134#pragma once
135
136#define QMK_VERSION "{git_version}"
137#define QMK_BUILDDATE "{current_time}"
138#define CHIBIOS_VERSION "{chibios_version}"
139#define CHIBIOS_CONTRIB_VERSION "{chibios_contrib_version}"
140"""
117 141
118 version_h_file = Path('quantum/version.h') 142 return version_h_lines
119 version_h_file.write_text('\n'.join(version_h))
120 143
121 144
122def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_vars): 145def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_vars):
@@ -149,13 +172,8 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
149 keymap_dir.mkdir(exist_ok=True, parents=True) 172 keymap_dir.mkdir(exist_ok=True, parents=True)
150 keymap_c.write_text(c_text) 173 keymap_c.write_text(c_text)
151 174
152 # Write the version.h file 175 version_h = Path('quantum/version.h')
153 git_version = get_git_version() 176 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 177
160 # Return a command that can be run to make the keymap and flash if given 178 # 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' 179 verbose = 'true' if cli.config.general.verbose else 'false'
@@ -181,10 +199,6 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
181 make_command.append(f'{key}={value}') 199 make_command.append(f'{key}={value}')
182 200
183 make_command.extend([ 201 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"]}', 202 f'KEYBOARD={user_keymap["keyboard"]}',
189 f'KEYMAP={user_keymap["keymap"]}', 203 f'KEYMAP={user_keymap["keymap"]}',
190 f'KEYBOARD_FILESAFE={keyboard_filesafe}', 204 f'KEYBOARD_FILESAFE={keyboard_filesafe}',
diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py
index 49e5e0eb4..be5ce5d4e 100644
--- a/lib/python/qmk/constants.py
+++ b/lib/python/qmk/constants.py
@@ -10,7 +10,7 @@ QMK_FIRMWARE = Path.cwd()
10MAX_KEYBOARD_SUBFOLDERS = 5 10MAX_KEYBOARD_SUBFOLDERS = 5
11 11
12# Supported processor types 12# Supported processor types
13CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'MK66F18', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L433', 'STM32L443' 13CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'MK66F18', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L412', 'STM32L422', 'STM32L433', 'STM32L443'
14LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None 14LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None
15VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85' 15VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85'
16 16
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index 47c8bff7a..5525f0fe6 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -9,7 +9,7 @@ from milc import cli
9 9
10from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS 10from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS
11from qmk.c_parse import find_layouts 11from qmk.c_parse import find_layouts
12from qmk.json_schema import deep_update, json_load, keyboard_validate, keyboard_api_validate 12from qmk.json_schema import deep_update, json_load, validate
13from qmk.keyboard import config_h, rules_mk 13from qmk.keyboard import config_h, rules_mk
14from qmk.keymap import list_keymaps 14from qmk.keymap import list_keymaps
15from qmk.makefile import parse_rules_mk_file 15from qmk.makefile import parse_rules_mk_file
@@ -66,7 +66,7 @@ def info_json(keyboard):
66 66
67 # Validate against the jsonschema 67 # Validate against the jsonschema
68 try: 68 try:
69 keyboard_api_validate(info_data) 69 validate(info_data, 'qmk.api.keyboard.v1')
70 70
71 except jsonschema.ValidationError as e: 71 except jsonschema.ValidationError as e:
72 json_path = '.'.join([str(p) for p in e.absolute_path]) 72 json_path = '.'.join([str(p) for p in e.absolute_path])
@@ -143,10 +143,7 @@ def _pin_name(pin):
143 elif pin == 'NO_PIN': 143 elif pin == 'NO_PIN':
144 return None 144 return None
145 145
146 elif pin[0] in 'ABCDEFGHIJK' and pin[1].isdigit(): 146 return pin
147 return pin
148
149 raise ValueError(f'Invalid pin: {pin}')
150 147
151 148
152def _extract_pins(pins): 149def _extract_pins(pins):
@@ -493,7 +490,7 @@ def merge_info_jsons(keyboard, info_data):
493 continue 490 continue
494 491
495 try: 492 try:
496 keyboard_validate(new_info_data) 493 validate(new_info_data, 'qmk.keyboard.v1')
497 except jsonschema.ValidationError as e: 494 except jsonschema.ValidationError as e:
498 json_path = '.'.join([str(p) for p in e.absolute_path]) 495 json_path = '.'.join([str(p) for p in e.absolute_path])
499 cli.log.error('Not including data from file: %s', info_file) 496 cli.log.error('Not including data from file: %s', info_file)
diff --git a/lib/python/qmk/json_schema.py b/lib/python/qmk/json_schema.py
index 077dfcaa9..3e5663a29 100644
--- a/lib/python/qmk/json_schema.py
+++ b/lib/python/qmk/json_schema.py
@@ -24,9 +24,10 @@ def json_load(json_file):
24 24
25def load_jsonschema(schema_name): 25def load_jsonschema(schema_name):
26 """Read a jsonschema file from disk. 26 """Read a jsonschema file from disk.
27
28 FIXME(skullydazed/anyone): Refactor to make this a public function.
29 """ 27 """
28 if Path(schema_name).exists():
29 return json_load(schema_name)
30
30 schema_path = Path(f'data/schemas/{schema_name}.jsonschema') 31 schema_path = Path(f'data/schemas/{schema_name}.jsonschema')
31 32
32 if not schema_path.exists(): 33 if not schema_path.exists():
@@ -35,28 +36,33 @@ def load_jsonschema(schema_name):
35 return json_load(schema_path) 36 return json_load(schema_path)
36 37
37 38
38def keyboard_validate(data): 39def create_validator(schema):
39 """Validates data against the keyboard jsonschema. 40 """Creates a validator for the given schema id.
40 """ 41 """
41 schema = load_jsonschema('keyboard') 42 schema_store = {}
42 validator = jsonschema.Draft7Validator(schema).validate
43 43
44 return validator(data) 44 for schema_file in Path('data/schemas').glob('*.jsonschema'):
45 schema_data = load_jsonschema(schema_file)
46 if not isinstance(schema_data, dict):
47 cli.log.debug('Skipping schema file %s', schema_file)
48 continue
49 schema_store[schema_data['$id']] = schema_data
50
51 resolver = jsonschema.RefResolver.from_schema(schema_store['qmk.keyboard.v1'], store=schema_store)
52
53 return jsonschema.Draft7Validator(schema_store[schema], resolver=resolver).validate
45 54
46 55
47def keyboard_api_validate(data): 56def validate(data, schema):
48 """Validates data against the api_keyboard jsonschema. 57 """Validates data against a schema.
49 """ 58 """
50 base = load_jsonschema('keyboard') 59 validator = create_validator(schema)
51 relative = load_jsonschema('api_keyboard')
52 resolver = jsonschema.RefResolver.from_schema(base)
53 validator = jsonschema.Draft7Validator(relative, resolver=resolver).validate
54 60
55 return validator(data) 61 return validator(data)
56 62
57 63
58def deep_update(origdict, newdict): 64def deep_update(origdict, newdict):
59 """Update a dictionary in place, recursing to do a deep copy. 65 """Update a dictionary in place, recursing to do a depth-first deep copy.
60 """ 66 """
61 for key, value in newdict.items(): 67 for key, value in newdict.items():
62 if isinstance(value, Mapping): 68 if isinstance(value, Mapping):
diff --git a/lib/python/qmk/tests/test_cli_commands.py b/lib/python/qmk/tests/test_cli_commands.py
index afdbc8142..b341e1c91 100644
--- a/lib/python/qmk/tests/test_cli_commands.py
+++ b/lib/python/qmk/tests/test_cli_commands.py
@@ -258,6 +258,12 @@ def test_generate_rules_mk():
258 assert 'MCU ?= atmega32u4' in result.stdout 258 assert 'MCU ?= atmega32u4' in result.stdout
259 259
260 260
261def test_generate_version_h():
262 result = check_subcommand('generate-version-h')
263 check_returncode(result)
264 assert '#define QMK_VERSION' in result.stdout
265
266
261def test_generate_layouts(): 267def test_generate_layouts():
262 result = check_subcommand('generate-layouts', '-kb', 'handwired/pytest/basic') 268 result = check_subcommand('generate-layouts', '-kb', 'handwired/pytest/basic')
263 check_returncode(result) 269 check_returncode(result)