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)76
-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.py133
-rw-r--r--lib/python/qmk/constants.py5
-rw-r--r--lib/python/qmk/info.py57
-rw-r--r--lib/python/qmk/json_schema.py34
-rw-r--r--lib/python/qmk/tests/test_cli_commands.py6
14 files changed, 321 insertions, 100 deletions
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
index 1e1c26671..91d42bb3a 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -50,6 +50,7 @@ subcommands = [
50 'qmk.cli.generate.layouts', 50 'qmk.cli.generate.layouts',
51 'qmk.cli.generate.rgb_breathe_table', 51 'qmk.cli.generate.rgb_breathe_table',
52 'qmk.cli.generate.rules_mk', 52 'qmk.cli.generate.rules_mk',
53 'qmk.cli.generate.version_h',
53 'qmk.cli.hello', 54 'qmk.cli.hello',
54 'qmk.cli.info', 55 'qmk.cli.info',
55 'qmk.cli.json2c', 56 '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..6a31ccdfd 100755
--- a/lib/python/qmk/cli/doctor.py
+++ b/lib/python/qmk/cli/doctor/main.py
@@ -7,9 +7,11 @@ from subprocess import DEVNULL
7 7
8from milc import cli 8from milc import cli
9from milc.questions import yesno 9from milc.questions import yesno
10
10from qmk import submodules 11from qmk import submodules
11from qmk.constants import QMK_FIRMWARE 12from qmk.constants import QMK_FIRMWARE, QMK_FIRMWARE_UPSTREAM
12from qmk.os_helpers import CheckStatus, check_binaries, check_binary_versions, check_submodules, check_git_repo 13from .check import CheckStatus, check_binaries, check_binary_versions, check_submodules
14from qmk.commands import git_check_repo, git_get_branch, git_is_dirty, git_get_remotes, git_check_deviation, in_virtualenv
13 15
14 16
15def os_tests(): 17def os_tests():
@@ -18,51 +20,48 @@ def os_tests():
18 platform_id = platform.platform().lower() 20 platform_id = platform.platform().lower()
19 21
20 if 'darwin' in platform_id or 'macos' in platform_id: 22 if 'darwin' in platform_id or 'macos' in platform_id:
23 from .macos import os_test_macos
21 return os_test_macos() 24 return os_test_macos()
22 elif 'linux' in platform_id: 25 elif 'linux' in platform_id:
26 from .linux import os_test_linux
23 return os_test_linux() 27 return os_test_linux()
24 elif 'windows' in platform_id: 28 elif 'windows' in platform_id:
29 from .windows import os_test_windows
25 return os_test_windows() 30 return os_test_windows()
26 else: 31 else:
27 cli.log.warning('Unsupported OS detected: %s', platform_id) 32 cli.log.warning('Unsupported OS detected: %s', platform_id)
28 return CheckStatus.WARNING 33 return CheckStatus.WARNING
29 34
30 35
31def os_test_linux(): 36def git_tests():
32 """Run the Linux specific tests. 37 """Run Git-related checks
33 """ 38 """
34 # Don't bother with udev on WSL, for now 39 status = CheckStatus.OK
35 if 'microsoft' in platform.uname().release.lower():
36 cli.log.info("Detected {fg_cyan}Linux (WSL){fg_reset}.")
37
38 # https://github.com/microsoft/WSL/issues/4197
39 if QMK_FIRMWARE.as_posix().startswith("/mnt"):
40 cli.log.warning("I/O performance on /mnt may be extremely slow.")
41 return CheckStatus.WARNING
42 40
43 return CheckStatus.OK 41 # Make sure our QMK home is a Git repo
42 git_ok = git_check_repo()
43 if not git_ok:
44 cli.log.warning("{fg_yellow}QMK home does not appear to be a Git repository! (no .git folder)")
45 status = CheckStatus.WARNING
44 else: 46 else:
45 cli.log.info("Detected {fg_cyan}Linux{fg_reset}.") 47 git_branch = git_get_branch()
46 from qmk.os_helpers.linux import check_udev_rules 48 if git_branch:
47 49 cli.log.info('Git branch: %s', git_branch)
48 return check_udev_rules() 50 git_dirty = git_is_dirty()
49 51 if git_dirty:
50 52 cli.log.warning('{fg_yellow}Git has unstashed/uncommitted changes.')
51def os_test_macos(): 53 status = CheckStatus.WARNING
52 """Run the Mac specific tests. 54 git_remotes = git_get_remotes()
53 """ 55 if 'upstream' not in git_remotes.keys() or QMK_FIRMWARE_UPSTREAM not in git_remotes['upstream'].get('url', ''):
54 cli.log.info("Detected {fg_cyan}macOS %s{fg_reset}.", platform.mac_ver()[0]) 56 cli.log.warning('{fg_yellow}The official repository does not seem to be configured as git remote "upstream".')
55 57 status = CheckStatus.WARNING
56 return CheckStatus.OK 58 else:
57 59 git_deviation = git_check_deviation(git_branch)
58 60 if git_branch in ['master', 'develop'] and git_deviation:
59def os_test_windows(): 61 cli.log.warning('{fg_yellow}The local "%s" branch contains commits not found in the upstream branch.', git_branch)
60 """Run the Windows specific tests. 62 status = CheckStatus.WARNING
61 """ 63
62 win32_ver = platform.win32_ver() 64 return status
63 cli.log.info("Detected {fg_cyan}Windows %s (%s){fg_reset}.", win32_ver[0], win32_ver[1])
64
65 return CheckStatus.OK
66 65
67 66
68@cli.argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.') 67@cli.argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.')
@@ -82,12 +81,11 @@ def doctor(cli):
82 81
83 status = os_tests() 82 status = os_tests()
84 83
85 # Make sure our QMK home is a Git repo 84 status = git_tests()
86 git_ok = check_git_repo()
87 85
88 if git_ok == CheckStatus.WARNING: 86 venv = in_virtualenv()
89 cli.log.warning("QMK home does not appear to be a Git repository! (no .git folder)") 87 if venv:
90 status = CheckStatus.WARNING 88 cli.log.info('CLI installed in virtualenv.')
91 89
92 # Make sure the basic CLI tools we need are available and can be executed. 90 # Make sure the basic CLI tools we need are available and can be executed.
93 bin_ok = check_binaries() 91 bin_ok = check_binaries()
diff --git a/lib/python/qmk/cli/doctor/windows.py b/lib/python/qmk/cli/doctor/windows.py
new file mode 100644
index 000000000..381ab36fd
--- /dev/null
+++ b/lib/python/qmk/cli/doctor/windows.py
@@ -0,0 +1,14 @@
1import platform
2
3from milc import cli
4
5from .check import CheckStatus
6
7
8def os_test_windows():
9 """Run the Windows specific tests.
10 """
11 win32_ver = platform.win32_ver()
12 cli.log.info("Detected {fg_cyan}Windows %s (%s){fg_reset}.", win32_ver[0], win32_ver[1])
13
14 return CheckStatus.OK
diff --git a/lib/python/qmk/cli/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..8ff8501bf 100644
--- a/lib/python/qmk/commands.py
+++ b/lib/python/qmk/commands.py
@@ -2,6 +2,7 @@
2""" 2"""
3import json 3import json
4import os 4import os
5import sys
5import shutil 6import shutil
6from pathlib import Path 7from pathlib import Path
7from subprocess import DEVNULL 8from subprocess import DEVNULL
@@ -10,7 +11,7 @@ from time import strftime
10from milc import cli 11from milc import cli
11 12
12import qmk.keymap 13import qmk.keymap
13from qmk.constants import KEYBOARD_OUTPUT_PREFIX 14from qmk.constants import QMK_FIRMWARE, KEYBOARD_OUTPUT_PREFIX
14from qmk.json_schema import json_load 15from qmk.json_schema import json_load
15 16
16time_fmt = '%Y-%m-%d-%H:%M:%S' 17time_fmt = '%Y-%m-%d-%H:%M:%S'
@@ -86,11 +87,17 @@ def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):
86 return create_make_target(':'.join(make_args), parallel, **env_vars) 87 return create_make_target(':'.join(make_args), parallel, **env_vars)
87 88
88 89
89def get_git_version(repo_dir='.', check_dir='.'): 90def get_git_version(current_time, repo_dir='.', check_dir='.'):
90 """Returns the current git version for a repo, or the current time. 91 """Returns the current git version for a repo, or the current time.
91 """ 92 """
92 git_describe_cmd = ['git', 'describe', '--abbrev=6', '--dirty', '--always', '--tags'] 93 git_describe_cmd = ['git', 'describe', '--abbrev=6', '--dirty', '--always', '--tags']
93 94
95 if repo_dir != '.':
96 repo_dir = Path('lib') / repo_dir
97
98 if check_dir != '.':
99 check_dir = repo_dir / check_dir
100
94 if Path(check_dir).exists(): 101 if Path(check_dir).exists():
95 git_describe = cli.run(git_describe_cmd, stdin=DEVNULL, cwd=repo_dir) 102 git_describe = cli.run(git_describe_cmd, stdin=DEVNULL, cwd=repo_dir)
96 103
@@ -100,23 +107,40 @@ 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
104 111
105 return strftime(time_fmt) 112 return current_time
106 113
107 114
108def write_version_h(git_version, build_date, chibios_version, chibios_contrib_version): 115def create_version_h(skip_git=False, skip_all=False):
109 """Generate and write quantum/version.h 116 """Generate version.h contents
110 """ 117 """
111 version_h = [ 118 if skip_all:
112 f'#define QMK_VERSION "{git_version}"', 119 current_time = "1970-01-01-00:00:00"
113 f'#define QMK_BUILDDATE "{build_date}"', 120 else:
114 f'#define CHIBIOS_VERSION "{chibios_version}"', 121 current_time = strftime(time_fmt)
115 f'#define CHIBIOS_CONTRIB_VERSION "{chibios_contrib_version}"', 122
116 ] 123 if skip_git:
124 git_version = "NA"
125 chibios_version = "NA"
126 chibios_contrib_version = "NA"
127 else:
128 git_version = get_git_version(current_time)
129 chibios_version = get_git_version(current_time, "chibios", "os")
130 chibios_contrib_version = get_git_version(current_time, "chibios-contrib", "os")
131
132 version_h_lines = f"""/* This file was automatically generated. Do not edit or copy.
133 */
134
135#pragma once
136
137#define QMK_VERSION "{git_version}"
138#define QMK_BUILDDATE "{current_time}"
139#define CHIBIOS_VERSION "{chibios_version}"
140#define CHIBIOS_CONTRIB_VERSION "{chibios_contrib_version}"
141"""
117 142
118 version_h_file = Path('quantum/version.h') 143 return version_h_lines
119 version_h_file.write_text('\n'.join(version_h))
120 144
121 145
122def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_vars): 146def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_vars):
@@ -149,13 +173,8 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
149 keymap_dir.mkdir(exist_ok=True, parents=True) 173 keymap_dir.mkdir(exist_ok=True, parents=True)
150 keymap_c.write_text(c_text) 174 keymap_c.write_text(c_text)
151 175
152 # Write the version.h file 176 version_h = Path('quantum/version.h')
153 git_version = get_git_version() 177 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 178
160 # Return a command that can be run to make the keymap and flash if given 179 # 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' 180 verbose = 'true' if cli.config.general.verbose else 'false'
@@ -181,10 +200,6 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
181 make_command.append(f'{key}={value}') 200 make_command.append(f'{key}={value}')
182 201
183 make_command.extend([ 202 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"]}', 203 f'KEYBOARD={user_keymap["keyboard"]}',
189 f'KEYMAP={user_keymap["keymap"]}', 204 f'KEYMAP={user_keymap["keymap"]}',
190 f'KEYBOARD_FILESAFE={keyboard_filesafe}', 205 f'KEYBOARD_FILESAFE={keyboard_filesafe}',
@@ -223,3 +238,71 @@ def parse_configurator_json(configurator_file):
223 user_keymap['layout'] = aliases[orig_keyboard]['layouts'][user_keymap['layout']] 238 user_keymap['layout'] = aliases[orig_keyboard]['layouts'][user_keymap['layout']]
224 239
225 return user_keymap 240 return user_keymap
241
242
243def git_check_repo():
244 """Checks that the .git directory exists inside QMK_HOME.
245
246 This is a decent enough indicator that the qmk_firmware directory is a
247 proper Git repository, rather than a .zip download from GitHub.
248 """
249 dot_git_dir = QMK_FIRMWARE / '.git'
250
251 return dot_git_dir.is_dir()
252
253
254def git_get_branch():
255 """Returns the current branch for a repo, or None.
256 """
257 git_branch = cli.run(['git', 'branch', '--show-current'])
258 if not git_branch.returncode != 0 or not git_branch.stdout:
259 # Workaround for Git pre-2.22
260 git_branch = cli.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'])
261
262 if git_branch.returncode == 0:
263 return git_branch.stdout.strip()
264
265
266def git_is_dirty():
267 """Returns 1 if repo is dirty, or 0 if clean
268 """
269 git_diff_staged_cmd = ['git', 'diff', '--quiet']
270 git_diff_unstaged_cmd = [*git_diff_staged_cmd, '--cached']
271
272 unstaged = cli.run(git_diff_staged_cmd)
273 staged = cli.run(git_diff_unstaged_cmd)
274
275 return unstaged.returncode != 0 or staged.returncode != 0
276
277
278def git_get_remotes():
279 """Returns the current remotes for a repo.
280 """
281 remotes = {}
282
283 git_remote_show_cmd = ['git', 'remote', 'show']
284 git_remote_get_cmd = ['git', 'remote', 'get-url']
285
286 git_remote_show = cli.run(git_remote_show_cmd)
287 if git_remote_show.returncode == 0:
288 for name in git_remote_show.stdout.splitlines():
289 git_remote_name = cli.run([*git_remote_get_cmd, name])
290 remotes[name.strip()] = {"url": git_remote_name.stdout.strip()}
291
292 return remotes
293
294
295def git_check_deviation(active_branch):
296 """Return True if branch has custom commits
297 """
298 cli.run(['git', 'fetch', 'upstream', active_branch])
299 deviations = cli.run(['git', '--no-pager', 'log', f'upstream/{active_branch}...{active_branch}'])
300 return bool(deviations.returncode)
301
302
303def in_virtualenv():
304 """Check if running inside a virtualenv.
305 Based on https://stackoverflow.com/a/1883251
306 """
307 active_prefix = getattr(sys, "base_prefix", None) or getattr(sys, "real_prefix", None) or sys.prefix
308 return active_prefix != sys.prefix
diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py
index 49e5e0eb4..1078f4ad5 100644
--- a/lib/python/qmk/constants.py
+++ b/lib/python/qmk/constants.py
@@ -6,11 +6,14 @@ from pathlib import Path
6# The root of the qmk_firmware tree. 6# The root of the qmk_firmware tree.
7QMK_FIRMWARE = Path.cwd() 7QMK_FIRMWARE = Path.cwd()
8 8
9# Upstream repo url
10QMK_FIRMWARE_UPSTREAM = 'qmk/qmk_firmware'
11
9# This is the number of directories under `qmk_firmware/keyboards` that will be traversed. This is currently a limitation of our make system. 12# This is the number of directories under `qmk_firmware/keyboards` that will be traversed. This is currently a limitation of our make system.
10MAX_KEYBOARD_SUBFOLDERS = 5 13MAX_KEYBOARD_SUBFOLDERS = 5
11 14
12# Supported processor types 15# Supported processor types
13CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'MK66F18', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L433', 'STM32L443' 16CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'MK66F18', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L412', 'STM32L422', 'STM32L433', 'STM32L443'
14LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None 17LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None
15VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85' 18VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85'
16 19
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index 47c8bff7a..bcb4d81ef 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
@@ -64,9 +64,12 @@ def info_json(keyboard):
64 info_data = _extract_config_h(info_data) 64 info_data = _extract_config_h(info_data)
65 info_data = _extract_rules_mk(info_data) 65 info_data = _extract_rules_mk(info_data)
66 66
67 # Ensure that we have matrix row and column counts
68 info_data = _matrix_size(info_data)
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])
@@ -90,6 +93,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', {}): 93 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)) 94 _log_error(info_data, 'Claims to support community layout %s but no %s() macro found' % (layout, layout_name))
92 95
96 # Check that the reported matrix size is consistent with the actual matrix size
97 _check_matrix(info_data)
98
93 return info_data 99 return info_data
94 100
95 101
@@ -143,10 +149,7 @@ def _pin_name(pin):
143 elif pin == 'NO_PIN': 149 elif pin == 'NO_PIN':
144 return None 150 return None
145 151
146 elif pin[0] in 'ABCDEFGHIJK' and pin[1].isdigit(): 152 return pin
147 return pin
148
149 raise ValueError(f'Invalid pin: {pin}')
150 153
151 154
152def _extract_pins(pins): 155def _extract_pins(pins):
@@ -341,6 +344,46 @@ def _extract_rules_mk(info_data):
341 return info_data 344 return info_data
342 345
343 346
347def _matrix_size(info_data):
348 """Add info_data['matrix_size'] if it doesn't exist.
349 """
350 if 'matrix_size' not in info_data and 'matrix_pins' in info_data:
351 info_data['matrix_size'] = {}
352
353 if 'direct' in info_data['matrix_pins']:
354 info_data['matrix_size']['cols'] = len(info_data['matrix_pins']['direct'][0])
355 info_data['matrix_size']['rows'] = len(info_data['matrix_pins']['direct'])
356 elif 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']:
357 info_data['matrix_size']['cols'] = len(info_data['matrix_pins']['cols'])
358 info_data['matrix_size']['rows'] = len(info_data['matrix_pins']['rows'])
359
360 return info_data
361
362
363def _check_matrix(info_data):
364 """Check the matrix to ensure that row/column count is consistent.
365 """
366 if 'matrix_pins' in info_data and 'matrix_size' in info_data:
367 actual_col_count = info_data['matrix_size'].get('cols', 0)
368 actual_row_count = info_data['matrix_size'].get('rows', 0)
369 col_count = row_count = 0
370
371 if 'direct' in info_data['matrix_pins']:
372 col_count = len(info_data['matrix_pins']['direct'][0])
373 row_count = len(info_data['matrix_pins']['direct'])
374 elif 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']:
375 col_count = len(info_data['matrix_pins']['cols'])
376 row_count = len(info_data['matrix_pins']['rows'])
377
378 if col_count != actual_col_count and col_count != (actual_col_count / 2):
379 # FIXME: once we can we should detect if split is enabled to do the actual_col_count/2 check.
380 _log_error(info_data, f'MATRIX_COLS is inconsistent with the size of MATRIX_COL_PINS: {col_count} != {actual_col_count}')
381
382 if row_count != actual_row_count and row_count != (actual_row_count / 2):
383 # FIXME: once we can we should detect if split is enabled to do the actual_row_count/2 check.
384 _log_error(info_data, f'MATRIX_ROWS is inconsistent with the size of MATRIX_ROW_PINS: {row_count} != {actual_row_count}')
385
386
344def _merge_layouts(info_data, new_info_data): 387def _merge_layouts(info_data, new_info_data):
345 """Merge new_info_data into info_data in an intelligent way. 388 """Merge new_info_data into info_data in an intelligent way.
346 """ 389 """
@@ -493,7 +536,7 @@ def merge_info_jsons(keyboard, info_data):
493 continue 536 continue
494 537
495 try: 538 try:
496 keyboard_validate(new_info_data) 539 validate(new_info_data, 'qmk.keyboard.v1')
497 except jsonschema.ValidationError as e: 540 except jsonschema.ValidationError as e:
498 json_path = '.'.join([str(p) for p in e.absolute_path]) 541 json_path = '.'.join([str(p) for p in e.absolute_path])
499 cli.log.error('Not including data from file: %s', info_file) 542 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)