diff options
Diffstat (limited to 'lib/python')
-rw-r--r-- | lib/python/qmk/cli/__init__.py | 1 | ||||
-rwxr-xr-x | lib/python/qmk/cli/doctor/__init__.py | 5 | ||||
-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.py | 13 | ||||
-rwxr-xr-x | lib/python/qmk/cli/doctor/main.py (renamed from lib/python/qmk/cli/doctor.py) | 76 | ||||
-rw-r--r-- | lib/python/qmk/cli/doctor/windows.py | 14 | ||||
-rwxr-xr-x | lib/python/qmk/cli/format/json.py | 5 | ||||
-rw-r--r-- | lib/python/qmk/cli/generate/version_h.py | 28 | ||||
-rw-r--r-- | lib/python/qmk/commands.py | 133 | ||||
-rw-r--r-- | lib/python/qmk/constants.py | 5 | ||||
-rw-r--r-- | lib/python/qmk/info.py | 57 | ||||
-rw-r--r-- | lib/python/qmk/json_schema.py | 34 | ||||
-rw-r--r-- | lib/python/qmk/tests/test_cli_commands.py | 6 |
14 files changed, 321 insertions, 100 deletions
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index 1e1c266710..91d42bb3a2 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py @@ -50,6 +50,7 @@ subcommands = [ 'qmk.cli.generate.layouts', 'qmk.cli.generate.rgb_breathe_table', 'qmk.cli.generate.rules_mk', + 'qmk.cli.generate.version_h', 'qmk.cli.hello', 'qmk.cli.info', '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 0000000000..272e042023 --- /dev/null +++ b/lib/python/qmk/cli/doctor/__init__.py @@ -0,0 +1,5 @@ +"""QMK Doctor + +Check out the user's QMK environment and make sure it's ready to compile. +""" +from .main import doctor diff --git a/lib/python/qmk/os_helpers/__init__.py b/lib/python/qmk/cli/doctor/check.py index 3e98db3c32..a0bbb28168 100644 --- a/lib/python/qmk/os_helpers/__init__.py +++ b/lib/python/qmk/cli/doctor/check.py @@ -1,4 +1,4 @@ -"""OS-agnostic helper functions +"""Check for specific programs. """ from enum import Enum import re @@ -30,7 +30,7 @@ ESSENTIAL_BINARIES = { } -def parse_gcc_version(version): +def _parse_gcc_version(version): m = re.match(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?", version) return { @@ -40,7 +40,7 @@ def parse_gcc_version(version): } -def check_arm_gcc_version(): +def _check_arm_gcc_version(): """Returns True if the arm-none-eabi-gcc version is not known to cause problems. """ if 'output' in ESSENTIAL_BINARIES['arm-none-eabi-gcc']: @@ -50,7 +50,7 @@ def check_arm_gcc_version(): return CheckStatus.OK # Right now all known arm versions are ok -def check_avr_gcc_version(): +def _check_avr_gcc_version(): """Returns True if the avr-gcc version is not known to cause problems. """ rc = CheckStatus.ERROR @@ -60,7 +60,7 @@ def check_avr_gcc_version(): cli.log.info('Found avr-gcc version %s', version_number) rc = CheckStatus.OK - parsed_version = parse_gcc_version(version_number) + parsed_version = _parse_gcc_version(version_number) if parsed_version['major'] > 8: cli.log.warning('{fg_yellow}We do not recommend avr-gcc newer than 8. Downgrading to 8.x is recommended.') rc = CheckStatus.WARNING @@ -68,7 +68,7 @@ def check_avr_gcc_version(): return rc -def check_avrdude_version(): +def _check_avrdude_version(): if 'output' in ESSENTIAL_BINARIES['avrdude']: last_line = ESSENTIAL_BINARIES['avrdude']['output'].split('\n')[-2] version_number = last_line.split()[2][:-1] @@ -77,7 +77,7 @@ def check_avrdude_version(): return CheckStatus.OK -def check_dfu_util_version(): +def _check_dfu_util_version(): if 'output' in ESSENTIAL_BINARIES['dfu-util']: first_line = ESSENTIAL_BINARIES['dfu-util']['output'].split('\n')[0] version_number = first_line.split()[1] @@ -86,7 +86,7 @@ def check_dfu_util_version(): return CheckStatus.OK -def check_dfu_programmer_version(): +def _check_dfu_programmer_version(): if 'output' in ESSENTIAL_BINARIES['dfu-programmer']: first_line = ESSENTIAL_BINARIES['dfu-programmer']['output'].split('\n')[0] version_number = first_line.split()[1] @@ -111,7 +111,7 @@ def check_binary_versions(): """Check the versions of ESSENTIAL_BINARIES """ versions = [] - for check in (check_arm_gcc_version, check_avr_gcc_version, check_avrdude_version, check_dfu_util_version, check_dfu_programmer_version): + for check in (_check_arm_gcc_version, _check_avr_gcc_version, _check_avrdude_version, _check_dfu_util_version, _check_dfu_programmer_version): versions.append(check()) return versions diff --git a/lib/python/qmk/os_helpers/linux/__init__.py b/lib/python/qmk/cli/doctor/linux.py index 008654ab0f..c0b77216a1 100644 --- a/lib/python/qmk/os_helpers/linux/__init__.py +++ b/lib/python/qmk/cli/doctor/linux.py @@ -1,11 +1,13 @@ """OS-specific functions for: Linux """ -from pathlib import Path +import platform import shutil +from pathlib import Path from milc import cli + from qmk.constants import QMK_FIRMWARE -from qmk.os_helpers import CheckStatus +from .check import CheckStatus def _udev_rule(vid, pid=None, *args): @@ -138,3 +140,23 @@ def check_modem_manager(): """(TODO): Add check for non-systemd systems """ return False + + +def os_test_linux(): + """Run the Linux specific tests. + """ + # Don't bother with udev on WSL, for now + if 'microsoft' in platform.uname().release.lower(): + cli.log.info("Detected {fg_cyan}Linux (WSL){fg_reset}.") + + # https://github.com/microsoft/WSL/issues/4197 + if QMK_FIRMWARE.as_posix().startswith("/mnt"): + cli.log.warning("I/O performance on /mnt may be extremely slow.") + return CheckStatus.WARNING + + return CheckStatus.OK + else: + cli.log.info("Detected {fg_cyan}Linux{fg_reset}.") + from .linux import check_udev_rules + + 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 0000000000..00fb272858 --- /dev/null +++ b/lib/python/qmk/cli/doctor/macos.py @@ -0,0 +1,13 @@ +import platform + +from milc import cli + +from .check import CheckStatus + + +def os_test_macos(): + """Run the Mac specific tests. + """ + cli.log.info("Detected {fg_cyan}macOS %s{fg_reset}.", platform.mac_ver()[0]) + + return CheckStatus.OK diff --git a/lib/python/qmk/cli/doctor.py b/lib/python/qmk/cli/doctor/main.py index 327bc9cb30..6a31ccdfdd 100755 --- a/lib/python/qmk/cli/doctor.py +++ b/lib/python/qmk/cli/doctor/main.py @@ -7,9 +7,11 @@ from subprocess import DEVNULL from milc import cli from milc.questions import yesno + from qmk import submodules -from qmk.constants import QMK_FIRMWARE -from qmk.os_helpers import CheckStatus, check_binaries, check_binary_versions, check_submodules, check_git_repo +from qmk.constants import QMK_FIRMWARE, QMK_FIRMWARE_UPSTREAM +from .check import CheckStatus, check_binaries, check_binary_versions, check_submodules +from qmk.commands import git_check_repo, git_get_branch, git_is_dirty, git_get_remotes, git_check_deviation, in_virtualenv def os_tests(): @@ -18,51 +20,48 @@ def os_tests(): platform_id = platform.platform().lower() if 'darwin' in platform_id or 'macos' in platform_id: + from .macos import os_test_macos return os_test_macos() elif 'linux' in platform_id: + from .linux import os_test_linux return os_test_linux() elif 'windows' in platform_id: + from .windows import os_test_windows return os_test_windows() else: cli.log.warning('Unsupported OS detected: %s', platform_id) return CheckStatus.WARNING -def os_test_linux(): - """Run the Linux specific tests. +def git_tests(): + """Run Git-related checks """ - # Don't bother with udev on WSL, for now - if 'microsoft' in platform.uname().release.lower(): - cli.log.info("Detected {fg_cyan}Linux (WSL){fg_reset}.") - - # https://github.com/microsoft/WSL/issues/4197 - if QMK_FIRMWARE.as_posix().startswith("/mnt"): - cli.log.warning("I/O performance on /mnt may be extremely slow.") - return CheckStatus.WARNING + status = CheckStatus.OK - return CheckStatus.OK + # Make sure our QMK home is a Git repo + git_ok = git_check_repo() + if not git_ok: + cli.log.warning("{fg_yellow}QMK home does not appear to be a Git repository! (no .git folder)") + status = CheckStatus.WARNING else: - cli.log.info("Detected {fg_cyan}Linux{fg_reset}.") - from qmk.os_helpers.linux import check_udev_rules - - return check_udev_rules() - - -def os_test_macos(): - """Run the Mac specific tests. - """ - cli.log.info("Detected {fg_cyan}macOS %s{fg_reset}.", platform.mac_ver()[0]) - - return CheckStatus.OK - - -def os_test_windows(): - """Run the Windows specific tests. - """ - win32_ver = platform.win32_ver() - cli.log.info("Detected {fg_cyan}Windows %s (%s){fg_reset}.", win32_ver[0], win32_ver[1]) - - return CheckStatus.OK + git_branch = git_get_branch() + if git_branch: + cli.log.info('Git branch: %s', git_branch) + git_dirty = git_is_dirty() + if git_dirty: + cli.log.warning('{fg_yellow}Git has unstashed/uncommitted changes.') + status = CheckStatus.WARNING + git_remotes = git_get_remotes() + if 'upstream' not in git_remotes.keys() or QMK_FIRMWARE_UPSTREAM not in git_remotes['upstream'].get('url', ''): + cli.log.warning('{fg_yellow}The official repository does not seem to be configured as git remote "upstream".') + status = CheckStatus.WARNING + else: + git_deviation = git_check_deviation(git_branch) + if git_branch in ['master', 'develop'] and git_deviation: + cli.log.warning('{fg_yellow}The local "%s" branch contains commits not found in the upstream branch.', git_branch) + status = CheckStatus.WARNING + + return status @cli.argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.') @@ -82,12 +81,11 @@ def doctor(cli): status = os_tests() - # Make sure our QMK home is a Git repo - git_ok = check_git_repo() + status = git_tests() - if git_ok == CheckStatus.WARNING: - cli.log.warning("QMK home does not appear to be a Git repository! (no .git folder)") - status = CheckStatus.WARNING + venv = in_virtualenv() + if venv: + cli.log.info('CLI installed in virtualenv.') # Make sure the basic CLI tools we need are available and can be executed. 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 0000000000..381ab36fde --- /dev/null +++ b/lib/python/qmk/cli/doctor/windows.py @@ -0,0 +1,14 @@ +import platform + +from milc import cli + +from .check import CheckStatus + + +def os_test_windows(): + """Run the Windows specific tests. + """ + win32_ver = platform.win32_ver() + cli.log.info("Detected {fg_cyan}Windows %s (%s){fg_reset}.", win32_ver[0], win32_ver[1]) + + return CheckStatus.OK diff --git a/lib/python/qmk/cli/format/json.py b/lib/python/qmk/cli/format/json.py index 1358c70e7a..19d504491f 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 from milc import cli from qmk.info import info_json -from qmk.json_schema import json_load, keyboard_validate +from qmk.json_schema import json_load, validate from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder from qmk.path import normpath @@ -23,14 +23,13 @@ def format_json(cli): if cli.args.format == 'auto': try: - keyboard_validate(json_file) + validate(json_file, 'qmk.keyboard.v1') json_encoder = InfoJSONEncoder except ValidationError as e: cli.log.warning('File %s did not validate as a keyboard:\n\t%s', cli.args.json_file, e) cli.log.info('Treating %s as a keymap file.', cli.args.json_file) json_encoder = KeymapJSONEncoder - elif cli.args.format == 'keyboard': json_encoder = InfoJSONEncoder 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 0000000000..b8e52588c4 --- /dev/null +++ b/lib/python/qmk/cli/generate/version_h.py @@ -0,0 +1,28 @@ +"""Used by the make system to generate version.h for use in code. +""" +from milc import cli + +from qmk.commands import create_version_h +from qmk.path import normpath + + +@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to') +@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") +@cli.argument('--skip-git', arg_only=True, action='store_true', help='Skip Git operations') +@cli.argument('--skip-all', arg_only=True, action='store_true', help='Use placeholder values for all defines (implies --skip-git)') +@cli.subcommand('Used by the make system to generate version.h for use in code', hidden=True) +def generate_version_h(cli): + """Generates the version.h file. + """ + if cli.args.skip_all: + cli.args.skip_git = True + + version_h = create_version_h(cli.args.skip_git, cli.args.skip_all) + + if cli.args.output: + cli.args.output.write_text(version_h) + + if not cli.args.quiet: + cli.log.info('Wrote version.h to %s.', cli.args.output) + else: + print(version_h) diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py index 3a35c11031..8ff8501bf6 100644 --- a/lib/python/qmk/commands.py +++ b/lib/python/qmk/commands.py @@ -2,6 +2,7 @@ """ import json import os +import sys import shutil from pathlib import Path from subprocess import DEVNULL @@ -10,7 +11,7 @@ from time import strftime from milc import cli import qmk.keymap -from qmk.constants import KEYBOARD_OUTPUT_PREFIX +from qmk.constants import QMK_FIRMWARE, KEYBOARD_OUTPUT_PREFIX from qmk.json_schema import json_load time_fmt = '%Y-%m-%d-%H:%M:%S' @@ -86,11 +87,17 @@ def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars): return create_make_target(':'.join(make_args), parallel, **env_vars) -def get_git_version(repo_dir='.', check_dir='.'): +def get_git_version(current_time, repo_dir='.', check_dir='.'): """Returns the current git version for a repo, or the current time. """ git_describe_cmd = ['git', 'describe', '--abbrev=6', '--dirty', '--always', '--tags'] + if repo_dir != '.': + repo_dir = Path('lib') / repo_dir + + if check_dir != '.': + check_dir = repo_dir / check_dir + if Path(check_dir).exists(): git_describe = cli.run(git_describe_cmd, stdin=DEVNULL, cwd=repo_dir) @@ -100,23 +107,40 @@ def get_git_version(repo_dir='.', check_dir='.'): else: cli.log.warn(f'"{" ".join(git_describe_cmd)}" returned error code {git_describe.returncode}') print(git_describe.stderr) - return strftime(time_fmt) + return current_time - return strftime(time_fmt) + return current_time -def write_version_h(git_version, build_date, chibios_version, chibios_contrib_version): - """Generate and write quantum/version.h +def create_version_h(skip_git=False, skip_all=False): + """Generate version.h contents """ - version_h = [ - f'#define QMK_VERSION "{git_version}"', - f'#define QMK_BUILDDATE "{build_date}"', - f'#define CHIBIOS_VERSION "{chibios_version}"', - f'#define CHIBIOS_CONTRIB_VERSION "{chibios_contrib_version}"', - ] + if skip_all: + current_time = "1970-01-01-00:00:00" + else: + current_time = strftime(time_fmt) + + if skip_git: + git_version = "NA" + chibios_version = "NA" + chibios_contrib_version = "NA" + else: + git_version = get_git_version(current_time) + chibios_version = get_git_version(current_time, "chibios", "os") + chibios_contrib_version = get_git_version(current_time, "chibios-contrib", "os") + + version_h_lines = f"""/* This file was automatically generated. Do not edit or copy. + */ + +#pragma once + +#define QMK_VERSION "{git_version}" +#define QMK_BUILDDATE "{current_time}" +#define CHIBIOS_VERSION "{chibios_version}" +#define CHIBIOS_CONTRIB_VERSION "{chibios_contrib_version}" +""" - version_h_file = Path('quantum/version.h') - version_h_file.write_text('\n'.join(version_h)) + return version_h_lines def 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 keymap_dir.mkdir(exist_ok=True, parents=True) keymap_c.write_text(c_text) - # Write the version.h file - git_version = get_git_version() - build_date = strftime('%Y-%m-%d-%H:%M:%S') - chibios_version = get_git_version("lib/chibios", "lib/chibios/os") - chibios_contrib_version = get_git_version("lib/chibios-contrib", "lib/chibios-contrib/os") - - write_version_h(git_version, build_date, chibios_version, chibios_contrib_version) + version_h = Path('quantum/version.h') + version_h.write_text(create_version_h()) # Return a command that can be run to make the keymap and flash if given 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 make_command.append(f'{key}={value}') make_command.extend([ - f'GIT_VERSION={git_version}', - f'BUILD_DATE={build_date}', - f'CHIBIOS_VERSION={chibios_version}', - f'CHIBIOS_CONTRIB_VERSION={chibios_contrib_version}', f'KEYBOARD={user_keymap["keyboard"]}', f'KEYMAP={user_keymap["keymap"]}', f'KEYBOARD_FILESAFE={keyboard_filesafe}', @@ -223,3 +238,71 @@ def parse_configurator_json(configurator_file): user_keymap['layout'] = aliases[orig_keyboard]['layouts'][user_keymap['layout']] return user_keymap + + +def git_check_repo(): + """Checks that the .git directory exists inside QMK_HOME. + + This is a decent enough indicator that the qmk_firmware directory is a + proper Git repository, rather than a .zip download from GitHub. + """ + dot_git_dir = QMK_FIRMWARE / '.git' + + return dot_git_dir.is_dir() + + +def git_get_branch(): + """Returns the current branch for a repo, or None. + """ + git_branch = cli.run(['git', 'branch', '--show-current']) + if not git_branch.returncode != 0 or not git_branch.stdout: + # Workaround for Git pre-2.22 + git_branch = cli.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD']) + + if git_branch.returncode == 0: + return git_branch.stdout.strip() + + +def git_is_dirty(): + """Returns 1 if repo is dirty, or 0 if clean + """ + git_diff_staged_cmd = ['git', 'diff', '--quiet'] + git_diff_unstaged_cmd = [*git_diff_staged_cmd, '--cached'] + + unstaged = cli.run(git_diff_staged_cmd) + staged = cli.run(git_diff_unstaged_cmd) + + return unstaged.returncode != 0 or staged.returncode != 0 + + +def git_get_remotes(): + """Returns the current remotes for a repo. + """ + remotes = {} + + git_remote_show_cmd = ['git', 'remote', 'show'] + git_remote_get_cmd = ['git', 'remote', 'get-url'] + + git_remote_show = cli.run(git_remote_show_cmd) + if git_remote_show.returncode == 0: + for name in git_remote_show.stdout.splitlines(): + git_remote_name = cli.run([*git_remote_get_cmd, name]) + remotes[name.strip()] = {"url": git_remote_name.stdout.strip()} + + return remotes + + +def git_check_deviation(active_branch): + """Return True if branch has custom commits + """ + cli.run(['git', 'fetch', 'upstream', active_branch]) + deviations = cli.run(['git', '--no-pager', 'log', f'upstream/{active_branch}...{active_branch}']) + return bool(deviations.returncode) + + +def in_virtualenv(): + """Check if running inside a virtualenv. + Based on https://stackoverflow.com/a/1883251 + """ + active_prefix = getattr(sys, "base_prefix", None) or getattr(sys, "real_prefix", None) or sys.prefix + return active_prefix != sys.prefix diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py index 49e5e0eb42..1078f4ad5e 100644 --- a/lib/python/qmk/constants.py +++ b/lib/python/qmk/constants.py @@ -6,11 +6,14 @@ from pathlib import Path # The root of the qmk_firmware tree. QMK_FIRMWARE = Path.cwd() +# Upstream repo url +QMK_FIRMWARE_UPSTREAM = 'qmk/qmk_firmware' + # This is the number of directories under `qmk_firmware/keyboards` that will be traversed. This is currently a limitation of our make system. MAX_KEYBOARD_SUBFOLDERS = 5 # Supported processor types -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' +CHIBIOS_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' LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85' diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index 47c8bff7a8..bcb4d81ef2 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -9,7 +9,7 @@ from milc import cli from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS from qmk.c_parse import find_layouts -from qmk.json_schema import deep_update, json_load, keyboard_validate, keyboard_api_validate +from qmk.json_schema import deep_update, json_load, validate from qmk.keyboard import config_h, rules_mk from qmk.keymap import list_keymaps from qmk.makefile import parse_rules_mk_file @@ -64,9 +64,12 @@ def info_json(keyboard): info_data = _extract_config_h(info_data) info_data = _extract_rules_mk(info_data) + # Ensure that we have matrix row and column counts + info_data = _matrix_size(info_data) + # Validate against the jsonschema try: - keyboard_api_validate(info_data) + validate(info_data, 'qmk.api.keyboard.v1') except jsonschema.ValidationError as e: json_path = '.'.join([str(p) for p in e.absolute_path]) @@ -90,6 +93,9 @@ def info_json(keyboard): if layout_name not in info_data.get('layouts', {}) and layout_name not in info_data.get('layout_aliases', {}): _log_error(info_data, 'Claims to support community layout %s but no %s() macro found' % (layout, layout_name)) + # Check that the reported matrix size is consistent with the actual matrix size + _check_matrix(info_data) + return info_data @@ -143,10 +149,7 @@ def _pin_name(pin): elif pin == 'NO_PIN': return None - elif pin[0] in 'ABCDEFGHIJK' and pin[1].isdigit(): - return pin - - raise ValueError(f'Invalid pin: {pin}') + return pin def _extract_pins(pins): @@ -341,6 +344,46 @@ def _extract_rules_mk(info_data): return info_data +def _matrix_size(info_data): + """Add info_data['matrix_size'] if it doesn't exist. + """ + if 'matrix_size' not in info_data and 'matrix_pins' in info_data: + info_data['matrix_size'] = {} + + if 'direct' in info_data['matrix_pins']: + info_data['matrix_size']['cols'] = len(info_data['matrix_pins']['direct'][0]) + info_data['matrix_size']['rows'] = len(info_data['matrix_pins']['direct']) + elif 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']: + info_data['matrix_size']['cols'] = len(info_data['matrix_pins']['cols']) + info_data['matrix_size']['rows'] = len(info_data['matrix_pins']['rows']) + + return info_data + + +def _check_matrix(info_data): + """Check the matrix to ensure that row/column count is consistent. + """ + if 'matrix_pins' in info_data and 'matrix_size' in info_data: + actual_col_count = info_data['matrix_size'].get('cols', 0) + actual_row_count = info_data['matrix_size'].get('rows', 0) + col_count = row_count = 0 + + if 'direct' in info_data['matrix_pins']: + col_count = len(info_data['matrix_pins']['direct'][0]) + row_count = len(info_data['matrix_pins']['direct']) + elif 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']: + col_count = len(info_data['matrix_pins']['cols']) + row_count = len(info_data['matrix_pins']['rows']) + + if col_count != actual_col_count and col_count != (actual_col_count / 2): + # FIXME: once we can we should detect if split is enabled to do the actual_col_count/2 check. + _log_error(info_data, f'MATRIX_COLS is inconsistent with the size of MATRIX_COL_PINS: {col_count} != {actual_col_count}') + + if row_count != actual_row_count and row_count != (actual_row_count / 2): + # FIXME: once we can we should detect if split is enabled to do the actual_row_count/2 check. + _log_error(info_data, f'MATRIX_ROWS is inconsistent with the size of MATRIX_ROW_PINS: {row_count} != {actual_row_count}') + + def _merge_layouts(info_data, new_info_data): """Merge new_info_data into info_data in an intelligent way. """ @@ -493,7 +536,7 @@ def merge_info_jsons(keyboard, info_data): continue try: - keyboard_validate(new_info_data) + validate(new_info_data, 'qmk.keyboard.v1') except jsonschema.ValidationError as e: json_path = '.'.join([str(p) for p in e.absolute_path]) 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 077dfcaa93..3e5663a291 100644 --- a/lib/python/qmk/json_schema.py +++ b/lib/python/qmk/json_schema.py @@ -24,9 +24,10 @@ def json_load(json_file): def load_jsonschema(schema_name): """Read a jsonschema file from disk. - - FIXME(skullydazed/anyone): Refactor to make this a public function. """ + if Path(schema_name).exists(): + return json_load(schema_name) + schema_path = Path(f'data/schemas/{schema_name}.jsonschema') if not schema_path.exists(): @@ -35,28 +36,33 @@ def load_jsonschema(schema_name): return json_load(schema_path) -def keyboard_validate(data): - """Validates data against the keyboard jsonschema. +def create_validator(schema): + """Creates a validator for the given schema id. """ - schema = load_jsonschema('keyboard') - validator = jsonschema.Draft7Validator(schema).validate + schema_store = {} - return validator(data) + for schema_file in Path('data/schemas').glob('*.jsonschema'): + schema_data = load_jsonschema(schema_file) + if not isinstance(schema_data, dict): + cli.log.debug('Skipping schema file %s', schema_file) + continue + schema_store[schema_data['$id']] = schema_data + + resolver = jsonschema.RefResolver.from_schema(schema_store['qmk.keyboard.v1'], store=schema_store) + + return jsonschema.Draft7Validator(schema_store[schema], resolver=resolver).validate -def keyboard_api_validate(data): - """Validates data against the api_keyboard jsonschema. +def validate(data, schema): + """Validates data against a schema. """ - base = load_jsonschema('keyboard') - relative = load_jsonschema('api_keyboard') - resolver = jsonschema.RefResolver.from_schema(base) - validator = jsonschema.Draft7Validator(relative, resolver=resolver).validate + validator = create_validator(schema) return validator(data) def deep_update(origdict, newdict): - """Update a dictionary in place, recursing to do a deep copy. + """Update a dictionary in place, recursing to do a depth-first deep copy. """ for key, value in newdict.items(): 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 afdbc81429..b341e1c912 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(): assert 'MCU ?= atmega32u4' in result.stdout +def test_generate_version_h(): + result = check_subcommand('generate-version-h') + check_returncode(result) + assert '#define QMK_VERSION' in result.stdout + + def test_generate_layouts(): result = check_subcommand('generate-layouts', '-kb', 'handwired/pytest/basic') check_returncode(result) |