From ededff8556daff544633cb143cb6d939afd09014 Mon Sep 17 00:00:00 2001 From: Zach White Date: Tue, 1 Dec 2020 12:52:02 -0800 Subject: [PATCH] validate keyboard data with jsonschema --- lib/python/qmk/cli/generate/info_json.py | 2 +- lib/python/qmk/cli/generate/rules_mk.py | 13 ++ lib/python/qmk/info.py | 151 +++++++++++++++++++++-- requirements.txt | 1 + 4 files changed, 155 insertions(+), 12 deletions(-) diff --git a/lib/python/qmk/cli/generate/info_json.py b/lib/python/qmk/cli/generate/info_json.py index 7e6654e45d..fba4b1c014 100755 --- a/lib/python/qmk/cli/generate/info_json.py +++ b/lib/python/qmk/cli/generate/info_json.py @@ -39,7 +39,7 @@ def generate_info_json(cli): pared_down_json[key] = kb_info_json[key] pared_down_json['layouts'] = {} - if 'layouts' in pared_down_json: + if 'layouts' in kb_info_json: for layout_name, layout in kb_info_json['layouts'].items(): pared_down_json['layouts'][layout_name] = {} pared_down_json['layouts'][layout_name]['key_count'] = layout.get('key_count', len(layout['layout'])) diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py index 4268ae047b..72ed3c45fa 100755 --- a/lib/python/qmk/cli/generate/rules_mk.py +++ b/lib/python/qmk/cli/generate/rules_mk.py @@ -6,6 +6,10 @@ from qmk.decorators import automagic_keyboard, automagic_keymap from qmk.info import info_json from qmk.path import is_keyboard, normpath +info_to_rules = { + 'bootloader': 'BOOTLOADER', + 'processor': 'MCU' +} @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") @@ -30,6 +34,10 @@ def generate_rules_mk(cli): kb_info_json = info_json(cli.config.generate_rules_mk.keyboard) rules_mk_lines = ['# This file was generated by `qmk generate-rules-mk`. Do not edit or copy.', ''] + # Bring in settings + for info_key, rule_key in info_to_rules.items(): + rules_mk_lines.append(f'{rule_key} := {kb_info_json[info_key]}') + # Find features that should be enabled if 'features' in kb_info_json: for feature, enabled in kb_info_json['features'].items(): @@ -37,6 +45,11 @@ def generate_rules_mk(cli): enabled = 'yes' if enabled else 'no' rules_mk_lines.append(f'{feature}_ENABLE := {enabled}') + # Set the LED driver + if 'led_matrix' in kb_info_json and 'driver' in kb_info_json['led_matrix']: + driver = kb_info_json['led_matrix']['driver'] + rules_mk_lines.append(f'LED_MATRIX_DRIVER = {driver}') + # Add community layouts if 'community_layouts' in kb_info_json: rules_mk_lines.append(f'LAYOUTS = {" ".join(kb_info_json["community_layouts"])}') diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index 4611874e85..1cf12190d6 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -4,6 +4,7 @@ import json from glob import glob from pathlib import Path +import jsonschema from milc import cli from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS, LED_INDICATORS @@ -13,6 +14,17 @@ from qmk.keymap import list_keymaps from qmk.makefile import parse_rules_mk_file from qmk.math import compute +led_matrix_properties = { + 'driver_count': 'LED_DRIVER_COUNT', + 'driver_addr1': 'LED_DRIVER_ADDR_1', + 'driver_addr2': 'LED_DRIVER_ADDR_2', + 'driver_addr3': 'LED_DRIVER_ADDR_3', + 'driver_addr4': 'LED_DRIVER_ADDR_4', + 'led_count': 'LED_DRIVER_LED_COUNT', + 'timeout': 'ISSI_TIMEOUT', + 'persistence': 'ISSI_PERSISTENCE' +} + rgblight_properties = { 'led_count': 'RGBLED_NUM', 'pin': 'RGB_DI_PIN', @@ -80,6 +92,15 @@ def info_json(keyboard): info_data = _extract_config_h(info_data) info_data = _extract_rules_mk(info_data) + # Validate against the jsonschema + try: + keyboard_api_validate(info_data) + + except jsonschema.ValidationError as e: + cli.log.error('Invalid info.json data: %s', e.message) + print(dir(e)) + exit() + # Make sure we have at least one layout if not info_data.get('layouts'): _log_error(info_data, 'No LAYOUTs defined! Need at least one layout defined in the keyboard.h or info.json.') @@ -102,6 +123,50 @@ def info_json(keyboard): return info_data +def _json_load(json_file): + """Load a json file from disk. + + Note: file must be a Path object. + """ + try: + return json.load(json_file.open()) + + except json.decoder.JSONDecodeError as e: + cli.log.error('Invalid JSON encountered attempting to load {fg_cyan}%s{fg_reset}:\n\t{fg_red}%s', json_file, e) + exit(1) + + +def _jsonschema(schema_name): + """Read a jsonschema file from disk. + """ + schema_path = Path(f'data/schemas/{schema_name}.jsonschema') + + if not schema_path.exists(): + schema_path = Path('data/schemas/false.jsonschema') + + return _json_load(schema_path) + + +def keyboard_validate(data): + """Validates data against the keyboard jsonschema. + """ + schema = _jsonschema('keyboard') + validator = jsonschema.Draft7Validator(schema).validate + + return validator(data) + + +def keyboard_api_validate(data): + """Validates data against the api_keyboard jsonschema. + """ + base = _jsonschema('keyboard') + relative = _jsonschema('api_keyboard') + resolver = jsonschema.RefResolver.from_schema(base) + validator = jsonschema.Draft7Validator(relative, resolver=resolver).validate + + return validator(data) + + def _extract_debounce(info_data, config_c): """Handle debounce. """ @@ -109,7 +174,7 @@ def _extract_debounce(info_data, config_c): _log_warning(info_data, 'Debounce is specified in both info.json and config.h, the config.h value wins.') if 'DEBOUNCE' in config_c: - info_data['debounce'] = config_c.get('DEBOUNCE') + info_data['debounce'] = int(config_c['DEBOUNCE']) return info_data @@ -181,8 +246,36 @@ def _extract_features(info_data, rules): return info_data +def _extract_led_drivers(info_data, rules): + """Find all the LED drivers set in rules.mk. + """ + if 'LED_MATRIX_DRIVER' in rules: + if 'led_matrix' not in info_data: + info_data['led_matrix'] = {} + + if info_data['led_matrix'].get('driver'): + _log_warning(info_data, 'LED Matrix driver is specified in both info.json and rules.mk, the rules.mk value wins.') + + info_data['led_matrix']['driver'] = rules['LED_MATRIX_DRIVER'] + + return info_data + + +def _extract_led_matrix(info_data, config_c): + """Handle the led_matrix configuration. + """ + led_matrix = info_data.get('led_matrix', {}) + + for json_key, config_key in led_matrix_properties.items(): + if config_key in config_c: + if json_key in led_matrix: + _log_warning(info_data, 'LED Matrix: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,)) + + led_matrix[json_key] = config_c[config_key] + + def _extract_rgblight(info_data, config_c): - """Handle the rgblight configuration + """Handle the rgblight configuration. """ rgblight = info_data.get('rgblight', {}) animations = rgblight.get('animations', {}) @@ -303,6 +396,7 @@ def _extract_config_h(info_data): _extract_indicators(info_data, config_c) _extract_matrix_info(info_data, config_c) _extract_usb_info(info_data, config_c) + _extract_led_matrix(info_data, config_c) _extract_rgblight(info_data, config_c) return info_data @@ -326,6 +420,7 @@ def _extract_rules_mk(info_data): _extract_community_layouts(info_data, rules) _extract_features(info_data, rules) + _extract_led_drivers(info_data, rules) return info_data @@ -412,13 +507,28 @@ def arm_processor_rules(info_data, rules): """Setup the default info for an ARM board. """ info_data['processor_type'] = 'arm' - info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'unknown' - info_data['processor'] = rules['MCU'] if 'MCU' in rules else 'unknown' info_data['protocol'] = 'ChibiOS' - if info_data['bootloader'] == 'unknown': + if 'MCU' in rules: + if 'processor' in info_data: + _log_warning(info_data, 'Processor/MCU is specified in both info.json and rules.mk, the rules.mk value wins.') + + info_data['processor'] = rules['MCU'] + + elif 'processor' not in info_data: + info_data['processor'] = 'unknown' + + if 'BOOTLOADER' in rules: + if 'bootloader' in info_data: + _log_warning(info_data, 'Bootloader is specified in both info.json and rules.mk, the rules.mk value wins.') + + info_data['bootloader'] = rules['BOOTLOADER'] + + else: if 'STM32' in info_data['processor']: info_data['bootloader'] = 'stm32-dfu' + else: + info_data['bootloader'] = 'unknown' if 'STM32' in info_data['processor']: info_data['platform'] = 'STM32' @@ -436,9 +546,25 @@ def avr_processor_rules(info_data, rules): info_data['processor_type'] = 'avr' info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'atmel-dfu' info_data['platform'] = rules['ARCH'] if 'ARCH' in rules else 'unknown' - info_data['processor'] = rules['MCU'] if 'MCU' in rules else 'unknown' info_data['protocol'] = 'V-USB' if rules.get('MCU') in VUSB_PROCESSORS else 'LUFA' + if 'MCU' in rules: + if 'processor' in info_data: + _log_warning(info_data, 'Processor/MCU is specified in both info.json and rules.mk, the rules.mk value wins.') + + info_data['processor'] = rules['MCU'] + + elif 'processor' not in info_data: + info_data['processor'] = 'unknown' + + if 'BOOTLOADER' in rules: + if 'bootloader' in info_data: + _log_warning(info_data, 'Bootloader is specified in both info.json and rules.mk, the rules.mk value wins.') + + info_data['bootloader'] = rules['BOOTLOADER'] + else: + info_data['bootloader'] = 'atmel-dfu' + # FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk: # info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA' @@ -463,10 +589,13 @@ def merge_info_jsons(keyboard, info_data): for info_file in find_info_json(keyboard): # Load and validate the JSON data try: - new_info_data = json.load(info_file.open('r')) - except Exception as e: - _log_error(info_data, "Invalid JSON in file %s: %s: %s" % (str(info_file), e.__class__.__name__, e)) - new_info_data = {} + new_info_data = _json_load(info_file) + keyboard_validate(new_info_data) + + except jsonschema.ValidationError as e: + cli.log.error('Invalid info.json data: %s', e.message) + cli.log.error('Not including file %s', info_file) + continue if not isinstance(new_info_data, dict): _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),)) @@ -479,7 +608,7 @@ def merge_info_jsons(keyboard, info_data): # Deep merge certain keys # FIXME(skullydazed/anyone): this should be generalized more so that we can inteligently merge more than one level deep. It would be nice if we could filter on valid keys too. That may have to wait for a future where we use openapi or something. - for key in ('features', 'layout_aliases', 'matrix_pins', 'rgblight', 'usb'): + for key in ('features', 'layout_aliases', 'led_matrix', 'matrix_pins', 'rgblight', 'usb'): if key in new_info_data: if key not in info_data: info_data[key] = {} diff --git a/requirements.txt b/requirements.txt index 6e907cf8e8..f4d43da8d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,6 @@ appdirs argcomplete colorama hjson +jsonschema milc pygments