From de381ad3b72ccc8161744c8c14b95430fd4d498d Mon Sep 17 00:00:00 2001 From: Joel Challis Date: Wed, 13 Sep 2023 01:12:46 +0100 Subject: Generate keymap.json config options more forcefully (#21960) --- lib/python/qmk/cli/generate/config_h.py | 5 +++++ lib/python/qmk/cli/generate/rules_mk.py | 26 ++++++++++++++++---------- 2 files changed, 21 insertions(+), 10 deletions(-) (limited to 'lib/python') diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py index 64d4db6ffe..828785ea48 100755 --- a/lib/python/qmk/cli/generate/config_h.py +++ b/lib/python/qmk/cli/generate/config_h.py @@ -15,7 +15,12 @@ from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE def generate_define(define, value=None): + is_keymap = cli.args.filename value = f' {value}' if value is not None else '' + if is_keymap: + return f""" +#undef {define} +#define {define}{value}""" return f""" #ifndef {define} # define {define}{value} diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py index fc272da6c6..5291556109 100755 --- a/lib/python/qmk/cli/generate/rules_mk.py +++ b/lib/python/qmk/cli/generate/rules_mk.py @@ -14,6 +14,12 @@ from qmk.path import normpath, FileType from qmk.constants import GPL2_HEADER_SH_LIKE, GENERATED_HEADER_SH_LIKE +def generate_rule(rules_key, rules_value): + is_keymap = cli.args.filename + rule_assignment_operator = '=' if is_keymap else '?=' + return f'{rules_key} {rule_assignment_operator} {rules_value}' + + def process_mapping_rule(kb_info_json, rules_key, info_dict): """Return the rules.mk line(s) for a mapping rule. """ @@ -29,15 +35,15 @@ def process_mapping_rule(kb_info_json, rules_key, info_dict): return None if key_type in ['array', 'list']: - return f'{rules_key} ?= {" ".join(rules_value)}' + return generate_rule(rules_key, " ".join(rules_value)) elif key_type == 'bool': - return f'{rules_key} ?= {"yes" if rules_value else "no"}' + return generate_rule(rules_key, "yes" if rules_value else "no") elif key_type == 'mapping': - return '\n'.join([f'{key} ?= {value}' for key, value in rules_value.items()]) + return '\n'.join([generate_rule(key, value) for key, value in rules_value.items()]) elif key_type == 'str': - return f'{rules_key} ?= "{rules_value}"' + return generate_rule(rules_key, f'"{rules_value}"') - return f'{rules_key} ?= {rules_value}' + return generate_rule(rules_key, rules_value) @cli.argument('filename', nargs='?', arg_only=True, type=FileType('r'), completer=FilesCompleter('.json'), help='A configurator export JSON to be compiled and flashed or a pre-compiled binary firmware file (bin/hex) to be flashed.') @@ -77,21 +83,21 @@ def generate_rules_mk(cli): for feature, enabled in kb_info_json['features'].items(): feature = feature.upper() enabled = 'yes' if enabled else 'no' - rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}') + rules_mk_lines.append(generate_rule(f'{feature}_ENABLE', enabled)) # Set SPLIT_TRANSPORT, if needed if kb_info_json.get('split', {}).get('transport', {}).get('protocol') == 'custom': - rules_mk_lines.append('SPLIT_TRANSPORT ?= custom') + rules_mk_lines.append(generate_rule('SPLIT_TRANSPORT', 'custom')) # Set CUSTOM_MATRIX, if needed if kb_info_json.get('matrix_pins', {}).get('custom'): if kb_info_json.get('matrix_pins', {}).get('custom_lite'): - rules_mk_lines.append('CUSTOM_MATRIX ?= lite') + rules_mk_lines.append(generate_rule('CUSTOM_MATRIX', 'lite')) else: - rules_mk_lines.append('CUSTOM_MATRIX ?= yes') + rules_mk_lines.append(generate_rule('CUSTOM_MATRIX', 'yes')) if converter: - rules_mk_lines.append(f'CONVERT_TO ?= {converter}') + rules_mk_lines.append(generate_rule('CONVERT_TO', converter)) # Show the results dump_lines(cli.args.output, rules_mk_lines) -- cgit v1.2.3 From 4e86dca49d5b86cf47d6b43172f0d4b17f15cd3a Mon Sep 17 00:00:00 2001 From: Joel Challis Date: Wed, 27 Sep 2023 03:57:37 +0100 Subject: Fix parsing/validation for 21939 (#22148) --- data/schemas/keyboard.jsonschema | 4 ++-- lib/python/qmk/info.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'lib/python') diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index 9c63ab8bcf..e0be8f5b97 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -404,7 +404,7 @@ "speed_steps": {"$ref": "qmk.definitions.v1#/unsigned_int"}, "led_flush_limit": {"$ref": "qmk.definitions.v1#/unsigned_int"}, "led_process_limit": {"$ref": "qmk.definitions.v1#/unsigned_int"}, - "react_on_keyup": {"$ref": "qmk.definitions.v1#/unsigned_int"}, + "react_on_keyup": {"type": "boolean"}, "sleep": {"type": "boolean"}, "split_count": { "type": "array", @@ -460,7 +460,7 @@ "speed_steps": {"$ref": "qmk.definitions.v1#/unsigned_int"}, "led_flush_limit": {"$ref": "qmk.definitions.v1#/unsigned_int"}, "led_process_limit": {"$ref": "qmk.definitions.v1#/unsigned_int"}, - "react_on_keyup": {"$ref": "qmk.definitions.v1#/unsigned_int"}, + "react_on_keyup": {"type": "boolean"}, "sleep": {"type": "boolean"}, "split_count": { "type": "array", diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index 9c8521a2a3..4806add67a 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -520,6 +520,8 @@ def _config_to_json(key_type, config_value): return list(map(str.strip, config_value.split(','))) elif key_type == 'bool': + if isinstance(config_value, bool): + return config_value return config_value in true_values elif key_type == 'hex': -- cgit v1.2.3 From e4c54a9612318ea03accfb688c8440b62c5706e1 Mon Sep 17 00:00:00 2001 From: Joel Challis Date: Sat, 21 Oct 2023 01:48:05 +0100 Subject: Support additional split sync items for info.json (#22193) --- data/mappings/info_config.hjson | 12 ++++++++++-- data/schemas/keyboard.jsonschema | 28 +++++++++++++++++++++++++--- docs/reference_info_json.md | 37 +++++++++++++++++++++++++++++++------ lib/python/qmk/info.py | 9 +++++++++ 4 files changed, 75 insertions(+), 11 deletions(-) (limited to 'lib/python') diff --git a/data/mappings/info_config.hjson b/data/mappings/info_config.hjson index e68a4439d0..4cebe906b4 100644 --- a/data/mappings/info_config.hjson +++ b/data/mappings/info_config.hjson @@ -146,13 +146,21 @@ // Split Keyboard "SOFT_SERIAL_PIN": {"info_key": "split.soft_serial_pin"}, "SOFT_SERIAL_SPEED": {"info_key": "split.soft_serial_speed"}, - "SPLIT_MODS_ENABLE": {"info_key": "split.transport.sync_modifiers", "value_type": "bool"}, - "SPLIT_TRANSPORT_MIRROR": {"info_key": "split.transport.sync_matrix_state", "value_type": "bool"}, "SPLIT_USB_DETECT": {"info_key": "split.usb_detect.enabled", "value_type": "bool"}, "SPLIT_USB_TIMEOUT": {"info_key": "split.usb_detect.timeout", "value_type": "int"}, "SPLIT_USB_TIMEOUT_POLL": {"info_key": "split.usb_detect.polling_interval", "value_type": "int"}, "SPLIT_WATCHDOG_ENABLE": {"info_key": "split.transport.watchdog", "value_type": "bool"}, "SPLIT_WATCHDOG_TIMEOUT": {"info_key": "split.transport.watchdog_timeout", "value_type": "int"}, + "SPLIT_ACTIVITY_ENABLE": {"info_key": "split.transport.sync.activity", "value_type": "bool"}, + "SPLIT_DETECTED_OS_ENABLE": {"info_key": "split.transport.sync.detected_os", "value_type": "bool"}, + "SPLIT_HAPTIC_ENABLE": {"info_key": "split.transport.sync.haptic", "value_type": "bool"}, + "SPLIT_LAYER_STATE_ENABLE": {"info_key": "split.transport.sync.layer_state", "value_type": "bool"}, + "SPLIT_LED_STATE_ENABLE": {"info_key": "split.transport.sync.indicators", "value_type": "bool"}, + "SPLIT_TRANSPORT_MIRROR": {"info_key": "split.transport.sync.matrix_state", "value_type": "bool"}, + "SPLIT_MODS_ENABLE": {"info_key": "split.transport.sync.modifiers", "value_type": "bool"}, + "SPLIT_OLED_ENABLE": {"info_key": "split.transport.sync.oled", "value_type": "bool"}, + "SPLIT_ST7565_ENABLE": {"info_key": "split.transport.sync.st7565", "value_type": "bool"}, + "SPLIT_WPM_ENABLE": {"info_key": "split.transport.sync.wpm", "value_type": "bool"}, // Tapping "HOLD_ON_OTHER_KEY_PRESS": {"info_key": "tapping.hold_on_other_key_press", "value_type": "bool"}, diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index e0be8f5b97..8ff94fb9f2 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -663,10 +663,32 @@ "type": "string", "enum": ["custom", "i2c", "serial", "serial_usart"] }, - "sync_matrix_state": {"type": "boolean"}, - "sync_modifiers": {"type": "boolean"}, + "sync": { + "type": "object", + "additionalProperties": false, + "properties": { + "activity": {"type": "boolean"}, + "detected_os": {"type": "boolean"}, + "haptic": {"type": "boolean"}, + "layer_state": {"type": "boolean"}, + "indicators": {"type": "boolean"}, + "matrix_state": {"type": "boolean"}, + "modifiers": {"type": "boolean"}, + "oled": {"type": "boolean"}, + "st7565": {"type": "boolean"}, + "wpm": {"type": "boolean"} + } + } "watchdog": {"type": "boolean"}, - "watchdog_timeout": {"$ref": "qmk.definitions.v1#/unsigned_int"} + "watchdog_timeout": {"$ref": "qmk.definitions.v1#/unsigned_int"}, + "sync_matrix_state": { + "type": "boolean", + "$comment": "Deprecated: use sync.matrix_state instead" + }, + "sync_modifiers": { + "type": "boolean", + "$comment": "Deprecated: use sync.modifiers instead" + } } }, "usb_detect": { diff --git a/docs/reference_info_json.md b/docs/reference_info_json.md index f31d3c4a2f..e05b09c6cd 100644 --- a/docs/reference_info_json.md +++ b/docs/reference_info_json.md @@ -633,12 +633,37 @@ Configures the [Split Keyboard](feature_split_keyboard.md) feature. * `transport` * `protocol` * The split transport protocol to use. Must be one of `custom`, `i2c`, `serial`, `serial_usart`. - * `sync_matrix_state` - * Mirror the main/primary half's matrix state to the secondary half. - * Default: `false` - * `sync_modifiers` - * Mirror the modifier state to the secondary half. - * Default: `false` + * `sync` + * `activity` + * Mirror the activity timestamps to the secondary half. + * Default: `false` + * `detected_os` + * Mirror the [detected OS](feature_os_detection.md) to the secondary half. + * Default: `false` + * `haptic` + * Mirror the haptic state and process haptic feedback to the secondary half. + * Default: `false` + * `layer_state` + * Mirror the layer state to the secondary half. + * Default: `false` + * `indicators` + * Mirror the indicator state to the secondary half. + * Default: `false` + * `matrix_state` + * Mirror the main/primary half's matrix state to the secondary half. + * Default: `false` + * `modifiers` + * Mirror the modifier state to the secondary half. + * Default: `false` + * `oled` + * Mirror the OLED on/off status to the secondary half. + * Default: `false` + * `st7565` + * Mirror the ST7565 on/off status to the secondary half. + * Default: `false` + * `wpm` + * Mirror the current WPM value to the secondary half. + * Default: `false` * `watchdog` * Reboot the secondary half if it loses connection. * Default: `false` diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index 4806add67a..265e6a645f 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -428,6 +428,15 @@ def _extract_split_transport(info_data, config_c): if 'protocol' not in info_data['split']['transport']: info_data['split']['transport']['protocol'] = 'serial' + # Migrate + transport = info_data.get('split', {}).get('transport', {}) + if 'sync_matrix_state' in transport: + transport['sync'] = transport.get('sync', {}) + transport['sync']['matrix_state'] = transport.pop('sync_matrix_state') + if 'sync_modifiers' in transport: + transport['sync'] = transport.get('sync', {}) + transport['sync']['modifiers'] = transport.pop('sync_modifiers') + def _extract_split_right_pins(info_data, config_c): # Figure out the right half matrix pins -- cgit v1.2.3 From 98530cad3ba8733d8a100e0bc5f3cf47339c4b3e Mon Sep 17 00:00:00 2001 From: Joel Challis Date: Sun, 29 Oct 2023 01:09:02 +0100 Subject: Implement data driven dip switches (#22017) * Add data driven dip switches * Autogen weak matrix_mask --- data/mappings/info_config.hjson | 4 ++++ data/mappings/info_rules.hjson | 1 + data/schemas/keyboard.jsonschema | 38 ++++++++++++++++++++++++++++++- docs/reference_info_json.md | 18 +++++++++++++++ lib/python/qmk/cli/generate/config_h.py | 8 +++++++ lib/python/qmk/cli/generate/keyboard_c.py | 26 +++++++++++++++++++++ 6 files changed, 94 insertions(+), 1 deletion(-) (limited to 'lib/python') diff --git a/data/mappings/info_config.hjson b/data/mappings/info_config.hjson index 66aca35e94..9b65c73084 100644 --- a/data/mappings/info_config.hjson +++ b/data/mappings/info_config.hjson @@ -45,6 +45,10 @@ // Combos "COMBO_TERM": {"info_key": "combo.term", "value_type": "int"}, + "DIP_SWITCH_MATRIX_GRID": {"info_key": "dip_switch.matrix_grid", "value_type": "array.array.int", "to_json": false}, + "DIP_SWITCH_PINS": {"info_key": "dip_switch.pins", "value_type": "array"}, + "DIP_SWITCH_PINS_RIGHT": {"info_key": "split.dip_switch.right.pins", "value_type": "array"}, + // Dynamic Keymap "DYNAMIC_KEYMAP_EEPROM_MAX_ADDR": {"info_key": "dynamic_keymap.eeprom_max_addr", "value_type": "int"}, "DYNAMIC_KEYMAP_LAYER_COUNT": {"info_key": "dynamic_keymap.layer_count", "value_type": "int"}, diff --git a/data/mappings/info_rules.hjson b/data/mappings/info_rules.hjson index 6a0f0a3030..02fc2bee9d 100644 --- a/data/mappings/info_rules.hjson +++ b/data/mappings/info_rules.hjson @@ -17,6 +17,7 @@ "BOOTLOADER": {"info_key": "bootloader", "warn_duplicate": false}, "BOOTMAGIC_ENABLE": {"info_key": "bootmagic.enabled", "value_type": "bool"}, "CAPS_WORD_ENABLE": {"info_key": "caps_word.enabled", "value_type": "bool"}, + "DIP_SWITCH_ENABLE": {"info_key": "dip_switch.enabled", "value_type": "bool"}, "DEBOUNCE_TYPE": {"info_key": "build.debounce_type"}, "EEPROM_DRIVER": {"info_key": "eeprom.driver"}, "ENCODER_ENABLE": {"info_key": "encoder.enabled", "value_type": "bool"}, diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index c120099745..540ce4e42a 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -20,7 +20,15 @@ } } } - } + }, + "dip_switch_config": { + "type": "object", + "properties": { + "pins": { + "$ref": "qmk.definitions.v1#/mcu_pin_array" + } + } + }, }, "type": "object", "not": { "required": [ "vendorId", "productId" ] }, // reject via keys... @@ -245,6 +253,25 @@ "type": "array", "items": {"$ref": "qmk.definitions.v1#/filename"} }, + "dip_switch": { + "$ref": "#/definitions/dip_switch_config", + "properties": { + "enabled": {"type": "boolean"}, + "matrix_grid": { + "type": "array", + "minItems": 1, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "integer", + "minimum": 0 + } + } + } + } + }, "eeprom": { "properties": { "driver": {"type": "string"}, @@ -636,6 +663,15 @@ } } }, + "dip_switch": { + "type": "object", + "additionalProperties": false, + "properties": { + "right": { + "$ref": "#/definitions/dip_switch_config" + } + } + }, "encoder": { "type": "object", "additionalProperties": false, diff --git a/docs/reference_info_json.md b/docs/reference_info_json.md index 6b60bb20c4..1d9429b54d 100644 --- a/docs/reference_info_json.md +++ b/docs/reference_info_json.md @@ -201,6 +201,20 @@ Configures the [Combo](feature_combo.md) feature. * The amount of time to recognize a combo in milliseconds. * Default: `50` (50 ms) +## DIP Switches :id=dip-switch + +Configures the [DIP Switches](feature_dip_switch.md) feature. + +* `dip_switch` + * `enabled` + * Enable the DIP Switches feature. + * Default: `false` + * `pins` + * A list of GPIO pins connected to the MCU. + * `matrix_grid` + * A list of matrix locations in the key matrix. + * Example: `[ [0,6], [1,6], [2,6] ]` + ## EEPROM :id=eeprom Configures the [EEPROM](eeprom_driver.md) driver. @@ -622,6 +636,10 @@ Configures the [Split Keyboard](feature_split_keyboard.md) feature. * `bootmagic` * `matrix` * See [Bootmagic](#bootmagic) config. + * `dip_switch` + * `right` + * `pins` + * See [DIP Switches](#dip-switch) config. * `enabled` * Enable the Split Keyboard feature. * Default: `false` diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py index 828785ea48..b261d6f851 100755 --- a/lib/python/qmk/cli/generate/config_h.py +++ b/lib/python/qmk/cli/generate/config_h.py @@ -72,6 +72,12 @@ def generate_matrix_size(kb_info_json, config_h_lines): config_h_lines.append(generate_define('MATRIX_ROWS', kb_info_json['matrix_size']['rows'])) +def generate_matrix_masked(kb_info_json, config_h_lines): + """"Enable matrix mask if required""" + if 'matrix_grid' in kb_info_json.get('dip_switch', {}): + config_h_lines.append(generate_define('MATRIX_MASKED')) + + def generate_config_items(kb_info_json, config_h_lines): """Iterate through the info_config map to generate basic config values. """ @@ -196,6 +202,8 @@ def generate_config_h(cli): generate_matrix_size(kb_info_json, config_h_lines) + generate_matrix_masked(kb_info_json, config_h_lines) + if 'matrix_pins' in kb_info_json: config_h_lines.append(matrix_pins(kb_info_json['matrix_pins'])) diff --git a/lib/python/qmk/cli/generate/keyboard_c.py b/lib/python/qmk/cli/generate/keyboard_c.py index 9004b41abb..325624c9cc 100755 --- a/lib/python/qmk/cli/generate/keyboard_c.py +++ b/lib/python/qmk/cli/generate/keyboard_c.py @@ -57,6 +57,31 @@ def _gen_led_config(info_data): return lines +def _gen_matrix_mask(info_data): + """Convert info.json content to matrix_mask + """ + cols = info_data['matrix_size']['cols'] + rows = info_data['matrix_size']['rows'] + + # Default mask to everything enabled + mask = [['1'] * cols for i in range(rows)] + + # Automatically mask out dip_switch.matrix_grid locations + matrix_grid = info_data.get('dip_switch', {}).get('matrix_grid', []) + for row, col in matrix_grid: + mask[row][col] = '0' + + lines = [] + lines.append('#ifdef MATRIX_MASKED') + lines.append('__attribute__((weak)) const matrix_row_t matrix_mask[] = {') + for i in range(rows): + lines.append(f' 0b{"".join(reversed(mask[i]))},') + lines.append('};') + lines.append('#endif') + + return lines + + @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('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate keyboard.c for.') @@ -70,6 +95,7 @@ def generate_keyboard_c(cli): keyboard_h_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#include QMK_KEYBOARD_H', ''] keyboard_h_lines.extend(_gen_led_config(kb_info_json)) + keyboard_h_lines.extend(_gen_matrix_mask(kb_info_json)) # Show the results dump_lines(cli.args.output, keyboard_h_lines, cli.args.quiet) -- cgit v1.2.3 From 559450a099539773e65adf4ab8c2e485344b7885 Mon Sep 17 00:00:00 2001 From: Joel Challis Date: Sun, 29 Oct 2023 23:41:44 +0000 Subject: Fix 'to_c' for config.h mappings (#22364) --- lib/python/qmk/cli/generate/config_h.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/python') diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py index b261d6f851..c4260fde54 100755 --- a/lib/python/qmk/cli/generate/config_h.py +++ b/lib/python/qmk/cli/generate/config_h.py @@ -86,9 +86,9 @@ def generate_config_items(kb_info_json, config_h_lines): for config_key, info_dict in info_config_map.items(): info_key = info_dict['info_key'] key_type = info_dict.get('value_type', 'raw') - to_config = info_dict.get('to_config', True) + to_c = info_dict.get('to_c', True) - if not to_config: + if not to_c: continue try: -- cgit v1.2.3 From 17c3182b1cc98adb5385c7c5223c775fce4d4dd9 Mon Sep 17 00:00:00 2001 From: Joel Challis Date: Mon, 30 Oct 2023 00:49:56 +0000 Subject: Remove use of broken split.main (#22363) --- data/schemas/keyboard.jsonschema | 9 ++-- keyboards/controllerworks/mini42/info.json | 1 - keyboards/hillside/48/0_1/info.json | 1 - keyboards/splitkb/aurora/corne/rev1/info.json | 1 - keyboards/splitkb/aurora/helix/rev1/info.json | 1 - keyboards/splitkb/aurora/lily58/rev1/info.json | 1 - keyboards/splitkb/aurora/sofle_v2/rev1/info.json | 1 - keyboards/splitkb/aurora/sweep/rev1/info.json | 1 - keyboards/splitkb/kyria/rev3/info.json | 1 - keyboards/tzarc/djinn/info.json | 1 - lib/python/qmk/cli/generate/config_h.py | 18 -------- lib/python/qmk/info.py | 52 ------------------------ 12 files changed, 5 insertions(+), 83 deletions(-) (limited to 'lib/python') diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index 540ce4e42a..20216a7f86 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -681,10 +681,6 @@ } } }, - "main": { - "type": "string", - "enum": ["eeprom", "left", "matrix_grid", "pin", "right"] - }, "soft_serial_pin": {"$ref": "qmk.definitions.v1#/mcu_pin"}, "soft_serial_speed": { "type": "integer", @@ -735,6 +731,11 @@ "polling_interval": {"$ref": "qmk.definitions.v1#/unsigned_int"}, "timeout": {"$ref": "qmk.definitions.v1#/unsigned_int"} } + }, + "main": { + "type": "string", + "enum": ["eeprom", "left", "matrix_grid", "pin", "right"], + "$comment": "Deprecated: use config.h options for now" } } }, diff --git a/keyboards/controllerworks/mini42/info.json b/keyboards/controllerworks/mini42/info.json index c8c436c08f..5d1d56db1a 100644 --- a/keyboards/controllerworks/mini42/info.json +++ b/keyboards/controllerworks/mini42/info.json @@ -29,7 +29,6 @@ }, "split": { "enabled": true, - "main": "left", "matrix_pins": { "right": { "direct": [ diff --git a/keyboards/hillside/48/0_1/info.json b/keyboards/hillside/48/0_1/info.json index 2159d1ca68..b6007f1f72 100644 --- a/keyboards/hillside/48/0_1/info.json +++ b/keyboards/hillside/48/0_1/info.json @@ -23,7 +23,6 @@ }, "split": { "soft_serial_pin": "D2", - "main": "left", "encoder": { "right": { "rotary": [ diff --git a/keyboards/splitkb/aurora/corne/rev1/info.json b/keyboards/splitkb/aurora/corne/rev1/info.json index 7d82577b00..13f0034d55 100644 --- a/keyboards/splitkb/aurora/corne/rev1/info.json +++ b/keyboards/splitkb/aurora/corne/rev1/info.json @@ -83,7 +83,6 @@ "matrix": [4, 5] }, "soft_serial_pin": "D2", - "main": "pin", "matrix_pins": { "right": { "rows": ["B1", "B3", "B2", "B6"], diff --git a/keyboards/splitkb/aurora/helix/rev1/info.json b/keyboards/splitkb/aurora/helix/rev1/info.json index 7c6acf0070..1fee04011a 100644 --- a/keyboards/splitkb/aurora/helix/rev1/info.json +++ b/keyboards/splitkb/aurora/helix/rev1/info.json @@ -118,7 +118,6 @@ ] } }, - "main": "matrix_grid", "matrix_pins": { "right": { "cols": ["D4", "C6", "D7", "E6", "B4", "B5", "B6"], diff --git a/keyboards/splitkb/aurora/lily58/rev1/info.json b/keyboards/splitkb/aurora/lily58/rev1/info.json index 2d251b96b2..9a6ad58a14 100644 --- a/keyboards/splitkb/aurora/lily58/rev1/info.json +++ b/keyboards/splitkb/aurora/lily58/rev1/info.json @@ -94,7 +94,6 @@ "matrix": [5, 0] }, "soft_serial_pin": "D2", - "main": "matrix_grid", "matrix_pins": { "right": { "rows": ["F4", "D4", "B3", "B2", "B6"], diff --git a/keyboards/splitkb/aurora/sofle_v2/rev1/info.json b/keyboards/splitkb/aurora/sofle_v2/rev1/info.json index b9b8b9da6b..266ea72148 100644 --- a/keyboards/splitkb/aurora/sofle_v2/rev1/info.json +++ b/keyboards/splitkb/aurora/sofle_v2/rev1/info.json @@ -112,7 +112,6 @@ ] } }, - "main": "matrix_grid", "matrix_pins": { "right": { "cols": ["D7", "E6", "B4", "B5", "D4", "C6"], diff --git a/keyboards/splitkb/aurora/sweep/rev1/info.json b/keyboards/splitkb/aurora/sweep/rev1/info.json index 7d7fec921f..b5588e2a3c 100644 --- a/keyboards/splitkb/aurora/sweep/rev1/info.json +++ b/keyboards/splitkb/aurora/sweep/rev1/info.json @@ -79,7 +79,6 @@ "matrix": [4, 4] }, "soft_serial_pin": "D2", - "main": "pin", "matrix_pins": { "right": { "rows": ["B1", "F7", "F6", "B3"], diff --git a/keyboards/splitkb/kyria/rev3/info.json b/keyboards/splitkb/kyria/rev3/info.json index 29bea95323..81b539c507 100644 --- a/keyboards/splitkb/kyria/rev3/info.json +++ b/keyboards/splitkb/kyria/rev3/info.json @@ -98,7 +98,6 @@ "matrix": [4, 6] }, "soft_serial_pin": "D2", - "main": "matrix_grid", "matrix_pins": { "right": { "rows": ["F6", "F7", "B1", "B3"], diff --git a/keyboards/tzarc/djinn/info.json b/keyboards/tzarc/djinn/info.json index 1546369307..64ed1da690 100644 --- a/keyboards/tzarc/djinn/info.json +++ b/keyboards/tzarc/djinn/info.json @@ -45,7 +45,6 @@ }, "split": { "enabled": true, - "main": "pin", "encoder": { "right": { "rotary": [ diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py index c4260fde54..924834caef 100755 --- a/lib/python/qmk/cli/generate/config_h.py +++ b/lib/python/qmk/cli/generate/config_h.py @@ -141,24 +141,6 @@ def generate_encoder_config(encoder_json, config_h_lines, postfix=''): def generate_split_config(kb_info_json, config_h_lines): """Generate the config.h lines for split boards.""" - if 'primary' in kb_info_json['split']: - if kb_info_json['split']['primary'] in ('left', 'right'): - config_h_lines.append('') - config_h_lines.append('#ifndef MASTER_LEFT') - config_h_lines.append('# ifndef MASTER_RIGHT') - if kb_info_json['split']['primary'] == 'left': - config_h_lines.append('# define MASTER_LEFT') - elif kb_info_json['split']['primary'] == 'right': - config_h_lines.append('# define MASTER_RIGHT') - config_h_lines.append('# endif // MASTER_RIGHT') - config_h_lines.append('#endif // MASTER_LEFT') - elif kb_info_json['split']['primary'] == 'pin': - config_h_lines.append(generate_define('SPLIT_HAND_PIN')) - elif kb_info_json['split']['primary'] == 'matrix_grid': - config_h_lines.append(generate_define('SPLIT_HAND_MATRIX_GRID', f'{{ {",".join(kb_info_json["split"]["matrix_grid"])} }}')) - elif kb_info_json['split']['primary'] == 'eeprom': - config_h_lines.append(generate_define('EE_HANDS')) - if 'protocol' in kb_info_json['split'].get('transport', {}): if kb_info_json['split']['transport']['protocol'] == 'i2c': config_h_lines.append(generate_define('USE_I2C')) diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index 265e6a645f..b56ec0fbff 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -352,57 +352,6 @@ def _extract_secure_unlock(info_data, config_c): info_data['secure']['unlock_sequence'] = unlock_array -def _extract_split_main(info_data, config_c): - """Populate data about the split configuration - """ - # Figure out how the main half is determined - if config_c.get('SPLIT_HAND_PIN') is True: - if 'split' not in info_data: - info_data['split'] = {} - - if 'main' in info_data['split']: - _log_warning(info_data, 'Split main hand is specified in both config.h (SPLIT_HAND_PIN) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main']) - - info_data['split']['main'] = 'pin' - - if config_c.get('SPLIT_HAND_MATRIX_GRID'): - if 'split' not in info_data: - info_data['split'] = {} - - if 'main' in info_data['split']: - _log_warning(info_data, 'Split main hand is specified in both config.h (SPLIT_HAND_MATRIX_GRID) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main']) - - info_data['split']['main'] = 'matrix_grid' - info_data['split']['matrix_grid'] = _extract_pins(config_c['SPLIT_HAND_MATRIX_GRID']) - - if config_c.get('EE_HANDS') is True: - if 'split' not in info_data: - info_data['split'] = {} - - if 'main' in info_data['split']: - _log_warning(info_data, 'Split main hand is specified in both config.h (EE_HANDS) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main']) - - info_data['split']['main'] = 'eeprom' - - if config_c.get('MASTER_RIGHT') is True: - if 'split' not in info_data: - info_data['split'] = {} - - if 'main' in info_data['split']: - _log_warning(info_data, 'Split main hand is specified in both config.h (MASTER_RIGHT) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main']) - - info_data['split']['main'] = 'right' - - if config_c.get('MASTER_LEFT') is True: - if 'split' not in info_data: - info_data['split'] = {} - - if 'main' in info_data['split']: - _log_warning(info_data, 'Split main hand is specified in both config.h (MASTER_LEFT) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main']) - - info_data['split']['main'] = 'left' - - def _extract_split_transport(info_data, config_c): # Figure out the transport method if config_c.get('USE_I2C') is True: @@ -594,7 +543,6 @@ def _extract_config_h(info_data, config_c): _extract_matrix_info(info_data, config_c) _extract_audio(info_data, config_c) _extract_secure_unlock(info_data, config_c) - _extract_split_main(info_data, config_c) _extract_split_transport(info_data, config_c) _extract_split_right_pins(info_data, config_c) _extract_encoders(info_data, config_c) -- cgit v1.2.3 From a19ae3d78466588caa9caf7c38d1617932255733 Mon Sep 17 00:00:00 2001 From: Joel Challis Date: Wed, 1 Nov 2023 00:55:48 +0000 Subject: Add dd mapping for hardware based split handedness (#22369) --- data/mappings/info_config.hjson | 2 ++ data/schemas/keyboard.jsonschema | 21 +++++++++++++++++---- docs/reference_info_json.md | 6 ++++++ lib/python/qmk/cli/generate/config_h.py | 13 +++++++++++++ lib/python/qmk/cli/generate/keyboard_c.py | 15 ++++++++------- lib/python/qmk/info.py | 9 +++++++++ 6 files changed, 55 insertions(+), 11 deletions(-) (limited to 'lib/python') diff --git a/data/mappings/info_config.hjson b/data/mappings/info_config.hjson index 9b65c73084..2ad0135950 100644 --- a/data/mappings/info_config.hjson +++ b/data/mappings/info_config.hjson @@ -154,6 +154,8 @@ // Split Keyboard "SOFT_SERIAL_PIN": {"info_key": "split.soft_serial_pin"}, "SOFT_SERIAL_SPEED": {"info_key": "split.soft_serial_speed"}, + "SPLIT_HAND_MATRIX_GRID": {"info_key": "split.handedness.matrix_grid", "value_type": "array", "to_c": false}, + "SPLIT_HAND_PIN": {"info_key": "split.handedness.pin"}, "SPLIT_USB_DETECT": {"info_key": "split.usb_detect.enabled", "value_type": "bool"}, "SPLIT_USB_TIMEOUT": {"info_key": "split.usb_detect.timeout", "value_type": "int"}, "SPLIT_USB_TIMEOUT_POLL": {"info_key": "split.usb_detect.polling_interval", "value_type": "int"}, diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index 20216a7f86..188e0a5b3c 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -640,10 +640,6 @@ } } }, - "matrix_grid": { - "type": "array", - "items": {"$ref": "qmk.definitions.v1#/mcu_pin"} - }, "matrix_pins": { "type": "object", "additionalProperties": false, @@ -681,6 +677,18 @@ } } }, + "handedness": { + "type": "object", + "additionalProperties": false, + "properties": { + "pin": {"$ref": "qmk.definitions.v1#/mcu_pin"}, + "matrix_grid": { + "$ref": "qmk.definitions.v1#/mcu_pin_array", + "minItems": 2, + "maxItems": 2 + } + } + }, "soft_serial_pin": {"$ref": "qmk.definitions.v1#/mcu_pin"}, "soft_serial_speed": { "type": "integer", @@ -736,6 +744,11 @@ "type": "string", "enum": ["eeprom", "left", "matrix_grid", "pin", "right"], "$comment": "Deprecated: use config.h options for now" + }, + "matrix_grid": { + "type": "array", + "items": {"$ref": "qmk.definitions.v1#/mcu_pin"}, + "$comment": "Deprecated: use split.handedness.matrix_grid instead" } } }, diff --git a/docs/reference_info_json.md b/docs/reference_info_json.md index 1d9429b54d..d9336fa2bb 100644 --- a/docs/reference_info_json.md +++ b/docs/reference_info_json.md @@ -647,6 +647,12 @@ Configures the [Split Keyboard](feature_split_keyboard.md) feature. * `right` * `rotary` * See [Encoder](#encoder) config. + * `handedness` + * `pin` + * The GPIO pin connected to determine handedness. + * `matrix_grid` + * The GPIO pins of the matrix position which determines the handedness. + * Example: `["A1", "B5"]` * `matrix_pins` * `right` * See [Matrix](#matrix) config. diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py index 924834caef..2c624e3e9a 100755 --- a/lib/python/qmk/cli/generate/config_h.py +++ b/lib/python/qmk/cli/generate/config_h.py @@ -74,7 +74,14 @@ def generate_matrix_size(kb_info_json, config_h_lines): def generate_matrix_masked(kb_info_json, config_h_lines): """"Enable matrix mask if required""" + mask_required = False + if 'matrix_grid' in kb_info_json.get('dip_switch', {}): + mask_required = True + if 'matrix_grid' in kb_info_json.get('split', {}).get('handedness', {}): + mask_required = True + + if mask_required: config_h_lines.append(generate_define('MATRIX_MASKED')) @@ -141,6 +148,12 @@ def generate_encoder_config(encoder_json, config_h_lines, postfix=''): def generate_split_config(kb_info_json, config_h_lines): """Generate the config.h lines for split boards.""" + if 'handedness' in kb_info_json['split']: + # TODO: change SPLIT_HAND_MATRIX_GRID to require brackets + handedness = kb_info_json['split']['handedness'] + if 'matrix_grid' in handedness: + config_h_lines.append(generate_define('SPLIT_HAND_MATRIX_GRID', ', '.join(handedness['matrix_grid']))) + if 'protocol' in kb_info_json['split'].get('transport', {}): if kb_info_json['split']['transport']['protocol'] == 'i2c': config_h_lines.append(generate_define('USE_I2C')) diff --git a/lib/python/qmk/cli/generate/keyboard_c.py b/lib/python/qmk/cli/generate/keyboard_c.py index 325624c9cc..f8a2372cf3 100755 --- a/lib/python/qmk/cli/generate/keyboard_c.py +++ b/lib/python/qmk/cli/generate/keyboard_c.py @@ -63,13 +63,14 @@ def _gen_matrix_mask(info_data): cols = info_data['matrix_size']['cols'] rows = info_data['matrix_size']['rows'] - # Default mask to everything enabled - mask = [['1'] * cols for i in range(rows)] - - # Automatically mask out dip_switch.matrix_grid locations - matrix_grid = info_data.get('dip_switch', {}).get('matrix_grid', []) - for row, col in matrix_grid: - mask[row][col] = '0' + # Default mask to everything disabled + mask = [['0'] * cols for i in range(rows)] + + # Mirror layout macros squashed on top of each other + for layout_data in info_data['layouts'].values(): + for key_data in layout_data['layout']: + row, col = key_data['matrix'] + mask[row][col] = '1' lines = [] lines.append('#ifdef MATRIX_MASKED') diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index b56ec0fbff..fe829a724a 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -352,6 +352,14 @@ def _extract_secure_unlock(info_data, config_c): info_data['secure']['unlock_sequence'] = unlock_array +def _extract_split_handedness(info_data, config_c): + # Migrate + split = info_data.get('split', {}) + if 'matrix_grid' in split: + split['handedness'] = split.get('handedness', {}) + split['handedness']['matrix_grid'] = split.pop('matrix_grid') + + def _extract_split_transport(info_data, config_c): # Figure out the transport method if config_c.get('USE_I2C') is True: @@ -543,6 +551,7 @@ def _extract_config_h(info_data, config_c): _extract_matrix_info(info_data, config_c) _extract_audio(info_data, config_c) _extract_secure_unlock(info_data, config_c) + _extract_split_handedness(info_data, config_c) _extract_split_transport(info_data, config_c) _extract_split_right_pins(info_data, config_c) _extract_encoders(info_data, config_c) -- cgit v1.2.3 From fbbb221a31a4890a3302a4b169465d9efb0c59ed Mon Sep 17 00:00:00 2001 From: Joel Challis Date: Wed, 1 Nov 2023 01:26:24 +0000 Subject: Implement data driven lighting defaults (#21825) --- data/mappings/info_config.hjson | 16 ++++++++ data/schemas/keyboard.jsonschema | 43 ++++++++++++++++++++++ docs/reference_info_json.md | 65 ++++++++++++++++++++++++++++++++- lib/python/qmk/cli/generate/config_h.py | 13 ++++--- 4 files changed, 130 insertions(+), 7 deletions(-) (limited to 'lib/python') diff --git a/data/mappings/info_config.hjson b/data/mappings/info_config.hjson index 2ad0135950..26b437b513 100644 --- a/data/mappings/info_config.hjson +++ b/data/mappings/info_config.hjson @@ -29,6 +29,9 @@ "BACKLIGHT_PIN": {"info_key": "backlight.pin"}, "BACKLIGHT_PINS": {"info_key": "backlight.pins", "value_type": "array"}, "BREATHING_PERIOD": {"info_key": "backlight.breathing_period", "value_type": "int"}, + "BACKLIGHT_DEFAULT_ON": {"info_key": "backlight.default.on", "value_type": "bool"}, + "BACKLIGHT_DEFAULT_BREATHING": {"info_key": "backlight.default.breathing", "value_type": "bool"}, + "BACKLIGHT_DEFAULT_LEVEL": {"info_key": "backlight.default.brightness", "value_type": "int"}, // Bootmagic "BOOTMAGIC_LITE_COLUMN": {"info_key": "bootmagic.matrix.1", "value_type": "int"}, @@ -82,6 +85,9 @@ "LED_MATRIX_TIMEOUT": {"info_key": "led_matrix.timeout", "value_type": "int"}, "LED_MATRIX_VAL_STEP": {"info_key": "led_matrix.val_steps", "value_type": "int"}, "LED_MATRIX_LED_COUNT": {"info_key": "led_matrix.led_count", "value_type": "int", "to_json": false}, + "LED_MATRIX_DEFAULT_ON": {"info_key": "led_matrix.default.on", "value_type": "bool"}, + "LED_MATRIX_DEFAULT_VAL": {"info_key": "led_matrix.default.val", "value_type": "int"}, + "LED_MATRIX_DEFAULT_SPD": {"info_key": "led_matrix.default.speed", "value_type": "int"}, // Locking Switch "LOCKING_SUPPORT_ENABLE": {"info_key": "qmk.locking.enabled", "value_type": "bool"}, @@ -129,6 +135,11 @@ "RGB_MATRIX_TIMEOUT": {"info_key": "rgb_matrix.timeout", "value_type": "int"}, "RGB_MATRIX_VAL_STEP": {"info_key": "rgb_matrix.val_steps", "value_type": "int"}, "RGB_MATRIX_LED_COUNT": {"info_key": "rgb_matrix.led_count", "value_type": "int", "to_json": false}, + "RGB_MATRIX_DEFAULT_ON": {"info_key": "rgb_matrix.default.on", "value_type": "bool"}, + "RGB_MATRIX_DEFAULT_HUE": {"info_key": "rgb_matrix.default.hue", "value_type": "int"}, + "RGB_MATRIX_DEFAULT_SAT": {"info_key": "rgb_matrix.default.sat", "value_type": "int"}, + "RGB_MATRIX_DEFAULT_VAL": {"info_key": "rgb_matrix.default.val", "value_type": "int"}, + "RGB_MATRIX_DEFAULT_SPD": {"info_key": "rgb_matrix.default.speed", "value_type": "int"}, // RGBLight "RGBLED_NUM": {"info_key": "rgblight.led_count", "value_type": "int"}, @@ -144,6 +155,11 @@ "RGBLIGHT_SLEEP": {"info_key": "rgblight.sleep", "value_type": "bool"}, "RGBLIGHT_SPLIT": {"info_key": "rgblight.split", "value_type": "bool"}, "RGBLIGHT_VAL_STEP": {"info_key": "rgblight.brightness_steps", "value_type": "int"}, + "RGBLIGHT_DEFAULT_ON": {"info_key": "rgblight.default.on", "value_type": "bool"}, + "RGBLIGHT_DEFAULT_HUE": {"info_key": "rgblight.default.hue", "value_type": "int"}, + "RGBLIGHT_DEFAULT_SAT": {"info_key": "rgblight.default.sat", "value_type": "int"}, + "RGBLIGHT_DEFAULT_VAL": {"info_key": "rgblight.default.val", "value_type": "int"}, + "RGBLIGHT_DEFAULT_SPD": {"info_key": "rgblight.default.speed", "value_type": "int"}, "RGBW": {"info_key": "rgblight.rgbw", "value_type": "bool"}, // Secure diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index 188e0a5b3c..9fc455530c 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -136,6 +136,15 @@ "type": "string", "enum": ["pwm", "software", "timer", "custom"] }, + "default": { + "type": "object", + "additionalProperties": false, + "properties": { + "on": {"type": "boolean"}, + "breathing": {"type": "boolean"}, + "brightness": {"$ref": "qmk.definitions.v1#/unsigned_int_8"} + } + }, "breathing": {"type": "boolean"}, "breathing_period": {"$ref": "qmk.definitions.v1#/unsigned_int_8"}, "levels": { @@ -418,6 +427,16 @@ "type": "boolean" } }, + "default": { + "type": "object", + "additionalProperties": false, + "properties": { + "on": {"type": "boolean"}, + "animation": {"type": "string"}, + "val": {"$ref": "qmk.definitions.v1#/unsigned_int_8"}, + "speed": {"$ref": "qmk.definitions.v1#/unsigned_int_8"} + } + }, "driver": {"type": "string"}, "center_point": { "type": "array", @@ -472,6 +491,18 @@ "type": "boolean" } }, + "default": { + "type": "object", + "additionalProperties": false, + "properties": { + "on": {"type": "boolean"}, + "animation": {"type": "string"}, + "hue": {"$ref": "qmk.definitions.v1#/unsigned_int_8"}, + "sat": {"$ref": "qmk.definitions.v1#/unsigned_int_8"}, + "val": {"$ref": "qmk.definitions.v1#/unsigned_int_8"}, + "speed": {"$ref": "qmk.definitions.v1#/unsigned_int_8"} + } + }, "driver": {"type": "string"}, "center_point": { "type": "array", @@ -530,6 +561,18 @@ } }, "brightness_steps": {"$ref": "qmk.definitions.v1#/unsigned_int"}, + "default": { + "type": "object", + "additionalProperties": false, + "properties": { + "on": {"type": "boolean"}, + "animation": {"type": "string"}, + "hue": {"$ref": "qmk.definitions.v1#/unsigned_int_8"}, + "sat": {"$ref": "qmk.definitions.v1#/unsigned_int_8"}, + "val": {"$ref": "qmk.definitions.v1#/unsigned_int_8"}, + "speed": {"$ref": "qmk.definitions.v1#/unsigned_int_8"} + } + }, "driver": { "type": "string", "enum": ["apa102", "custom", "ws2812"] diff --git a/docs/reference_info_json.md b/docs/reference_info_json.md index d9336fa2bb..4a70a4bb6f 100644 --- a/docs/reference_info_json.md +++ b/docs/reference_info_json.md @@ -134,6 +134,16 @@ Configures the [Backlight](feature_backlight.md) feature. * `breathing_period` * The length of one backlight breathing cycle in seconds. * Default: `6` (6 seconds) + * `default` + * `on` + * The default backlight enabled state. + * Default: `true` + * `breathing` + * The default backlight breathing state. + * Default: `false` + * `brightness` + * The default brightness level. + * Default: `max_brightness` * `driver` * The driver to use. Must be one of `custom`, `pwm`, `software`, `timer`. * Default: `"pwm"` @@ -341,7 +351,7 @@ Configures the [LED Matrix](feature_led_matrix.md) feature. * Example: ```json { - "alpha_mods": true, + "alphas_mods": true, "breathing": true, "cycle_left_right": false } @@ -349,6 +359,19 @@ Configures the [LED Matrix](feature_led_matrix.md) feature. * `center_point` * The centroid (geometric center) of the LEDs. Used for certain effects. * Default: `[112, 32]` + * `default` + * `animation` + * The default effect. Must be one of `led_matrix.animations` + * Default: `"solid"` + * `on` + * The default enabled state. + * Default: `true` + * `val` + * The default brightness level. + * Default: `max_brightness` + * `speed` + * The default animation speed. + * Default: `128` * `driver` (Required) * The driver to use. Must be one of `custom`, `is31fl3218`, `is31fl3731`, `is31fl3733`, `is31fl3736`, `is31fl3737`, `is31fl3741`, `is31fl3742a`, `is31fl3743a`, `is31fl3745`, `is31fl3746a`, `snled27351`. * `layout` (Required) @@ -506,6 +529,25 @@ Configures the [RGB Lighting](feature_rgblight.md) feature. * `brightness_steps` * The number of brightness adjustment steps. * Default: `17` + * `default` + * `animation` + * The default effect. Must be one of `rgblight.animations` + * Default: `"static_light"` + * `on` + * The default enabled state. + * Default: `true` + * `hue` + * The default hue value. + * Default: `0` + * `sat` + * The default saturation value. + * Default: `255` + * `val` + * The default brightness level. + * Default: `max_brightness` + * `speed` + * The default animation speed. + * Default: `0` * `driver` * The driver to use. Must be one of `apa102`, `custom`, `ws2812`. * Default: `"ws2812"` @@ -554,7 +596,7 @@ Configures the [RGB Matrix](feature_rgb_matrix.md) feature. * Example: ```json { - "alpha_mods": true, + "alphas_mods": true, "breathing": true, "cycle_left_right": false } @@ -562,6 +604,25 @@ Configures the [RGB Matrix](feature_rgb_matrix.md) feature. * `center_point` * The centroid (geometric center) of the LEDs. Used for certain effects. * Default: `[112, 32]` + * `default` + * `animation` + * The default effect. Must be one of `rgb_matrix.animations` + * Default: `"solid_color"` + * `on` + * The default enabled state. + * Default: `true` + * `hue` + * The default hue value. + * Default: `0` + * `sat` + * The default saturation value. + * Default: `255` + * `val` + * The default brightness level. + * Default: `max_brightness` + * `speed` + * The default animation speed. + * Default: `128` * `driver` (Required) * The driver to use. Must be one of `aw20216s`, `custom`, `is31fl3218`, `is31fl3731`, `is31fl3733`, `is31fl3736`, `is31fl3737`, `is31fl3741`, `is31fl3742a`, `is31fl3743a`, `is31fl3745`, `is31fl3746a`, `snled27351`, `ws2812`. * `hue_steps` diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py index 2c624e3e9a..00fb1d9585 100755 --- a/lib/python/qmk/cli/generate/config_h.py +++ b/lib/python/qmk/cli/generate/config_h.py @@ -165,10 +165,13 @@ def generate_split_config(kb_info_json, config_h_lines): generate_encoder_config(kb_info_json['split']['encoder']['right'], config_h_lines, '_RIGHT') -def generate_led_animations_config(led_feature_json, config_h_lines, prefix): +def generate_led_animations_config(feature, led_feature_json, config_h_lines, enable_prefix, animation_prefix): + if 'animation' in led_feature_json.get('default', {}): + config_h_lines.append(generate_define(f'{feature.upper()}_DEFAULT_MODE', f'{animation_prefix}{led_feature_json["default"]["animation"].upper()}')) + for animation in led_feature_json.get('animations', {}): if led_feature_json['animations'][animation]: - config_h_lines.append(generate_define(f'{prefix}{animation.upper()}')) + config_h_lines.append(generate_define(f'{enable_prefix}{animation.upper()}')) @cli.argument('filename', nargs='?', arg_only=True, type=FileType('r'), completer=FilesCompleter('.json'), help='A configurator export JSON to be compiled and flashed or a pre-compiled binary firmware file (bin/hex) to be flashed.') @@ -209,13 +212,13 @@ def generate_config_h(cli): generate_split_config(kb_info_json, config_h_lines) if 'led_matrix' in kb_info_json: - generate_led_animations_config(kb_info_json['led_matrix'], config_h_lines, 'ENABLE_LED_MATRIX_') + generate_led_animations_config('led_matrix', kb_info_json['led_matrix'], config_h_lines, 'ENABLE_LED_MATRIX_', 'LED_MATRIX_') if 'rgb_matrix' in kb_info_json: - generate_led_animations_config(kb_info_json['rgb_matrix'], config_h_lines, 'ENABLE_RGB_MATRIX_') + generate_led_animations_config('rgb_matrix', kb_info_json['rgb_matrix'], config_h_lines, 'ENABLE_RGB_MATRIX_', 'RGB_MATRIX_') if 'rgblight' in kb_info_json: - generate_led_animations_config(kb_info_json['rgblight'], config_h_lines, 'RGBLIGHT_EFFECT_') + generate_led_animations_config('rgblight', kb_info_json['rgblight'], config_h_lines, 'RGBLIGHT_EFFECT_', 'RGBLIGHT_MODE_') # Show the results dump_lines(cli.args.output, config_h_lines, cli.args.quiet) -- cgit v1.2.3 From b31426252ed379937132ed7d2baf84a677123b5b Mon Sep 17 00:00:00 2001 From: Joel Challis Date: Wed, 1 Nov 2023 02:11:42 +0000 Subject: Generate switch statement helpers for keycode ranges (#20059) --- lib/python/qmk/cli/generate/keycodes.py | 8 ++++++++ quantum/keycodes.h | 22 ++++++++++++++++++++++ quantum/keymap_common.c | 10 +++++----- 3 files changed, 35 insertions(+), 5 deletions(-) (limited to 'lib/python') diff --git a/lib/python/qmk/cli/generate/keycodes.py b/lib/python/qmk/cli/generate/keycodes.py index ed8b6827bd..719fced5d5 100644 --- a/lib/python/qmk/cli/generate/keycodes.py +++ b/lib/python/qmk/cli/generate/keycodes.py @@ -94,6 +94,14 @@ def _generate_helpers(lines, keycodes): hi = keycodes["keycodes"][f'0x{codes[1]:04X}']['key'] lines.append(f'#define IS_{ _translate_group(group).upper() }_KEYCODE(code) ((code) >= {lo} && (code) <= {hi})') + lines.append('') + lines.append('// Switch statement Helpers') + for group, codes in temp.items(): + lo = keycodes["keycodes"][f'0x{codes[0]:04X}']['key'] + hi = keycodes["keycodes"][f'0x{codes[1]:04X}']['key'] + name = f'{ _translate_group(group).upper() }_KEYCODE_RANGE' + lines.append(f'#define { name.ljust(35) } {lo} ... {hi}') + def _generate_aliases(lines, keycodes): # Work around ChibiOS ch.h include guard diff --git a/quantum/keycodes.h b/quantum/keycodes.h index bbf10da36d..39fd2e2726 100644 --- a/quantum/keycodes.h +++ b/quantum/keycodes.h @@ -1420,3 +1420,25 @@ enum qk_keycode_defines { #define IS_QUANTUM_KEYCODE(code) ((code) >= QK_BOOTLOADER && (code) <= QK_ALT_REPEAT_KEY) #define IS_KB_KEYCODE(code) ((code) >= QK_KB_0 && (code) <= QK_KB_31) #define IS_USER_KEYCODE(code) ((code) >= QK_USER_0 && (code) <= QK_USER_31) + +// Switch statement Helpers +#define INTERNAL_KEYCODE_RANGE KC_NO ... KC_TRANSPARENT +#define BASIC_KEYCODE_RANGE KC_A ... KC_EXSEL +#define SYSTEM_KEYCODE_RANGE KC_SYSTEM_POWER ... KC_SYSTEM_WAKE +#define CONSUMER_KEYCODE_RANGE KC_AUDIO_MUTE ... KC_LAUNCHPAD +#define MOUSE_KEYCODE_RANGE KC_MS_UP ... KC_MS_ACCEL2 +#define MODIFIER_KEYCODE_RANGE KC_LEFT_CTRL ... KC_RIGHT_GUI +#define SWAP_HANDS_KEYCODE_RANGE QK_SWAP_HANDS_TOGGLE ... QK_SWAP_HANDS_ONE_SHOT +#define MAGIC_KEYCODE_RANGE QK_MAGIC_SWAP_CONTROL_CAPS_LOCK ... QK_MAGIC_TOGGLE_ESCAPE_CAPS_LOCK +#define MIDI_KEYCODE_RANGE QK_MIDI_ON ... QK_MIDI_PITCH_BEND_UP +#define SEQUENCER_KEYCODE_RANGE QK_SEQUENCER_ON ... QK_SEQUENCER_STEPS_CLEAR +#define JOYSTICK_KEYCODE_RANGE QK_JOYSTICK_BUTTON_0 ... QK_JOYSTICK_BUTTON_31 +#define PROGRAMMABLE_BUTTON_KEYCODE_RANGE QK_PROGRAMMABLE_BUTTON_1 ... QK_PROGRAMMABLE_BUTTON_32 +#define AUDIO_KEYCODE_RANGE QK_AUDIO_ON ... QK_AUDIO_VOICE_PREVIOUS +#define STENO_KEYCODE_RANGE QK_STENO_BOLT ... QK_STENO_COMB_MAX +#define MACRO_KEYCODE_RANGE QK_MACRO_0 ... QK_MACRO_31 +#define BACKLIGHT_KEYCODE_RANGE QK_BACKLIGHT_ON ... QK_BACKLIGHT_TOGGLE_BREATHING +#define RGB_KEYCODE_RANGE RGB_TOG ... RGB_MODE_TWINKLE +#define QUANTUM_KEYCODE_RANGE QK_BOOTLOADER ... QK_ALT_REPEAT_KEY +#define KB_KEYCODE_RANGE QK_KB_0 ... QK_KB_31 +#define USER_KEYCODE_RANGE QK_USER_0 ... QK_USER_31 diff --git a/quantum/keymap_common.c b/quantum/keymap_common.c index 9a67fad278..91e47a72ee 100644 --- a/quantum/keymap_common.c +++ b/quantum/keymap_common.c @@ -59,19 +59,19 @@ action_t action_for_keycode(uint16_t keycode) { (void)mod; switch (keycode) { - case KC_A ... KC_EXSEL: - case KC_LEFT_CTRL ... KC_RIGHT_GUI: + case BASIC_KEYCODE_RANGE: + case MODIFIER_KEYCODE_RANGE: action.code = ACTION_KEY(keycode); break; #ifdef EXTRAKEY_ENABLE - case KC_SYSTEM_POWER ... KC_SYSTEM_WAKE: + case SYSTEM_KEYCODE_RANGE: action.code = ACTION_USAGE_SYSTEM(KEYCODE2SYSTEM(keycode)); break; - case KC_AUDIO_MUTE ... KC_LAUNCHPAD: + case CONSUMER_KEYCODE_RANGE: action.code = ACTION_USAGE_CONSUMER(KEYCODE2CONSUMER(keycode)); break; #endif - case KC_MS_UP ... KC_MS_ACCEL2: + case MOUSE_KEYCODE_RANGE: action.code = ACTION_MOUSEKEY(keycode); break; case KC_TRANSPARENT: -- cgit v1.2.3 From 8ea955c72ff9c2888a08c9b6b2f5dda8b49eb856 Mon Sep 17 00:00:00 2001 From: Joel Challis Date: Wed, 1 Nov 2023 22:37:05 +0000 Subject: Improve argument handling of c2json (#22170) * Improve argument handling of c2json * Add automagic --- lib/python/qmk/cli/c2json.py | 59 +++++++++++++++++++++++++------------------- lib/python/qmk/keymap.py | 23 +++++++++-------- lib/python/qmk/path.py | 6 ++--- 3 files changed, 46 insertions(+), 42 deletions(-) (limited to 'lib/python') diff --git a/lib/python/qmk/cli/c2json.py b/lib/python/qmk/cli/c2json.py index 7f6aca070a..f7f1f2ffba 100644 --- a/lib/python/qmk/cli/c2json.py +++ b/lib/python/qmk/cli/c2json.py @@ -1,45 +1,57 @@ """Generate a keymap.json from a keymap.c file. """ +import re import json from argcomplete.completers import FilesCompleter from milc import cli -import qmk.keymap import qmk.path from qmk.json_encoders import InfoJSONEncoder +from qmk.decorators import automagic_keyboard, automagic_keymap from qmk.keyboard import keyboard_completer, keyboard_folder +from qmk.keymap import locate_keymap, find_keymap_from_dir, generate_json, c2json as c2json_impl from qmk.errors import CppError +from qmk.commands import dump_lines @cli.argument('--no-cpp', arg_only=True, action='store_false', help='Do not use \'cpp\' on keymap.c') @cli.argument('-o', '--output', arg_only=True, type=qmk.path.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('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, required=True, help='The keyboard\'s name') -@cli.argument('-km', '--keymap', arg_only=True, required=True, help='The keymap\'s name') -@cli.argument('filename', arg_only=True, completer=FilesCompleter('.c'), help='keymap.c file') +@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='The keyboard\'s name') +@cli.argument('-km', '--keymap', help='The keymap\'s name') +@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.c'), help='keymap.c file') @cli.subcommand('Creates a keymap.json from a keymap.c file.') +@automagic_keyboard +@automagic_keymap def c2json(cli): """Generate a keymap.json from a keymap.c file. This command uses the `qmk.keymap` module to generate a keymap.json from a keymap.c file. The generated keymap is written to stdout, or to a file if -o is provided. """ - if cli.args.filename != '-': - cli.args.filename = qmk.path.normpath(cli.args.filename) + filename = cli.args.filename + keyboard = cli.config.c2json.keyboard + keymap = cli.config.c2json.keymap - # Error checking - if not cli.args.filename.exists(): - cli.log.error('C file does not exist!') - cli.print_usage() - return False + if filename: + if not keyboard and not keymap: + # fallback to inferring keyboard/keymap from path + (keymap, found_type) = find_keymap_from_dir(filename) + if found_type == 'keymap_directory': + keyboard = re.search(fr"keyboards/(.+)/keymaps/{keymap}/.*", filename.as_posix()).group(1) - # Environment processing - if cli.args.output == ('-'): - cli.args.output = None + elif keyboard and keymap: + if not filename: + # fallback to inferring keyboard/keymap from path + filename = locate_keymap(keyboard, keymap) + + if not all((filename, keyboard, keymap)): + cli.log.error('You must supply keyboard and keymap, a path to a keymap.c within qmk_firmware, or absolute filename and keyboard and keymap') + cli.print_help() + return False - # Parse the keymap.c try: - keymap_json = qmk.keymap.c2json(cli.args.keyboard, cli.args.keymap, cli.args.filename, use_cpp=cli.args.no_cpp) + keymap_json = c2json_impl(keyboard, keymap, filename, use_cpp=cli.args.no_cpp) except CppError as e: if cli.config.general.verbose: cli.log.debug('The C pre-processor ran into a fatal error: %s', e) @@ -48,19 +60,14 @@ def c2json(cli): # Generate the keymap.json try: - keymap_json = qmk.keymap.generate_json(keymap_json['keymap'], keymap_json['keyboard'], keymap_json['layout'], keymap_json['layers']) + keymap_json = generate_json(keymap_json['keymap'], keymap_json['keyboard'], keymap_json['layout'], keymap_json['layers']) except KeyError: cli.log.error('Something went wrong. Try to use --no-cpp.') return False if cli.args.output: - cli.args.output.parent.mkdir(parents=True, exist_ok=True) - if cli.args.output.exists(): - cli.args.output.replace(cli.args.output.parent / (cli.args.output.name + '.bak')) - cli.args.output.write_text(json.dumps(keymap_json, cls=InfoJSONEncoder, sort_keys=True)) - - if not cli.args.quiet: - cli.log.info('Wrote keymap to %s.', cli.args.output) - + keymap_lines = [json.dumps(keymap_json, cls=InfoJSONEncoder, sort_keys=True)] else: - print(json.dumps(keymap_json)) + keymap_lines = [json.dumps(keymap_json)] + + dump_lines(cli.args.output, keymap_lines, cli.args.quiet) diff --git a/lib/python/qmk/keymap.py b/lib/python/qmk/keymap.py index 11e8d39dad..281c53cfda 100644 --- a/lib/python/qmk/keymap.py +++ b/lib/python/qmk/keymap.py @@ -191,16 +191,15 @@ def _strip_any(keycode): return keycode -def find_keymap_from_dir(): - """Returns `(keymap_name, source)` for the directory we're currently in. - +def find_keymap_from_dir(*args): + """Returns `(keymap_name, source)` for the directory provided (or cwd if not specified). """ - relative_cwd = qmk.path.under_qmk_firmware() + relative_path = qmk.path.under_qmk_firmware(*args) - if relative_cwd and len(relative_cwd.parts) > 1: + if relative_path and len(relative_path.parts) > 1: # If we're in `qmk_firmware/keyboards` and `keymaps` is in our path, try to find the keyboard name. - if relative_cwd.parts[0] == 'keyboards' and 'keymaps' in relative_cwd.parts: - current_path = Path('/'.join(relative_cwd.parts[1:])) # Strip 'keyboards' from the front + if relative_path.parts[0] == 'keyboards' and 'keymaps' in relative_path.parts: + current_path = Path('/'.join(relative_path.parts[1:])) # Strip 'keyboards' from the front if 'keymaps' in current_path.parts and current_path.name != 'keymaps': while current_path.parent.name != 'keymaps': @@ -209,13 +208,13 @@ def find_keymap_from_dir(): return current_path.name, 'keymap_directory' # If we're in `qmk_firmware/layouts` guess the name from the community keymap they're in - elif relative_cwd.parts[0] == 'layouts' and is_keymap_dir(relative_cwd): - return relative_cwd.name, 'layouts_directory' + elif relative_path.parts[0] == 'layouts' and is_keymap_dir(relative_path): + return relative_path.name, 'layouts_directory' # If we're in `qmk_firmware/users` guess the name from the userspace they're in - elif relative_cwd.parts[0] == 'users': + elif relative_path.parts[0] == 'users': # Guess the keymap name based on which userspace they're in - return relative_cwd.parts[1], 'users_directory' + return relative_path.parts[1], 'users_directory' return None, None @@ -657,7 +656,7 @@ def parse_keymap_c(keymap_file, use_cpp=True): Returns: a dictionary containing the parsed keymap """ - if keymap_file == '-': + if not isinstance(keymap_file, (Path, str)) or keymap_file == '-': if use_cpp: keymap_file = _c_preprocess(None, sys.stdin) else: diff --git a/lib/python/qmk/path.py b/lib/python/qmk/path.py index 9d248451b8..94582a05e0 100644 --- a/lib/python/qmk/path.py +++ b/lib/python/qmk/path.py @@ -19,13 +19,11 @@ def is_keyboard(keyboard_name): return rules_mk.exists() -def under_qmk_firmware(): +def under_qmk_firmware(path=Path(os.environ['ORIG_CWD'])): """Returns a Path object representing the relative path under qmk_firmware, or None. """ - cwd = Path(os.environ['ORIG_CWD']) - try: - return cwd.relative_to(QMK_FIRMWARE) + return path.relative_to(QMK_FIRMWARE) except ValueError: return None -- cgit v1.2.3 From 49382107115f611a61f1f5e20a3b2a92000a35da Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Wed, 15 Nov 2023 16:24:54 +1100 Subject: CLI refactoring for common build target APIs (#22221) --- lib/python/qmk/build_targets.py | 211 +++++++++++++++++++++ lib/python/qmk/cli/clean.py | 4 +- lib/python/qmk/cli/compile.py | 58 ++---- lib/python/qmk/cli/find.py | 14 +- lib/python/qmk/cli/flash.py | 45 ++--- .../qmk/cli/generate/compilation_database.py | 16 +- lib/python/qmk/cli/mass_compile.py | 29 ++- lib/python/qmk/commands.py | 164 +--------------- lib/python/qmk/keyboard.py | 6 +- lib/python/qmk/search.py | 34 ++-- 10 files changed, 296 insertions(+), 285 deletions(-) create mode 100644 lib/python/qmk/build_targets.py (limited to 'lib/python') diff --git a/lib/python/qmk/build_targets.py b/lib/python/qmk/build_targets.py new file mode 100644 index 0000000000..236c2eaa03 --- /dev/null +++ b/lib/python/qmk/build_targets.py @@ -0,0 +1,211 @@ +# Copyright 2023 Nick Brassel (@tzarc) +# SPDX-License-Identifier: GPL-2.0-or-later +import json +import shutil +from typing import List, Union +from pathlib import Path +from dotty_dict import dotty, Dotty +from milc import cli +from qmk.constants import QMK_FIRMWARE, INTERMEDIATE_OUTPUT_PREFIX +from qmk.commands import find_make, get_make_parallel_args, parse_configurator_json +from qmk.keyboard import keyboard_folder +from qmk.info import keymap_json +from qmk.cli.generate.compilation_database import write_compilation_database + + +class BuildTarget: + def __init__(self, keyboard: str, keymap: str, json: Union[dict, Dotty] = None): + self._keyboard = keyboard_folder(keyboard) + self._keyboard_safe = self._keyboard.replace('/', '_') + self._keymap = keymap + self._parallel = 1 + self._clean = False + self._compiledb = False + self._target = f'{self._keyboard_safe}_{self.keymap}' + self._intermediate_output = Path(f'{INTERMEDIATE_OUTPUT_PREFIX}{self._target}') + self._generated_files_path = self._intermediate_output / 'src' + self._json = json.to_dict() if isinstance(json, Dotty) else json + + def __str__(self): + return f'{self.keyboard}:{self.keymap}' + + def __repr__(self): + return f'BuildTarget(keyboard={self.keyboard}, keymap={self.keymap})' + + def configure(self, parallel: int = None, clean: bool = None, compiledb: bool = None) -> None: + if parallel is not None: + self._parallel = parallel + if clean is not None: + self._clean = clean + if compiledb is not None: + self._compiledb = compiledb + + @property + def keyboard(self) -> str: + return self._keyboard + + @property + def keymap(self) -> str: + return self._keymap + + @property + def json(self) -> dict: + if not self._json: + self._load_json() + if not self._json: + return {} + return self._json + + @property + def dotty(self) -> Dotty: + return dotty(self.json) + + def _common_make_args(self, dry_run: bool = False, build_target: str = None): + compile_args = [ + find_make(), + *get_make_parallel_args(self._parallel), + '-r', + '-R', + '-f', + 'builddefs/build_keyboard.mk', + ] + + if not cli.config.general.verbose: + compile_args.append('-s') + + verbose = 'true' if cli.config.general.verbose else 'false' + color = 'true' if cli.config.general.color else 'false' + + if dry_run: + compile_args.append('-n') + + if build_target: + compile_args.append(build_target) + + compile_args.extend([ + f'KEYBOARD={self.keyboard}', + f'KEYMAP={self.keymap}', + f'KEYBOARD_FILESAFE={self._keyboard_safe}', + f'TARGET={self._target}', + f'INTERMEDIATE_OUTPUT={self._intermediate_output}', + f'VERBOSE={verbose}', + f'COLOR={color}', + 'SILENT=false', + 'QMK_BIN="qmk"', + ]) + + return compile_args + + def prepare_build(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None: + raise NotImplementedError("prepare_build() not implemented in base class") + + def compile_command(self, build_target: str = None, dry_run: bool = False, **env_vars) -> List[str]: + raise NotImplementedError("compile_command() not implemented in base class") + + def generate_compilation_database(self, build_target: str = None, skip_clean: bool = False, **env_vars) -> None: + self.prepare_build(build_target=build_target, **env_vars) + command = self.compile_command(build_target=build_target, dry_run=True, **env_vars) + write_compilation_database(command=command, output_path=QMK_FIRMWARE / 'compile_commands.json', skip_clean=skip_clean, **env_vars) + + def compile(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None: + if self._clean or self._compiledb: + command = [find_make(), "clean"] + if dry_run: + command.append('-n') + cli.log.info('Cleaning with {fg_cyan}%s', ' '.join(command)) + cli.run(command, capture_output=False) + + if self._compiledb and not dry_run: + self.generate_compilation_database(build_target=build_target, skip_clean=True, **env_vars) + + self.prepare_build(build_target=build_target, dry_run=dry_run, **env_vars) + command = self.compile_command(build_target=build_target, **env_vars) + cli.log.info('Compiling keymap with {fg_cyan}%s', ' '.join(command)) + if not dry_run: + cli.echo('\n') + ret = cli.run(command, capture_output=False) + if ret.returncode: + return ret.returncode + + +class KeyboardKeymapBuildTarget(BuildTarget): + def __init__(self, keyboard: str, keymap: str, json: dict = None): + super().__init__(keyboard=keyboard, keymap=keymap, json=json) + + def __repr__(self): + return f'KeyboardKeymapTarget(keyboard={self.keyboard}, keymap={self.keymap})' + + def _load_json(self): + self._json = keymap_json(self.keyboard, self.keymap) + + def prepare_build(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None: + pass + + def compile_command(self, build_target: str = None, dry_run: bool = False, **env_vars) -> List[str]: + compile_args = self._common_make_args(dry_run=dry_run, build_target=build_target) + + for key, value in env_vars.items(): + compile_args.append(f'{key}={value}') + + return compile_args + + +class JsonKeymapBuildTarget(BuildTarget): + def __init__(self, json_path): + if isinstance(json_path, Path): + self.json_path = json_path + else: + self.json_path = None + + json = parse_configurator_json(json_path) # Will load from stdin if provided + + # In case the user passes a keymap.json from a keymap directory directly to the CLI. + # e.g.: qmk compile - < keyboards/clueboard/california/keymaps/default/keymap.json + json["keymap"] = json.get("keymap", "default_json") + + super().__init__(keyboard=json['keyboard'], keymap=json['keymap'], json=json) + + self._keymap_json = self._generated_files_path / 'keymap.json' + + def __repr__(self): + return f'JsonKeymapTarget(keyboard={self.keyboard}, keymap={self.keymap}, path={self.json_path})' + + def _load_json(self): + pass # Already loaded in constructor + + def prepare_build(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None: + if self._clean: + if self._intermediate_output.exists(): + shutil.rmtree(self._intermediate_output) + + # begin with making the deepest folder in the tree + self._generated_files_path.mkdir(exist_ok=True, parents=True) + + # Compare minified to ensure consistent comparison + new_content = json.dumps(self.json, separators=(',', ':')) + if self._keymap_json.exists(): + old_content = json.dumps(json.loads(self._keymap_json.read_text(encoding='utf-8')), separators=(',', ':')) + if old_content == new_content: + new_content = None + + # Write the keymap.json file if different so timestamps are only updated + # if the content changes -- running `make` won't treat it as modified. + if new_content: + self._keymap_json.write_text(new_content, encoding='utf-8') + + def compile_command(self, build_target: str = None, dry_run: bool = False, **env_vars) -> List[str]: + compile_args = self._common_make_args(dry_run=dry_run, build_target=build_target) + compile_args.extend([ + f'MAIN_KEYMAP_PATH_1={self._intermediate_output}', + f'MAIN_KEYMAP_PATH_2={self._intermediate_output}', + f'MAIN_KEYMAP_PATH_3={self._intermediate_output}', + f'MAIN_KEYMAP_PATH_4={self._intermediate_output}', + f'MAIN_KEYMAP_PATH_5={self._intermediate_output}', + f'KEYMAP_JSON={self._keymap_json}', + f'KEYMAP_PATH={self._generated_files_path}', + ]) + + for key, value in env_vars.items(): + compile_args.append(f'{key}={value}') + + return compile_args diff --git a/lib/python/qmk/cli/clean.py b/lib/python/qmk/cli/clean.py index 72b7ffe810..bdec01e4b6 100644 --- a/lib/python/qmk/cli/clean.py +++ b/lib/python/qmk/cli/clean.py @@ -2,7 +2,7 @@ """ from subprocess import DEVNULL -from qmk.commands import create_make_target +from qmk.commands import find_make from milc import cli @@ -11,4 +11,4 @@ from milc import cli def clean(cli): """Runs `make clean` (or `make distclean` if --all is passed) """ - cli.run(create_make_target('distclean' if cli.args.all else 'clean'), capture_output=False, stdin=DEVNULL) + cli.run([find_make(), 'distclean' if cli.args.all else 'clean'], capture_output=False, stdin=DEVNULL) diff --git a/lib/python/qmk/cli/compile.py b/lib/python/qmk/cli/compile.py index 2478ea1fed..71c1dec162 100755 --- a/lib/python/qmk/cli/compile.py +++ b/lib/python/qmk/cli/compile.py @@ -7,22 +7,11 @@ from argcomplete.completers import FilesCompleter from milc import cli import qmk.path -from qmk.constants import QMK_FIRMWARE from qmk.decorators import automagic_keyboard, automagic_keymap -from qmk.commands import compile_configurator_json, create_make_command, parse_configurator_json, build_environment +from qmk.commands import build_environment from qmk.keyboard import keyboard_completer, keyboard_folder_or_all, is_all_keyboards from qmk.keymap import keymap_completer, locate_keymap -from qmk.cli.generate.compilation_database import write_compilation_database - - -def _is_keymap_target(keyboard, keymap): - if keymap == 'all': - return True - - if locate_keymap(keyboard, keymap): - return True - - return False +from qmk.build_targets import KeyboardKeymapBuildTarget, JsonKeymapBuildTarget @cli.argument('filename', nargs='?', arg_only=True, type=qmk.path.FileType('r'), completer=FilesCompleter('.json'), help='The configurator export to compile') @@ -32,6 +21,7 @@ def _is_keymap_target(keyboard, keymap): @cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.") @cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.") @cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.") +@cli.argument('-t', '--target', type=str, default=None, help="Intended alternative build target, such as `production` in `make planck/rev4:default:production`.") @cli.argument('--compiledb', arg_only=True, action='store_true', help="Generates the clang compile_commands.json file during build. Implies --clean.") @cli.subcommand('Compile a QMK Firmware.') @automagic_keyboard @@ -53,47 +43,27 @@ def compile(cli): # Build the environment vars envs = build_environment(cli.args.env) - # Determine the compile command - commands = [] - - current_keyboard = None - current_keymap = None + # Handler for the build target + target = None if cli.args.filename: - # If a configurator JSON was provided generate a keymap and compile it - user_keymap = parse_configurator_json(cli.args.filename) - commands = [compile_configurator_json(user_keymap, parallel=cli.config.compile.parallel, clean=cli.args.clean, **envs)] + # if we were given a filename, assume we have a json build target + target = JsonKeymapBuildTarget(cli.args.filename) elif cli.config.compile.keyboard and cli.config.compile.keymap: - # Generate the make command for a specific keyboard/keymap. - if not _is_keymap_target(cli.config.compile.keyboard, cli.config.compile.keymap): + # if we got a keyboard and keymap, attempt to find it + if not locate_keymap(cli.config.compile.keyboard, cli.config.compile.keymap): cli.log.error('Invalid keymap argument.') cli.print_help() return False - if cli.args.clean: - commands.append(create_make_command(cli.config.compile.keyboard, cli.config.compile.keymap, 'clean', **envs)) - commands.append(create_make_command(cli.config.compile.keyboard, cli.config.compile.keymap, parallel=cli.config.compile.parallel, **envs)) + # If we got here, then we have a valid keyboard and keymap for a build target + target = KeyboardKeymapBuildTarget(cli.config.compile.keyboard, cli.config.compile.keymap) - current_keyboard = cli.config.compile.keyboard - current_keymap = cli.config.compile.keymap - - if not commands: + if not target: cli.log.error('You must supply a configurator export, both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.') cli.print_help() return False - if cli.args.compiledb: - if current_keyboard is None or current_keymap is None: - cli.log.error('You must supply both `--keyboard` and `--keymap` or be in a directory with a keymap to generate a compile_commands.json file.') - cli.print_help() - return False - write_compilation_database(current_keyboard, current_keymap, QMK_FIRMWARE / 'compile_commands.json') - - cli.log.info('Compiling keymap with {fg_cyan}%s', ' '.join(commands[-1])) - if not cli.args.dry_run: - cli.echo('\n') - for command in commands: - ret = cli.run(command, capture_output=False) - if ret.returncode: - return ret.returncode + target.configure(parallel=cli.config.compile.parallel, clean=cli.args.clean, compiledb=cli.args.compiledb) + target.compile(cli.args.target, dry_run=cli.args.dry_run, **envs) diff --git a/lib/python/qmk/cli/find.py b/lib/python/qmk/cli/find.py index f2135bbc16..55a0530092 100644 --- a/lib/python/qmk/cli/find.py +++ b/lib/python/qmk/cli/find.py @@ -19,13 +19,9 @@ from qmk.search import search_keymap_targets def find(cli): """Search through all keyboards and keymaps for a given search criteria. """ + targets = search_keymap_targets([('all', cli.config.find.keymap)], cli.args.filter) + for target in sorted(targets, key=lambda t: (t.keyboard, t.keymap)): + print(f'{target}') - if len(cli.args.filter) == 0 and len(cli.args.print) > 0: - cli.log.warning('No filters supplied -- keymaps not parsed, unable to print requested values.') - - targets = search_keymap_targets([('all', cli.config.find.keymap)], cli.args.filter, cli.args.print) - for keyboard, keymap, print_vals in targets: - print(f'{keyboard}:{keymap}') - - for key, val in print_vals: - print(f' {key}={val}') + for key in cli.args.print: + print(f' {key}={target.dotty.get(key, None)}') diff --git a/lib/python/qmk/cli/flash.py b/lib/python/qmk/cli/flash.py index 8724f26889..5df31e4781 100644 --- a/lib/python/qmk/cli/flash.py +++ b/lib/python/qmk/cli/flash.py @@ -4,25 +4,17 @@ You can compile a keymap already in the repo or using a QMK Configurator export. A bootloader must be specified. """ from argcomplete.completers import FilesCompleter +from pathlib import Path from milc import cli import qmk.path from qmk.decorators import automagic_keyboard, automagic_keymap -from qmk.commands import compile_configurator_json, create_make_command, parse_configurator_json, build_environment +from qmk.commands import build_environment from qmk.keyboard import keyboard_completer, keyboard_folder from qmk.keymap import keymap_completer, locate_keymap from qmk.flashers import flasher - - -def _is_keymap_target(keyboard, keymap): - if keymap == 'all': - return True - - if locate_keymap(keyboard, keymap): - return True - - return False +from qmk.build_targets import KeyboardKeymapBuildTarget, JsonKeymapBuildTarget def _list_bootloaders(): @@ -89,7 +81,7 @@ def flash(cli): If bootloader is omitted the make system will use the configured bootloader for that keyboard. """ - if cli.args.filename and cli.args.filename.suffix in ['.bin', '.hex', '.uf2']: + if cli.args.filename and isinstance(cli.args.filename, Path) and cli.args.filename.suffix in ['.bin', '.hex', '.uf2']: return _flash_binary(cli.args.filename, cli.args.mcu) if cli.args.bootloaders: @@ -98,34 +90,27 @@ def flash(cli): # Build the environment vars envs = build_environment(cli.args.env) - # Determine the compile command - commands = [] + # Handler for the build target + target = None if cli.args.filename: - # If a configurator JSON was provided generate a keymap and compile it - user_keymap = parse_configurator_json(cli.args.filename) - commands = [compile_configurator_json(user_keymap, cli.args.bootloader, parallel=cli.config.flash.parallel, clean=cli.args.clean, **envs)] + # if we were given a filename, assume we have a json build target + target = JsonKeymapBuildTarget(cli.args.filename) elif cli.config.flash.keyboard and cli.config.flash.keymap: - # Generate the make command for a specific keyboard/keymap. - if not _is_keymap_target(cli.config.flash.keyboard, cli.config.flash.keymap): + # if we got a keyboard and keymap, attempt to find it + if not locate_keymap(cli.config.flash.keyboard, cli.config.flash.keymap): cli.log.error('Invalid keymap argument.') cli.print_help() return False - if cli.args.clean: - commands.append(create_make_command(cli.config.flash.keyboard, cli.config.flash.keymap, 'clean', **envs)) - commands.append(create_make_command(cli.config.flash.keyboard, cli.config.flash.keymap, cli.args.bootloader, parallel=cli.config.flash.parallel, **envs)) + # If we got here, then we have a valid keyboard and keymap for a build target + target = KeyboardKeymapBuildTarget(cli.config.flash.keyboard, cli.config.flash.keymap) - if not commands: + if not target: cli.log.error('You must supply a configurator export, both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.') cli.print_help() return False - cli.log.info('Compiling keymap with {fg_cyan}%s', ' '.join(commands[-1])) - if not cli.args.dry_run: - cli.echo('\n') - for command in commands: - ret = cli.run(command, capture_output=False) - if ret.returncode: - return ret.returncode + target.configure(parallel=cli.config.flash.parallel, clean=cli.args.clean) + target.compile(cli.args.bootloader, dry_run=cli.args.dry_run, **envs) diff --git a/lib/python/qmk/cli/generate/compilation_database.py b/lib/python/qmk/cli/generate/compilation_database.py index 9e5c266516..5100d2b6d2 100755 --- a/lib/python/qmk/cli/generate/compilation_database.py +++ b/lib/python/qmk/cli/generate/compilation_database.py @@ -12,7 +12,7 @@ from typing import Dict, Iterator, List, Union from milc import cli, MILC -from qmk.commands import create_make_command +from qmk.commands import find_make from qmk.constants import QMK_FIRMWARE from qmk.decorators import automagic_keyboard, automagic_keymap from qmk.keyboard import keyboard_completer, keyboard_folder @@ -76,9 +76,12 @@ def parse_make_n(f: Iterator[str]) -> List[Dict[str, str]]: return records -def write_compilation_database(keyboard: str, keymap: str, output_path: Path) -> bool: +def write_compilation_database(keyboard: str = None, keymap: str = None, output_path: Path = QMK_FIRMWARE / 'compile_commands.json', skip_clean: bool = False, command: List[str] = None, **env_vars) -> bool: # Generate the make command for a specific keyboard/keymap. - command = create_make_command(keyboard, keymap, dry_run=True) + if not command: + from qmk.build_targets import KeyboardKeymapBuildTarget # Lazy load due to circular references + target = KeyboardKeymapBuildTarget(keyboard, keymap) + command = target.compile_command(dry_run=True, **env_vars) if not command: cli.log.error('You must supply both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.') @@ -90,9 +93,10 @@ def write_compilation_database(keyboard: str, keymap: str, output_path: Path) -> env.pop("MAKEFLAGS", None) # re-use same executable as the main make invocation (might be gmake) - clean_command = [command[0], 'clean'] - cli.log.info('Making clean with {fg_cyan}%s', ' '.join(clean_command)) - cli.run(clean_command, capture_output=False, check=True, env=env) + if not skip_clean: + clean_command = [find_make(), "clean"] + cli.log.info('Making clean with {fg_cyan}%s', ' '.join(clean_command)) + cli.run(clean_command, capture_output=False, check=True, env=env) cli.log.info('Gathering build instructions from {fg_cyan}%s', ' '.join(command)) diff --git a/lib/python/qmk/cli/mass_compile.py b/lib/python/qmk/cli/mass_compile.py index 06e6e411a7..7968de53e7 100755 --- a/lib/python/qmk/cli/mass_compile.py +++ b/lib/python/qmk/cli/mass_compile.py @@ -3,26 +3,28 @@ This will compile everything in parallel, for testing purposes. """ import os +from typing import List from pathlib import Path from subprocess import DEVNULL from milc import cli from qmk.constants import QMK_FIRMWARE -from qmk.commands import _find_make, get_make_parallel_args +from qmk.commands import find_make, get_make_parallel_args, build_environment from qmk.search import search_keymap_targets, search_make_targets +from qmk.build_targets import BuildTarget, JsonKeymapBuildTarget -def mass_compile_targets(targets, clean, dry_run, no_temp, parallel, env): +def mass_compile_targets(targets: List[BuildTarget], clean: bool, dry_run: bool, no_temp: bool, parallel: int, **env): if len(targets) == 0: return - make_cmd = _find_make() + make_cmd = find_make() builddir = Path(QMK_FIRMWARE) / '.build' makefile = builddir / 'parallel_kb_builds.mk' if dry_run: cli.log.info('Compilation targets:') - for target in sorted(targets): + for target in sorted(targets, key=lambda t: (t.keyboard, t.keymap)): cli.log.info(f"{{fg_cyan}}qmk compile -kb {target[0]} -km {target[1]}{{fg_reset}}") else: if clean: @@ -30,9 +32,13 @@ def mass_compile_targets(targets, clean, dry_run, no_temp, parallel, env): builddir.mkdir(parents=True, exist_ok=True) with open(makefile, "w") as f: - for target in sorted(targets): - keyboard_name = target[0] - keymap_name = target[1] + for target in sorted(targets, key=lambda t: (t.keyboard, t.keymap)): + keyboard_name = target.keyboard + keymap_name = target.keymap + target.configure(parallel=1) # We ignore parallelism on a per-build basis as we defer to the parent make invocation + target.prepare_build(**env) # If we've got json targets, allow them to write out any extra info to .build before we kick off `make` + command = target.compile_command(**env) + command[0] = '+@$(MAKE)' # Override the make so that we can use jobserver to handle parallelism keyboard_safe = keyboard_name.replace('/', '_') build_log = f"{QMK_FIRMWARE}/.build/build.log.{os.getpid()}.{keyboard_safe}.{keymap_name}" failed_log = f"{QMK_FIRMWARE}/.build/failed.log.{os.getpid()}.{keyboard_safe}.{keymap_name}" @@ -43,7 +49,7 @@ all: {keyboard_safe}_{keymap_name}_binary {keyboard_safe}_{keymap_name}_binary: @rm -f "{build_log}" || true @echo "Compiling QMK Firmware for target: '{keyboard_name}:{keymap_name}'..." >>"{build_log}" - +@$(MAKE) -C "{QMK_FIRMWARE}" -f "{QMK_FIRMWARE}/builddefs/build_keyboard.mk" KEYBOARD="{keyboard_name}" KEYMAP="{keymap_name}" COLOR=true SILENT=false {' '.join(env)} \\ + {' '.join(command)} \\ >>"{build_log}" 2>&1 \\ || cp "{build_log}" "{failed_log}" @{{ grep '\[ERRORS\]' "{build_log}" >/dev/null 2>&1 && printf "Build %-64s \e[1;31m[ERRORS]\e[0m\\n" "{keyboard_name}:{keymap_name}" ; }} \\ @@ -95,8 +101,11 @@ def mass_compile(cli): """Compile QMK Firmware against all keyboards. """ if len(cli.args.builds) > 0: - targets = search_make_targets(cli.args.builds, cli.args.filter) + json_like_targets = list([Path(p) for p in filter(lambda e: Path(e).exists() and Path(e).suffix == '.json', cli.args.builds)]) + make_like_targets = list(filter(lambda e: Path(e) not in json_like_targets, cli.args.builds)) + targets = search_make_targets(make_like_targets) + targets.extend([JsonKeymapBuildTarget(e) for e in json_like_targets]) else: targets = search_keymap_targets([('all', cli.config.mass_compile.keymap)], cli.args.filter) - return mass_compile_targets(targets, cli.args.clean, cli.args.dry_run, cli.config.mass_compile.no_temp, cli.config.mass_compile.parallel, cli.args.env) + return mass_compile_targets(targets, cli.args.clean, cli.args.dry_run, cli.config.mass_compile.no_temp, cli.config.mass_compile.parallel, **build_environment(cli.args.env)) diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py index 34696e3793..e5fdbc4f30 100644 --- a/lib/python/qmk/commands.py +++ b/lib/python/qmk/commands.py @@ -2,18 +2,16 @@ """ import os import sys -import json import shutil from pathlib import Path from milc import cli import jsonschema -from qmk.constants import INTERMEDIATE_OUTPUT_PREFIX from qmk.json_schema import json_load, validate -def _find_make(): +def find_make(): """Returns the correct make command for this environment. """ make_cmd = os.environ.get('MAKE') @@ -24,74 +22,6 @@ def _find_make(): return make_cmd -def create_make_target(target, dry_run=False, parallel=1, **env_vars): - """Create a make command - - Args: - - target - Usually a make rule, such as 'clean' or 'all'. - - dry_run - make -n -- don't actually build - - parallel - The number of make jobs to run in parallel - - **env_vars - Environment variables to be passed to make. - - Returns: - - A command that can be run to make the specified keyboard and keymap - """ - env = [] - make_cmd = _find_make() - - for key, value in env_vars.items(): - env.append(f'{key}={value}') - - if cli.config.general.verbose: - env.append('VERBOSE=true') - - return [make_cmd, *(['-n'] if dry_run else []), *get_make_parallel_args(parallel), *env, target] - - -def create_make_command(keyboard, keymap, target=None, dry_run=False, parallel=1, **env_vars): - """Create a make compile command - - Args: - - keyboard - The path of the keyboard, for example 'plank' - - keymap - The name of the keymap, for example 'algernon' - - target - Usually a bootloader. - - dry_run - make -n -- don't actually build - - parallel - The number of make jobs to run in parallel - - **env_vars - Environment variables to be passed to make. - - Returns: - - A command that can be run to make the specified keyboard and keymap - """ - make_args = [keyboard, keymap] - - if target: - make_args.append(target) - - return create_make_target(':'.join(make_args), dry_run=dry_run, parallel=parallel, **env_vars) - - def get_make_parallel_args(parallel=1): """Returns the arguments for running the specified number of parallel jobs. """ @@ -100,7 +30,7 @@ def get_make_parallel_args(parallel=1): if int(parallel) <= 0: # 0 or -1 means -j without argument (unlimited jobs) parallel_args.append('--jobs') - else: + elif int(parallel) > 1: parallel_args.append('--jobs=' + str(parallel)) if int(parallel) != 1: @@ -110,96 +40,6 @@ def get_make_parallel_args(parallel=1): return parallel_args -def compile_configurator_json(user_keymap, bootloader=None, parallel=1, clean=False, **env_vars): - """Convert a configurator export JSON file into a C file and then compile it. - - Args: - - user_keymap - A deserialized keymap export - - bootloader - A bootloader to flash - - parallel - The number of make jobs to run in parallel - - Returns: - - A command to run to compile and flash the C file. - """ - # In case the user passes a keymap.json from a keymap directory directly to the CLI. - # e.g.: qmk compile - < keyboards/clueboard/california/keymaps/default/keymap.json - user_keymap["keymap"] = user_keymap.get("keymap", "default_json") - - keyboard_filesafe = user_keymap['keyboard'].replace('/', '_') - target = f'{keyboard_filesafe}_{user_keymap["keymap"]}' - intermediate_output = Path(f'{INTERMEDIATE_OUTPUT_PREFIX}{keyboard_filesafe}_{user_keymap["keymap"]}') - keymap_dir = intermediate_output / 'src' - keymap_json = keymap_dir / 'keymap.json' - - if clean: - if intermediate_output.exists(): - shutil.rmtree(intermediate_output) - - # begin with making the deepest folder in the tree - keymap_dir.mkdir(exist_ok=True, parents=True) - - # Compare minified to ensure consistent comparison - new_content = json.dumps(user_keymap, separators=(',', ':')) - if keymap_json.exists(): - old_content = json.dumps(json.loads(keymap_json.read_text(encoding='utf-8')), separators=(',', ':')) - if old_content == new_content: - new_content = None - - # Write the keymap.json file if different - if new_content: - keymap_json.write_text(new_content, encoding='utf-8') - - # Return a command that can be run to make the keymap and flash if given - verbose = 'true' if cli.config.general.verbose else 'false' - color = 'true' if cli.config.general.color else 'false' - make_command = [_find_make()] - - if not cli.config.general.verbose: - make_command.append('-s') - - make_command.extend([ - *get_make_parallel_args(parallel), - '-r', - '-R', - '-f', - 'builddefs/build_keyboard.mk', - ]) - - if bootloader: - make_command.append(bootloader) - - make_command.extend([ - f'KEYBOARD={user_keymap["keyboard"]}', - f'KEYMAP={user_keymap["keymap"]}', - f'KEYBOARD_FILESAFE={keyboard_filesafe}', - f'TARGET={target}', - f'INTERMEDIATE_OUTPUT={intermediate_output}', - f'MAIN_KEYMAP_PATH_1={intermediate_output}', - f'MAIN_KEYMAP_PATH_2={intermediate_output}', - f'MAIN_KEYMAP_PATH_3={intermediate_output}', - f'MAIN_KEYMAP_PATH_4={intermediate_output}', - f'MAIN_KEYMAP_PATH_5={intermediate_output}', - f'KEYMAP_JSON={keymap_json}', - f'KEYMAP_PATH={keymap_dir}', - f'VERBOSE={verbose}', - f'COLOR={color}', - 'SILENT=false', - 'QMK_BIN="qmk"', - ]) - - for key, value in env_vars.items(): - make_command.append(f'{key}={value}') - - return make_command - - def parse_configurator_json(configurator_file): """Open and parse a configurator json export """ diff --git a/lib/python/qmk/keyboard.py b/lib/python/qmk/keyboard.py index 9826f3f887..1aa63687d4 100644 --- a/lib/python/qmk/keyboard.py +++ b/lib/python/qmk/keyboard.py @@ -98,11 +98,7 @@ def keyboard_folder(keyboard): if keyboard == last_keyboard: break - rules_mk_file = Path(base_path, keyboard, 'rules.mk') - - if rules_mk_file.exists(): - rules_mk = parse_rules_mk_file(rules_mk_file) - keyboard = rules_mk.get('DEFAULT_FOLDER', keyboard) + keyboard = resolve_keyboard(keyboard) if not qmk.path.is_keyboard(keyboard): raise ValueError(f'Invalid keyboard: {keyboard}') diff --git a/lib/python/qmk/search.py b/lib/python/qmk/search.py index a74450ca87..301d69c6c2 100644 --- a/lib/python/qmk/search.py +++ b/lib/python/qmk/search.py @@ -11,8 +11,9 @@ from milc import cli from qmk.util import parallel_map from qmk.info import keymap_json -import qmk.keyboard -import qmk.keymap +from qmk.keyboard import list_keyboards, keyboard_folder +from qmk.keymap import list_keymaps, locate_keymap +from qmk.build_targets import KeyboardKeymapBuildTarget, BuildTarget def _set_log_level(level): @@ -36,15 +37,15 @@ def _all_keymaps(keyboard): """Returns a list of tuples of (keyboard, keymap) for all keymaps for the given keyboard. """ with ignore_logging(): - keyboard = qmk.keyboard.resolve_keyboard(keyboard) - return [(keyboard, keymap) for keymap in qmk.keymap.list_keymaps(keyboard)] + keyboard = keyboard_folder(keyboard) + return [(keyboard, keymap) for keymap in list_keymaps(keyboard)] def _keymap_exists(keyboard, keymap): """Returns the keyboard name if the keyboard+keymap combination exists, otherwise None. """ with ignore_logging(): - return keyboard if qmk.keymap.locate_keymap(keyboard, keymap) is not None else None + return keyboard if locate_keymap(keyboard, keymap) is not None else None def _load_keymap_info(kb_km): @@ -75,7 +76,7 @@ def _expand_keymap_target(keyboard: str, keymap: str, all_keyboards: List[str] = Caters for 'all' in either keyboard or keymap, or both. """ if all_keyboards is None: - all_keyboards = qmk.keyboard.list_keyboards() + all_keyboards = list_keyboards() if keyboard == 'all': if keymap == 'all': @@ -90,30 +91,29 @@ def _expand_keymap_target(keyboard: str, keymap: str, all_keyboards: List[str] = return [(kb, keymap) for kb in filter(lambda e: e is not None, parallel_map(keyboard_filter, all_keyboards))] else: if keymap == 'all': - keyboard = qmk.keyboard.resolve_keyboard(keyboard) cli.log.info(f'Retrieving list of keymaps for keyboard "{keyboard}"...') return _all_keymaps(keyboard) else: - return [(qmk.keyboard.resolve_keyboard(keyboard), keymap)] + return [(keyboard, keymap)] def expand_keymap_targets(targets: List[Tuple[str, str]]) -> List[Tuple[str, str]]: """Expand a list of (keyboard, keymap) tuples inclusive of 'all', into a list of explicit (keyboard, keymap) tuples. """ overall_targets = [] - all_keyboards = qmk.keyboard.list_keyboards() + all_keyboards = list_keyboards() for target in targets: overall_targets.extend(_expand_keymap_target(target[0], target[1], all_keyboards)) return list(sorted(set(overall_targets))) -def _filter_keymap_targets(target_list: List[Tuple[str, str]], filters: List[str] = [], print_vals: List[str] = []) -> List[Tuple[str, str, List[Tuple[str, str]]]]: +def _filter_keymap_targets(target_list: List[Tuple[str, str]], filters: List[str] = []) -> List[BuildTarget]: """Filter a list of (keyboard, keymap) tuples based on the supplied filters. Optionally includes the values of the queried info.json keys. """ - if len(filters) == 0 and len(print_vals) == 0: - targets = [(kb, km, {}) for kb, km in target_list] + if len(filters) == 0: + targets = [KeyboardKeymapBuildTarget(keyboard=kb, keymap=km) for kb, km in target_list] else: cli.log.info('Parsing data for all matching keyboard/keymap combinations...') valid_keymaps = [(e[0], e[1], dotty(e[2])) for e in parallel_map(_load_keymap_info, target_list)] @@ -172,18 +172,18 @@ def _filter_keymap_targets(target_list: List[Tuple[str, str]], filters: List[str cli.log.warning(f'Unrecognized filter expression: {filter_expr}') continue - targets = [(e[0], e[1], [(p, e[2].get(p)) for p in print_vals]) for e in valid_keymaps] + targets = [KeyboardKeymapBuildTarget(keyboard=e[0], keymap=e[1], json=e[2]) for e in valid_keymaps] return targets -def search_keymap_targets(targets: List[Tuple[str, str]] = [('all', 'default')], filters: List[str] = [], print_vals: List[str] = []) -> List[Tuple[str, str, List[Tuple[str, str]]]]: +def search_keymap_targets(targets: List[Tuple[str, str]] = [('all', 'default')], filters: List[str] = []) -> List[BuildTarget]: """Search for build targets matching the supplied criteria. """ - return list(sorted(_filter_keymap_targets(expand_keymap_targets(targets), filters, print_vals), key=lambda e: (e[0], e[1]))) + return _filter_keymap_targets(expand_keymap_targets(targets), filters) -def search_make_targets(targets: List[str], filters: List[str] = [], print_vals: List[str] = []) -> List[Tuple[str, str, List[Tuple[str, str]]]]: +def search_make_targets(targets: List[str], filters: List[str] = []) -> List[BuildTarget]: """Search for build targets matching the supplied criteria. """ - return list(sorted(_filter_keymap_targets(expand_make_targets(targets), filters, print_vals), key=lambda e: (e[0], e[1]))) + return _filter_keymap_targets(expand_make_targets(targets), filters) -- cgit v1.2.3 From 62cca5c43a63e694da4596dc41d1251f9c530198 Mon Sep 17 00:00:00 2001 From: Sergey Vlasov Date: Sun, 19 Nov 2023 00:26:00 +0300 Subject: `qmk find`: Fix failure with multiple filters (#22497) When multiple `-f FILTER` options were specified, `qmk find` did not return anything at all instead of printing the list of entries that matched all of the specified filters. The problem was that the statement in `_filter_keymap_targets()` that filled `targets` had a wrong indent and therefore was executed for every filter instead of only once after applying all filters, and `valid_keymaps` was actually an iterator and therefore could be used only once. Moving the statement outside of the loop fixes the problem. --- lib/python/qmk/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/python') diff --git a/lib/python/qmk/search.py b/lib/python/qmk/search.py index 301d69c6c2..1140abe69d 100644 --- a/lib/python/qmk/search.py +++ b/lib/python/qmk/search.py @@ -172,7 +172,7 @@ def _filter_keymap_targets(target_list: List[Tuple[str, str]], filters: List[str cli.log.warning(f'Unrecognized filter expression: {filter_expr}') continue - targets = [KeyboardKeymapBuildTarget(keyboard=e[0], keymap=e[1], json=e[2]) for e in valid_keymaps] + targets = [KeyboardKeymapBuildTarget(keyboard=e[0], keymap=e[1], json=e[2]) for e in valid_keymaps] return targets -- cgit v1.2.3 From e279c78ba3054ebc2e294dd91ea98341cc509d1e Mon Sep 17 00:00:00 2001 From: Joel Challis Date: Mon, 20 Nov 2023 15:41:48 +0000 Subject: Enable linking of encoders to switch within layout macros (#22264) --- data/schemas/keyboard.jsonschema | 1 + docs/reference_info_json.md | 2 ++ keyboards/drop/sense75/info.json | 2 +- lib/python/qmk/info.py | 28 ++++++++++++++++++++ lib/python/qmk/keyboard.py | 55 +++++++++++++++++++++++++++++++++++++++- 5 files changed, 86 insertions(+), 2 deletions(-) (limited to 'lib/python') diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index 9fc455530c..2996958084 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -348,6 +348,7 @@ "additionalProperties": false, "required": ["x", "y"], "properties": { + "encoder": {"$ref": "qmk.definitions.v1#/unsigned_int"}, "label": { "type": "string", "pattern": "^[^\\n]*$" diff --git a/docs/reference_info_json.md b/docs/reference_info_json.md index 4a70a4bb6f..e102b9bfb9 100644 --- a/docs/reference_info_json.md +++ b/docs/reference_info_json.md @@ -324,6 +324,8 @@ The ISO enter key is represented by a 1.25u×2uh key. Renderers which utilize in * `w` * The width of the key, in key units. * Default: `1` (1u) + * `encoder` + * The index of an encoder this key should be linked to * Example: `{"label": "Shift", "matrix": [4, 0], "x": 0, "y": 4.25, "w": 2.25}` ## Leader Key :id=leader-key diff --git a/keyboards/drop/sense75/info.json b/keyboards/drop/sense75/info.json index dbefc108f2..052b494375 100644 --- a/keyboards/drop/sense75/info.json +++ b/keyboards/drop/sense75/info.json @@ -44,7 +44,7 @@ {"matrix": [0, 11], "label": "F11", "x": 11.75, "y": 0}, {"matrix": [0, 12], "label": "F12", "x": 12.75, "y": 0}, {"matrix": [0, 13], "label": "PrtSc", "x": 14, "y": 0}, - {"matrix": [0, 14], "label": "Mute", "x": 15.25, "y": 0}, + {"matrix": [0, 14], "encoder":0, "label": "Mute", "x": 15.25, "y": 0}, {"matrix": [1, 0], "label": "~", "x": 0, "y": 1.25}, {"matrix": [1, 1], "label": "!", "x": 1, "y": 1.25}, {"matrix": [1, 2], "label": "@", "x": 2, "y": 1.25}, diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index fe829a724a..3efd34555c 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -55,6 +55,29 @@ def _get_key_left_position(key): return key['x'] - 0.25 if key.get('h', 1) == 2 and key.get('w', 1) == 1.25 else key['x'] +def _find_invalid_encoder_index(info_data): + """Perform additional validation of encoders + """ + enc_count = len(info_data.get('encoder', {}).get('rotary', [])) + enc_count += len(info_data.get('split', {}).get('encoder', {}).get('right', {}).get('rotary', [])) + + ret = [] + layouts = info_data.get('layouts', {}) + for layout_name, layout_data in layouts.items(): + found = set() + for key in layout_data['layout']: + if 'encoder' in key: + if enc_count == 0: + ret.append((layout_name, key['encoder'], 'non-configured')) + elif key['encoder'] >= enc_count: + ret.append((layout_name, key['encoder'], 'out of bounds')) + elif key['encoder'] in found: + ret.append((layout_name, key['encoder'], 'duplicate')) + found.add(key['encoder']) + + return ret + + def _additional_validation(keyboard, info_data): """Non schema checks """ @@ -105,6 +128,11 @@ def _additional_validation(keyboard, info_data): if not decl.get("aliases", []): _log_error(info_data, f'Keycode {decl["key"]} has no short form alias') + # encoder IDs in layouts must be in range and not duplicated + found = _find_invalid_encoder_index(info_data) + for layout_name, encoder_index, reason in found: + _log_error(info_data, f'Layout "{layout_name}" contains {reason} encoder index {encoder_index}.') + def _validate(keyboard, info_data): """Perform various validation on the provided info.json data diff --git a/lib/python/qmk/keyboard.py b/lib/python/qmk/keyboard.py index 1aa63687d4..4e525731f7 100644 --- a/lib/python/qmk/keyboard.py +++ b/lib/python/qmk/keyboard.py @@ -30,6 +30,28 @@ BOX_DRAWING_CHARACTERS = { "h": "_", }, } +ENC_DRAWING_CHARACTERS = { + "unicode": { + "tl": "╭", + "tr": "╮", + "bl": "╰", + "br": "╯", + "vl": "▲", + "vr": "▼", + "v": "│", + "h": "─", + }, + "ascii": { + "tl": " ", + "tr": " ", + "bl": "\\", + "br": "/", + "v": "|", + "vl": "/", + "vr": "\\", + "h": "_", + }, +} class AllKeyboards: @@ -213,7 +235,9 @@ def render_layout(layout_data, render_ascii, key_labels=None): else: label = key.get('label', '') - if x >= 0.25 and w == 1.25 and h == 2: + if 'encoder' in key: + render_encoder(textpad, x, y, w, h, label, style) + elif x >= 0.25 and w == 1.25 and h == 2: render_key_isoenter(textpad, x, y, w, h, label, style) elif w == 1.5 and h == 2: render_key_baenter(textpad, x, y, w, h, label, style) @@ -331,3 +355,32 @@ def render_key_baenter(textpad, x, y, w, h, label, style): textpad[y + 3][x - 3:x + w] = crn_line textpad[y + 4][x - 3:x + w] = lab_line textpad[y + 5][x - 3:x + w] = bot_line + + +def render_encoder(textpad, x, y, w, h, label, style): + box_chars = ENC_DRAWING_CHARACTERS[style] + x = ceil(x * 4) + y = ceil(y * 3) + w = ceil(w * 4) + h = ceil(h * 3) + + label_len = w - 2 + label_leftover = label_len - len(label) + + if len(label) > label_len: + label = label[:label_len] + + label_blank = ' ' * label_len + label_border = box_chars['h'] * label_len + label_middle = label + ' ' * label_leftover + + top_line = array('u', box_chars['tl'] + label_border + box_chars['tr']) + lab_line = array('u', box_chars['vl'] + label_middle + box_chars['vr']) + mid_line = array('u', box_chars['v'] + label_blank + box_chars['v']) + bot_line = array('u', box_chars['bl'] + label_border + box_chars['br']) + + textpad[y][x:x + w] = top_line + textpad[y + 1][x:x + w] = lab_line + for i in range(h - 3): + textpad[y + i + 2][x:x + w] = mid_line + textpad[y + h - 1][x:x + w] = bot_line -- cgit v1.2.3 From 7e27d72cbc9af11b5de3f0c30810a0b95267acee Mon Sep 17 00:00:00 2001 From: Sergey Vlasov Date: Tue, 21 Nov 2023 18:05:29 +0300 Subject: Fix `qmk find` failure due to circular imports (#22523) There was an import cycle in the Python modules: - `qmk.build_targets` imported `qmk.cli.generate.compilation_database`; - importing `qmk.cli.generate.compilation_database` requires initializing `qmk.cli` first; - the initialization of `qmk.cli` imported the modules for all CLI commands; - `qmk.cli.compile` imported `qmk.build_targets`. This cycle did not matter in most cases, because `qmk.cli` was imported first, and in that case importing `qmk.cli.generate.compilation_database` did not trigger the initialization of `qmk.cli` again. However, there was one corner case when `qmk.bulld_targets` was getting imported first: - The `qmk find` command uses the `multiprocessing` module. - The `multiprocessing` module uses the `spawn` start method on macOS and Windows. - When the `spawn` method is used, the child processes initialize without any Python modules loaded, and the required modules are loaded on demand by the `pickle` module when receiving the serialized objects from the main process. The result was that the `qmk find` command did not work properly on macOS (and probably Windows too); it reported exceptions like this: ImportError: cannot import name 'KeyboardKeymapBuildTarget' from partially initialized module 'qmk.build_targets' (most likely due to a circular import) Moving the offending `qmk.cli.generate.compilation_database` import into the method which actually uses it fixes the problem. --- lib/python/qmk/build_targets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/python') diff --git a/lib/python/qmk/build_targets.py b/lib/python/qmk/build_targets.py index 236c2eaa03..fc72022049 100644 --- a/lib/python/qmk/build_targets.py +++ b/lib/python/qmk/build_targets.py @@ -10,7 +10,6 @@ from qmk.constants import QMK_FIRMWARE, INTERMEDIATE_OUTPUT_PREFIX from qmk.commands import find_make, get_make_parallel_args, parse_configurator_json from qmk.keyboard import keyboard_folder from qmk.info import keymap_json -from qmk.cli.generate.compilation_database import write_compilation_database class BuildTarget: @@ -105,6 +104,7 @@ class BuildTarget: def generate_compilation_database(self, build_target: str = None, skip_clean: bool = False, **env_vars) -> None: self.prepare_build(build_target=build_target, **env_vars) command = self.compile_command(build_target=build_target, dry_run=True, **env_vars) + from qmk.cli.generate.compilation_database import write_compilation_database # Lazy load due to circular references write_compilation_database(command=command, output_path=QMK_FIRMWARE / 'compile_commands.json', skip_clean=skip_clean, **env_vars) def compile(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None: -- cgit v1.2.3 From 46b996a55e370b7c370dc0b092100a61121f9ddd Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Wed, 22 Nov 2023 11:14:34 +1100 Subject: CLI parallel search updates (#22525) --- lib/python/qmk/cli/ci/validate_aliases.py | 7 ++----- lib/python/qmk/cli/generate/api.py | 4 ++-- lib/python/qmk/commands.py | 4 ++-- lib/python/qmk/keyboard.py | 7 ++++++- lib/python/qmk/search.py | 17 ++++++++++++++--- 5 files changed, 26 insertions(+), 13 deletions(-) (limited to 'lib/python') diff --git a/lib/python/qmk/cli/ci/validate_aliases.py b/lib/python/qmk/cli/ci/validate_aliases.py index a205d03cff..7f781d4397 100644 --- a/lib/python/qmk/cli/ci/validate_aliases.py +++ b/lib/python/qmk/cli/ci/validate_aliases.py @@ -1,11 +1,8 @@ """Validates the list of keyboard aliases. """ -from pathlib import Path - from milc import cli -from qmk.json_schema import json_load -from qmk.keyboard import resolve_keyboard, keyboard_folder +from qmk.keyboard import resolve_keyboard, keyboard_folder, keyboard_alias_definitions def _safe_keyboard_folder(target): @@ -34,7 +31,7 @@ def _target_keyboard_exists(target): @cli.subcommand('Validates the list of keyboard aliases.', hidden=True) def ci_validate_aliases(cli): - aliases = json_load(Path('data/mappings/keyboard_aliases.hjson')) + aliases = keyboard_alias_definitions() success = True for alias in aliases.keys(): diff --git a/lib/python/qmk/cli/generate/api.py b/lib/python/qmk/cli/generate/api.py index 61a2f9f732..1948b483a1 100755 --- a/lib/python/qmk/cli/generate/api.py +++ b/lib/python/qmk/cli/generate/api.py @@ -10,7 +10,7 @@ from qmk.datetime import current_datetime from qmk.info import info_json from qmk.json_schema import json_load from qmk.keymap import list_keymaps -from qmk.keyboard import find_readme, list_keyboards +from qmk.keyboard import find_readme, list_keyboards, keyboard_alias_definitions from qmk.keycodes import load_spec, list_versions, list_languages DATA_PATH = Path('data') @@ -166,7 +166,7 @@ def generate_api(cli): # Generate data for the global files keyboard_list = sorted(kb_all) - keyboard_aliases = json_load(Path('data/mappings/keyboard_aliases.hjson')) + keyboard_aliases = keyboard_alias_definitions() keyboard_metadata = { 'last_updated': current_datetime(), 'keyboards': keyboard_list, diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py index e5fdbc4f30..519cb4c708 100644 --- a/lib/python/qmk/commands.py +++ b/lib/python/qmk/commands.py @@ -3,12 +3,12 @@ import os import sys import shutil -from pathlib import Path from milc import cli import jsonschema from qmk.json_schema import json_load, validate +from qmk.keyboard import keyboard_alias_definitions def find_make(): @@ -53,7 +53,7 @@ def parse_configurator_json(configurator_file): exit(1) keyboard = user_keymap['keyboard'] - aliases = json_load(Path('data/mappings/keyboard_aliases.hjson')) + aliases = keyboard_alias_definitions() while keyboard in aliases: last_keyboard = keyboard diff --git a/lib/python/qmk/keyboard.py b/lib/python/qmk/keyboard.py index 4e525731f7..34257bee8d 100644 --- a/lib/python/qmk/keyboard.py +++ b/lib/python/qmk/keyboard.py @@ -70,6 +70,11 @@ class AllKeyboards: base_path = os.path.join(os.getcwd(), "keyboards") + os.path.sep +@lru_cache(maxsize=1) +def keyboard_alias_definitions(): + return json_load(Path('data/mappings/keyboard_aliases.hjson')) + + def is_all_keyboards(keyboard): """Returns True if the keyboard is an AllKeyboards object. """ @@ -112,7 +117,7 @@ def keyboard_folder(keyboard): This checks aliases and DEFAULT_FOLDER to resolve the actual path for a keyboard. """ - aliases = json_load(Path('data/mappings/keyboard_aliases.hjson')) + aliases = keyboard_alias_definitions() while keyboard in aliases: last_keyboard = keyboard diff --git a/lib/python/qmk/search.py b/lib/python/qmk/search.py index 1140abe69d..9b8bb430b5 100644 --- a/lib/python/qmk/search.py +++ b/lib/python/qmk/search.py @@ -6,7 +6,7 @@ import fnmatch import logging import re from typing import List, Tuple -from dotty_dict import dotty +from dotty_dict import dotty, Dotty from milc import cli from qmk.util import parallel_map @@ -107,13 +107,22 @@ def expand_keymap_targets(targets: List[Tuple[str, str]]) -> List[Tuple[str, str return list(sorted(set(overall_targets))) +def _construct_build_target_kb_km(e): + return KeyboardKeymapBuildTarget(keyboard=e[0], keymap=e[1]) + + +def _construct_build_target_kb_km_json(e): + return KeyboardKeymapBuildTarget(keyboard=e[0], keymap=e[1], json=e[2]) + + def _filter_keymap_targets(target_list: List[Tuple[str, str]], filters: List[str] = []) -> List[BuildTarget]: """Filter a list of (keyboard, keymap) tuples based on the supplied filters. Optionally includes the values of the queried info.json keys. """ if len(filters) == 0: - targets = [KeyboardKeymapBuildTarget(keyboard=kb, keymap=km) for kb, km in target_list] + cli.log.info('Preparing target list...') + targets = list(parallel_map(_construct_build_target_kb_km, target_list)) else: cli.log.info('Parsing data for all matching keyboard/keymap combinations...') valid_keymaps = [(e[0], e[1], dotty(e[2])) for e in parallel_map(_load_keymap_info, target_list)] @@ -172,7 +181,9 @@ def _filter_keymap_targets(target_list: List[Tuple[str, str]], filters: List[str cli.log.warning(f'Unrecognized filter expression: {filter_expr}') continue - targets = [KeyboardKeymapBuildTarget(keyboard=e[0], keymap=e[1], json=e[2]) for e in valid_keymaps] + cli.log.info('Preparing target list...') + valid_keymaps = [(e[0], e[1], e[2].to_dict() if isinstance(e[2], Dotty) else e[2]) for e in valid_keymaps] # need to convert dotty_dict back to dict because it doesn't survive parallelisation + targets = list(parallel_map(_construct_build_target_kb_km_json, list(valid_keymaps))) return targets -- cgit v1.2.3 From 0fcd13f55225c4545d828e1898c219ef3cae578f Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Wed, 22 Nov 2023 12:08:26 +1100 Subject: [CLI] Remove duplicates from search results (#22528) --- lib/python/qmk/build_targets.py | 11 +++++++++++ lib/python/qmk/search.py | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) (limited to 'lib/python') diff --git a/lib/python/qmk/build_targets.py b/lib/python/qmk/build_targets.py index fc72022049..16a7ef87a2 100644 --- a/lib/python/qmk/build_targets.py +++ b/lib/python/qmk/build_targets.py @@ -31,6 +31,17 @@ class BuildTarget: def __repr__(self): return f'BuildTarget(keyboard={self.keyboard}, keymap={self.keymap})' + def __eq__(self, __value: object) -> bool: + if not isinstance(__value, BuildTarget): + return False + return self.__repr__() == __value.__repr__() + + def __ne__(self, __value: object) -> bool: + return not self.__eq__(__value) + + def __hash__(self) -> int: + return self.__repr__().__hash__() + def configure(self, parallel: int = None, clean: bool = None, compiledb: bool = None) -> None: if parallel is not None: self._parallel = parallel diff --git a/lib/python/qmk/search.py b/lib/python/qmk/search.py index 9b8bb430b5..84cf6cbe32 100644 --- a/lib/python/qmk/search.py +++ b/lib/python/qmk/search.py @@ -122,7 +122,7 @@ def _filter_keymap_targets(target_list: List[Tuple[str, str]], filters: List[str """ if len(filters) == 0: cli.log.info('Preparing target list...') - targets = list(parallel_map(_construct_build_target_kb_km, target_list)) + targets = list(set(parallel_map(_construct_build_target_kb_km, target_list))) else: cli.log.info('Parsing data for all matching keyboard/keymap combinations...') valid_keymaps = [(e[0], e[1], dotty(e[2])) for e in parallel_map(_load_keymap_info, target_list)] @@ -183,7 +183,7 @@ def _filter_keymap_targets(target_list: List[Tuple[str, str]], filters: List[str cli.log.info('Preparing target list...') valid_keymaps = [(e[0], e[1], e[2].to_dict() if isinstance(e[2], Dotty) else e[2]) for e in valid_keymaps] # need to convert dotty_dict back to dict because it doesn't survive parallelisation - targets = list(parallel_map(_construct_build_target_kb_km_json, list(valid_keymaps))) + targets = list(set(parallel_map(_construct_build_target_kb_km_json, list(valid_keymaps)))) return targets -- cgit v1.2.3 From 5501e804ff8d41ce656061b91896c4ac8c681d78 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Tue, 28 Nov 2023 07:53:43 +1100 Subject: QMK Userspace (#22222) Co-authored-by: Duncan Sutherland --- Makefile | 30 ++++- builddefs/build_json.mk | 19 ++++ builddefs/build_keyboard.mk | 96 +++++++++++----- builddefs/build_layout.mk | 4 + builddefs/common_rules.mk | 11 +- data/schemas/definitions.jsonschema | 18 +++ data/schemas/user_repo_v0.jsonschema | 14 +++ data/schemas/user_repo_v1.jsonschema | 22 ++++ docs/_summary.md | 2 +- docs/cli_commands.md | 125 +++++++++++++++++++++ docs/newbs_external_userspace.md | 96 ++++++++++++++++ lib/python/qmk/build_targets.py | 16 +++ lib/python/qmk/cli/__init__.py | 5 + lib/python/qmk/cli/compile.py | 4 +- lib/python/qmk/cli/doctor/main.py | 25 ++++- lib/python/qmk/cli/format/json.py | 70 ++++++++---- lib/python/qmk/cli/mass_compile.py | 2 +- lib/python/qmk/cli/new/keymap.py | 8 ++ lib/python/qmk/cli/userspace/__init__.py | 5 + lib/python/qmk/cli/userspace/add.py | 51 +++++++++ lib/python/qmk/cli/userspace/compile.py | 38 +++++++ lib/python/qmk/cli/userspace/doctor.py | 11 ++ lib/python/qmk/cli/userspace/list.py | 51 +++++++++ lib/python/qmk/cli/userspace/remove.py | 37 +++++++ lib/python/qmk/commands.py | 6 + lib/python/qmk/constants.py | 8 ++ lib/python/qmk/json_encoders.py | 18 +++ lib/python/qmk/keyboard.py | 22 +++- lib/python/qmk/keymap.py | 130 ++++++++++++++-------- lib/python/qmk/path.py | 59 +++++++++- lib/python/qmk/userspace.py | 185 +++++++++++++++++++++++++++++++ 31 files changed, 1081 insertions(+), 107 deletions(-) create mode 100644 data/schemas/user_repo_v0.jsonschema create mode 100644 data/schemas/user_repo_v1.jsonschema create mode 100644 docs/newbs_external_userspace.md create mode 100644 lib/python/qmk/cli/userspace/__init__.py create mode 100644 lib/python/qmk/cli/userspace/add.py create mode 100644 lib/python/qmk/cli/userspace/compile.py create mode 100644 lib/python/qmk/cli/userspace/doctor.py create mode 100644 lib/python/qmk/cli/userspace/list.py create mode 100644 lib/python/qmk/cli/userspace/remove.py create mode 100644 lib/python/qmk/userspace.py (limited to 'lib/python') diff --git a/Makefile b/Makefile index 9ef406e420..ab30a17f58 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,11 @@ $(info QMK Firmware $(QMK_VERSION)) endif endif +# Try to determine userspace from qmk config, if set. +ifeq ($(QMK_USERSPACE),) + QMK_USERSPACE = $(shell qmk config -ro user.overlay_dir | cut -d= -f2 | sed -e 's@^None$$@@g') +endif + # Determine which qmk cli to use QMK_BIN := qmk @@ -191,9 +196,20 @@ define PARSE_KEYBOARD KEYMAPS += $$(notdir $$(patsubst %/.,%,$$(wildcard $(ROOT_DIR)/keyboards/$$(KEYBOARD_FOLDER_PATH_4)/keymaps/*/.))) KEYMAPS += $$(notdir $$(patsubst %/.,%,$$(wildcard $(ROOT_DIR)/keyboards/$$(KEYBOARD_FOLDER_PATH_5)/keymaps/*/.))) + ifneq ($(QMK_USERSPACE),) + KEYMAPS += $$(notdir $$(patsubst %/.,%,$$(wildcard $(QMK_USERSPACE)/keyboards/$$(KEYBOARD_FOLDER_PATH_1)/keymaps/*/.))) + KEYMAPS += $$(notdir $$(patsubst %/.,%,$$(wildcard $(QMK_USERSPACE)/keyboards/$$(KEYBOARD_FOLDER_PATH_2)/keymaps/*/.))) + KEYMAPS += $$(notdir $$(patsubst %/.,%,$$(wildcard $(QMK_USERSPACE)/keyboards/$$(KEYBOARD_FOLDER_PATH_3)/keymaps/*/.))) + KEYMAPS += $$(notdir $$(patsubst %/.,%,$$(wildcard $(QMK_USERSPACE)/keyboards/$$(KEYBOARD_FOLDER_PATH_4)/keymaps/*/.))) + KEYMAPS += $$(notdir $$(patsubst %/.,%,$$(wildcard $(QMK_USERSPACE)/keyboards/$$(KEYBOARD_FOLDER_PATH_5)/keymaps/*/.))) + endif + KEYBOARD_LAYOUTS := $(shell $(QMK_BIN) list-layouts --keyboard $1) LAYOUT_KEYMAPS := $$(foreach LAYOUT,$$(KEYBOARD_LAYOUTS),$$(eval LAYOUT_KEYMAPS += $$(notdir $$(patsubst %/.,%,$$(wildcard $(ROOT_DIR)/layouts/*/$$(LAYOUT)/*/.))))) + ifneq ($(QMK_USERSPACE),) + $$(foreach LAYOUT,$$(KEYBOARD_LAYOUTS),$$(eval LAYOUT_KEYMAPS += $$(notdir $$(patsubst %/.,%,$$(wildcard $(QMK_USERSPACE)/layouts/$$(LAYOUT)/*/.))))) + endif KEYMAPS := $$(sort $$(KEYMAPS) $$(LAYOUT_KEYMAPS)) @@ -431,8 +447,18 @@ clean: rm -rf $(BUILD_DIR) echo 'done.' -.PHONY: distclean -distclean: clean +.PHONY: distclean distclean_qmk +distclean: distclean_qmk +distclean_qmk: clean echo -n 'Deleting *.bin, *.hex, and *.uf2 ... ' rm -f *.bin *.hex *.uf2 echo 'done.' + +ifneq ($(QMK_USERSPACE),) +.PHONY: distclean_userspace +distclean: distclean_userspace +distclean_userspace: clean + echo -n 'Deleting userspace *.bin, *.hex, and *.uf2 ... ' + rm -f $(QMK_USERSPACE)/*.bin $(QMK_USERSPACE)/*.hex $(QMK_USERSPACE)/*.uf2 + echo 'done.' +endif diff --git a/builddefs/build_json.mk b/builddefs/build_json.mk index e29d678e48..e9d1420f36 100644 --- a/builddefs/build_json.mk +++ b/builddefs/build_json.mk @@ -15,3 +15,22 @@ else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_1)/keymap.json)","") KEYMAP_JSON := $(MAIN_KEYMAP_PATH_1)/keymap.json KEYMAP_JSON_PATH := $(MAIN_KEYMAP_PATH_1) endif + +ifneq ($(QMK_USERSPACE),) + ifneq ("$(wildcard $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_5)/keymap.json)","") + KEYMAP_JSON := $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_5)/keymap.json + KEYMAP_PATH := $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_5) + else ifneq ("$(wildcard $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_4)/keymap.json)","") + KEYMAP_JSON := $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_4)/keymap.json + KEYMAP_PATH := $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_4) + else ifneq ("$(wildcard $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_3)/keymap.json)","") + KEYMAP_JSON := $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_3)/keymap.json + KEYMAP_PATH := $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_3) + else ifneq ("$(wildcard $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_2)/keymap.json)","") + KEYMAP_JSON := $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_2)/keymap.json + KEYMAP_PATH := $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_2) + else ifneq ("$(wildcard $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_1)/keymap.json)","") + KEYMAP_JSON := $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_1)/keymap.json + KEYMAP_PATH := $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_1) + endif +endif diff --git a/builddefs/build_keyboard.mk b/builddefs/build_keyboard.mk index 12a8c5b67b..f17171fe20 100644 --- a/builddefs/build_keyboard.mk +++ b/builddefs/build_keyboard.mk @@ -127,34 +127,60 @@ include $(INFO_RULES_MK) include $(BUILDDEFS_PATH)/build_json.mk # Pull in keymap level rules.mk -# Look through the possible keymap folders until we find a matching keymap.c -ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_1)/keymap.c)","") - -include $(MAIN_KEYMAP_PATH_1)/rules.mk - KEYMAP_C := $(MAIN_KEYMAP_PATH_1)/keymap.c - KEYMAP_PATH := $(MAIN_KEYMAP_PATH_1) -else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_2)/keymap.c)","") - -include $(MAIN_KEYMAP_PATH_2)/rules.mk - KEYMAP_C := $(MAIN_KEYMAP_PATH_2)/keymap.c - KEYMAP_PATH := $(MAIN_KEYMAP_PATH_2) -else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_3)/keymap.c)","") - -include $(MAIN_KEYMAP_PATH_3)/rules.mk - KEYMAP_C := $(MAIN_KEYMAP_PATH_3)/keymap.c - KEYMAP_PATH := $(MAIN_KEYMAP_PATH_3) -else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_4)/keymap.c)","") - -include $(MAIN_KEYMAP_PATH_4)/rules.mk - KEYMAP_C := $(MAIN_KEYMAP_PATH_4)/keymap.c - KEYMAP_PATH := $(MAIN_KEYMAP_PATH_4) -else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_5)/keymap.c)","") - -include $(MAIN_KEYMAP_PATH_5)/rules.mk - KEYMAP_C := $(MAIN_KEYMAP_PATH_5)/keymap.c - KEYMAP_PATH := $(MAIN_KEYMAP_PATH_5) -else ifneq ($(LAYOUTS),) - # If we haven't found a keymap yet fall back to community layouts - include $(BUILDDEFS_PATH)/build_layout.mk -# Not finding keymap.c is fine if we found a keymap.json -else ifeq ("$(wildcard $(KEYMAP_JSON_PATH))", "") - $(call CATASTROPHIC_ERROR,Invalid keymap,Could not find keymap) - # this state should never be reached +ifeq ("$(wildcard $(KEYMAP_PATH))", "") + # Look through the possible keymap folders until we find a matching keymap.c + ifneq ($(QMK_USERSPACE),) + ifneq ("$(wildcard $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_1)/keymap.c)","") + -include $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_1)/rules.mk + KEYMAP_C := $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_1)/keymap.c + KEYMAP_PATH := $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_1) + else ifneq ("$(wildcard $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_2)/keymap.c)","") + -include $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_2)/rules.mk + KEYMAP_C := $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_2)/keymap.c + KEYMAP_PATH := $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_2) + else ifneq ("$(wildcard $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_3)/keymap.c)","") + -include $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_3)/rules.mk + KEYMAP_C := $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_3)/keymap.c + KEYMAP_PATH := $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_3) + else ifneq ("$(wildcard $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_4)/keymap.c)","") + -include $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_4)/rules.mk + KEYMAP_C := $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_4)/keymap.c + KEYMAP_PATH := $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_4) + else ifneq ("$(wildcard $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_5)/keymap.c)","") + -include $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_5)/rules.mk + KEYMAP_C := $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_5)/keymap.c + KEYMAP_PATH := $(QMK_USERSPACE)/$(MAIN_KEYMAP_PATH_5) + endif + endif + ifeq ($(KEYMAP_PATH),) + ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_1)/keymap.c)","") + -include $(MAIN_KEYMAP_PATH_1)/rules.mk + KEYMAP_C := $(MAIN_KEYMAP_PATH_1)/keymap.c + KEYMAP_PATH := $(MAIN_KEYMAP_PATH_1) + else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_2)/keymap.c)","") + -include $(MAIN_KEYMAP_PATH_2)/rules.mk + KEYMAP_C := $(MAIN_KEYMAP_PATH_2)/keymap.c + KEYMAP_PATH := $(MAIN_KEYMAP_PATH_2) + else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_3)/keymap.c)","") + -include $(MAIN_KEYMAP_PATH_3)/rules.mk + KEYMAP_C := $(MAIN_KEYMAP_PATH_3)/keymap.c + KEYMAP_PATH := $(MAIN_KEYMAP_PATH_3) + else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_4)/keymap.c)","") + -include $(MAIN_KEYMAP_PATH_4)/rules.mk + KEYMAP_C := $(MAIN_KEYMAP_PATH_4)/keymap.c + KEYMAP_PATH := $(MAIN_KEYMAP_PATH_4) + else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_5)/keymap.c)","") + -include $(MAIN_KEYMAP_PATH_5)/rules.mk + KEYMAP_C := $(MAIN_KEYMAP_PATH_5)/keymap.c + KEYMAP_PATH := $(MAIN_KEYMAP_PATH_5) + else ifneq ($(LAYOUTS),) + # If we haven't found a keymap yet fall back to community layouts + include $(BUILDDEFS_PATH)/build_layout.mk + else ifeq ("$(wildcard $(KEYMAP_JSON_PATH))", "") # Not finding keymap.c is fine if we found a keymap.json + $(call CATASTROPHIC_ERROR,Invalid keymap,Could not find keymap) + # this state should never be reached + endif + endif endif # Have we found a keymap.json? @@ -364,6 +390,16 @@ ifeq ("$(USER_NAME)","") endif USER_PATH := users/$(USER_NAME) +# If we have userspace, then add it to the lookup VPATH +ifneq ($(wildcard $(QMK_USERSPACE)),) + VPATH += $(QMK_USERSPACE) +endif + +# If the equivalent users directory exists in userspace, use that in preference to anything currently in the main repo +ifneq ($(wildcard $(QMK_USERSPACE)/$(USER_PATH)),) + USER_PATH := $(QMK_USERSPACE)/$(USER_PATH) +endif + # Pull in user level rules.mk -include $(USER_PATH)/rules.mk ifneq ("$(wildcard $(USER_PATH)/config.h)","") @@ -404,6 +440,10 @@ ifneq ("$(KEYMAP_H)","") CONFIG_H += $(KEYMAP_H) endif +ifeq ($(KEYMAP_C),) + $(call CATASTROPHIC_ERROR,Invalid keymap,Could not find keymap) +endif + OPT_DEFS += -DKEYMAP_C=\"$(KEYMAP_C)\" # If a keymap or userspace places their keymap array in another file instead, allow for it to be included diff --git a/builddefs/build_layout.mk b/builddefs/build_layout.mk index 6166bd847c..9ff99cc221 100644 --- a/builddefs/build_layout.mk +++ b/builddefs/build_layout.mk @@ -1,6 +1,10 @@ LAYOUTS_PATH := layouts LAYOUTS_REPOS := $(patsubst %/,%,$(sort $(dir $(wildcard $(LAYOUTS_PATH)/*/)))) +ifneq ($(QMK_USERSPACE),) + LAYOUTS_REPOS += $(patsubst %/,%,$(QMK_USERSPACE)/$(LAYOUTS_PATH)) +endif + define SEARCH_LAYOUTS_REPO LAYOUT_KEYMAP_PATH := $$(LAYOUTS_REPO)/$$(LAYOUT)/$$(KEYMAP) LAYOUT_KEYMAP_JSON := $$(LAYOUT_KEYMAP_PATH)/keymap.json diff --git a/builddefs/common_rules.mk b/builddefs/common_rules.mk index 52dccbe475..cfd261737c 100644 --- a/builddefs/common_rules.mk +++ b/builddefs/common_rules.mk @@ -191,7 +191,7 @@ DFU_SUFFIX_ARGS ?= elf: $(BUILD_DIR)/$(TARGET).elf hex: $(BUILD_DIR)/$(TARGET).hex uf2: $(BUILD_DIR)/$(TARGET).uf2 -cpfirmware: $(FIRMWARE_FORMAT) +cpfirmware_qmk: $(FIRMWARE_FORMAT) $(SILENT) || printf "Copying $(TARGET).$(FIRMWARE_FORMAT) to qmk_firmware folder" | $(AWK_CMD) $(COPY) $(BUILD_DIR)/$(TARGET).$(FIRMWARE_FORMAT) $(TARGET).$(FIRMWARE_FORMAT) && $(PRINT_OK) eep: $(BUILD_DIR)/$(TARGET).eep @@ -200,6 +200,15 @@ sym: $(BUILD_DIR)/$(TARGET).sym LIBNAME=lib$(TARGET).a lib: $(LIBNAME) +cpfirmware: cpfirmware_qmk + +ifneq ($(QMK_USERSPACE),) +cpfirmware: cpfirmware_userspace +cpfirmware_userspace: cpfirmware_qmk + $(SILENT) || printf "Copying $(TARGET).$(FIRMWARE_FORMAT) to userspace folder" | $(AWK_CMD) + $(COPY) $(BUILD_DIR)/$(TARGET).$(FIRMWARE_FORMAT) $(QMK_USERSPACE)/$(TARGET).$(FIRMWARE_FORMAT) && $(PRINT_OK) +endif + # Display size of file, modifying the output so people don't mistakenly grab the hex output BINARY_SIZE = $(SIZE) --target=$(FORMAT) $(BUILD_DIR)/$(TARGET).hex | $(SED) -e 's/\.build\/.*$$/$(TARGET).$(FIRMWARE_FORMAT)/g' diff --git a/data/schemas/definitions.jsonschema b/data/schemas/definitions.jsonschema index 441e6395cf..ea29343d0a 100644 --- a/data/schemas/definitions.jsonschema +++ b/data/schemas/definitions.jsonschema @@ -177,5 +177,23 @@ "type": "integer", "minimum": 0, "maximum": 1 + }, + "keyboard_keymap_tuple": { + "type": "array", + "prefixItems": [ + { "$ref": "#/keyboard" }, + { "$ref": "#/filename" } + ], + "unevaluatedItems": false + }, + "json_file_path": { + "type": "string", + "pattern": "^[0-9a-z_/\\-]+\\.json$" + }, + "build_target": { + "oneOf": [ + { "$ref": "#/keyboard_keymap_tuple" }, + { "$ref": "#/json_file_path" } + ] } } diff --git a/data/schemas/user_repo_v0.jsonschema b/data/schemas/user_repo_v0.jsonschema new file mode 100644 index 0000000000..b18ac50428 --- /dev/null +++ b/data/schemas/user_repo_v0.jsonschema @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema#", + "$id": "qmk.user_repo.v0", + "title": "User Repository Information", + "type": "object", + "required": [ + "userspace_version" + ], + "properties": { + "userspace_version": { + "type": "string", + }, + } +} diff --git a/data/schemas/user_repo_v1.jsonschema b/data/schemas/user_repo_v1.jsonschema new file mode 100644 index 0000000000..6cdf758685 --- /dev/null +++ b/data/schemas/user_repo_v1.jsonschema @@ -0,0 +1,22 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema#", + "$id": "qmk.user_repo.v1", + "title": "User Repository Information", + "type": "object", + "required": [ + "userspace_version", + "build_targets" + ], + "properties": { + "userspace_version": { + "type": "string", + "enum": ["1.0"] + }, + "build_targets": { + "type": "array", + "items": { + "$ref": "qmk.definitions.v1#/build_target" + } + } + } +} diff --git a/docs/_summary.md b/docs/_summary.md index 722c5f9c5d..36c90c5bb9 100644 --- a/docs/_summary.md +++ b/docs/_summary.md @@ -4,7 +4,7 @@ * [Building Your First Firmware](newbs_building_firmware.md) * [Flashing Firmware](newbs_flashing.md) * [Getting Help/Support](support.md) - * [Building With GitHub Userspace](newbs_building_firmware_workflow.md) + * [External Userspace](newbs_external_userspace.md) * [Other Resources](newbs_learn_more_resources.md) * [Syllabus](syllabus.md) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 79fd9de575..7b5ad5b13a 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -482,6 +482,131 @@ $ qmk import-kbfirmware ~/Downloads/gh62.json --- +# External Userspace Commands + +## `qmk userspace-add` + +This command adds a keyboard/keymap to the External Userspace build targets. + +**Usage**: + +``` +qmk userspace-add [-h] [-km KEYMAP] [-kb KEYBOARD] [builds ...] + +positional arguments: + builds List of builds in form :, or path to a keymap JSON file. + +options: + -h, --help show this help message and exit + -km KEYMAP, --keymap KEYMAP + The keymap to build a firmware for. Ignored when a configurator export is supplied. + -kb KEYBOARD, --keyboard KEYBOARD + The keyboard to build a firmware for. Ignored when a configurator export is supplied. +``` + +**Example**: + +``` +$ qmk userspace-add -kb planck/rev6 -km default +Ψ Added planck/rev6:default to userspace build targets +Ψ Saved userspace file to /home/you/qmk_userspace/qmk.json +``` + +## `qmk userspace-remove` + +This command removes a keyboard/keymap from the External Userspace build targets. + +**Usage**: + +``` +qmk userspace-remove [-h] [-km KEYMAP] [-kb KEYBOARD] [builds ...] + +positional arguments: + builds List of builds in form :, or path to a keymap JSON file. + +options: + -h, --help show this help message and exit + -km KEYMAP, --keymap KEYMAP + The keymap to build a firmware for. Ignored when a configurator export is supplied. + -kb KEYBOARD, --keyboard KEYBOARD + The keyboard to build a firmware for. Ignored when a configurator export is supplied. +``` + +**Example**: + +``` +$ qmk userspace-remove -kb planck/rev6 -km default +Ψ Removed planck/rev6:default from userspace build targets +Ψ Saved userspace file to /home/you/qmk_userspace/qmk.json +``` + +## `qmk userspace-list` + +This command lists the External Userspace build targets. + +**Usage**: + +``` +qmk userspace-list [-h] [-e] + +options: + -h, --help show this help message and exit + -e, --expand Expands any use of `all` for either keyboard or keymap. +``` + +**Example**: + +``` +$ qmk userspace-list +Ψ Current userspace build targets: +Ψ Keyboard: planck/rev6, keymap: you +Ψ Keyboard: clueboard/66/rev3, keymap: you +``` + +## `qmk userspace-compile` + +This command compiles all the External Userspace build targets. + +**Usage**: + +``` +qmk userspace-compile [-h] [-e ENV] [-n] [-c] [-j PARALLEL] [-t] + +options: + -h, --help show this help message and exit + -e ENV, --env ENV Set a variable to be passed to make. May be passed multiple times. + -n, --dry-run Don't actually build, just show the commands to be run. + -c, --clean Remove object files before compiling. + -j PARALLEL, --parallel PARALLEL + Set the number of parallel make jobs; 0 means unlimited. + -t, --no-temp Remove temporary files during build. +``` + +**Example**: + +``` +$ qmk userspace-compile +Ψ Preparing target list... +Build planck/rev6:you [OK] +Build clueboard/66/rev3:you [OK] +``` + +## `qmk userspace-doctor` + +This command examines your environment and alerts you to potential problems related to External Userspace. + +**Example**: + +``` +% qmk userspace-doctor +Ψ QMK home: /home/you/qmk_userspace/qmk_firmware +Ψ Testing userspace candidate: /home/you/qmk_userspace -- Valid `qmk.json` +Ψ QMK userspace: /home/you/qmk_userspace +Ψ Userspace enabled: True +``` + +--- + # Developer Commands ## `qmk format-text` diff --git a/docs/newbs_external_userspace.md b/docs/newbs_external_userspace.md new file mode 100644 index 0000000000..9bdf4b0b18 --- /dev/null +++ b/docs/newbs_external_userspace.md @@ -0,0 +1,96 @@ +# External QMK Userspace + +QMK Firmware now officially supports storing user keymaps outside of the normal QMK Firmware repository, allowing users to maintain their own keymaps without having to fork, modify, and maintain a copy of QMK Firmware themselves. + +External Userspace mirrors the structure of the main QMK Firmware repository, but only contains the keymaps that you wish to build. You can still use `keyboards//keymaps/` to store your keymaps, or you can use the `layouts//` system as before -- they're just stored external to QMK Firmware. + +The build system will still honor the use of `users/` if you rely on the traditional QMK Firmware [userspace feature](feature_userspace.md) -- it's now supported externally too, using the same location inside the External Userspace directory. + +Additionally, there is first-class support for using GitHub Actions to build your keymaps, allowing you to automatically compile your keymaps whenever you push changes to your External Userspace repository. + +!> External Userspace is new functionality and may have issues. Tighter integration with the `qmk` command will occur over time. + +?> Historical keymap.json and GitHub-based firmware build instructions can be found [here](newbs_building_firmware_workflow.md). This document supersedes those instructions, but they should still function correctly. + +## Setting up QMK Locally + +If you wish to build on your local machine, you will need to set up QMK locally. This is a one-time process, and is documented in the [newbs setup guide](https://docs.qmk.fm/#/newbs). + +!> If you wish to use any QMK CLI commands related to manipulating External Userspace definitions, you will currently need a copy of QMK Firmware as well. + +!> Building locally has a much shorter turnaround time than waiting for GitHub Actions to complete. + +## External Userspace Repository Setup (forked on GitHub) + +A basic skeleton External Userspace repository can be found [here](https://github.com/qmk/qmk_userspace). If you wish to keep your keymaps on GitHub (strongly recommended!), you can fork the repository and use it as a base: + +![Userspace Fork](https://i.imgur.com/hcegguh.png) + +Going ahead with your fork will copy it to your account, at which point you can clone it to your local machine and begin adding your keymaps: + +![Userspace Clone](https://i.imgur.com/CWYmsk8.png) + +```sh +cd $HOME +git clone https://github.com/{myusername}/qmk_userspace.git +qmk config user.overlay_dir="$(realpath qmk_userspace)" +``` + +## External Userspace Setup (locally stored only) + +If you don't want to use GitHub and prefer to keep everything local, you can clone a copy of the default External Userspace locally instead: + +```sh +cd $HOME +git clone https://github.com/qmk/qmk_userspace.git +qmk config user.overlay_dir="$(realpath qmk_userspace)" +``` + +## Adding a Keymap + +_These instructions assume you have already set up QMK locally, and have a copy of the QMK Firmware repository on your machine._ + +Keymaps within External Userspace are defined in the same way as they are in the main QMK repository. You can either use the `qmk new-keymap` command to create a new keymap, or manually create a new directory in the `keyboards` directory. + +Alternatively, you can use the `layouts` directory to store your keymaps, using the same layout system as the main QMK repository -- if you choose to do so you'll want to use the path `layouts///keymap.*` to store your keymap files, where `layout name` matches an existing layout in QMK, such as `tkl_ansi`. + +After creating your new keymap, building the keymap matches normal QMK usage: + +```sh +qmk compile -kb -km +``` + +!> The `qmk config user.overlay_dir=...` command must have been run when cloning the External Userspace repository for this to work correctly. + +## Adding the keymap to External Userspace build targets + +Once you have created your keymap, if you want to use GitHub Actions to build your firmware, you will need to add it to the External Userspace build targets. This is done using the `qmk userspace-add` command: + +```sh +# for a keyboard/keymap combo: +qmk userspace-add -kb -km +# or, for a json-based keymap (if kept "loose"): +qmk userspace-add +``` + +This updates the `qmk.json` file in the root of your External Userspace directory. If you're using a git repository to store your keymaps, now is a great time to commit and push to your own fork. + +## Compiling External Userspace build targets + +Once you have added your keymaps to the External Userspace build targets, you can compile all of them at once using the `qmk userspace-compile` command: + +```sh +qmk userspace-compile +``` + +All firmware builds you've added to the External Userspace build targets will be built, and the resulting firmware files will be placed in the root of your External Userspace directory. + +## Using GitHub Actions + +GitHub Actions can be used to automatically build your keymaps whenever you push changes to your External Userspace repository. If you have set up your list of build targets, this is as simple as enabling workflows in the GitHub repository settings: + +![Repo Settings](https://i.imgur.com/EVkxOt1.png) + +Any push will result in compilation of all configured builds, and once completed a new release containing the newly-minted firmware files will be created on GitHub, which you can subsequently download and flash to your keyboard: + +![Releases](https://i.imgur.com/zmwOL5P.png) diff --git a/lib/python/qmk/build_targets.py b/lib/python/qmk/build_targets.py index 16a7ef87a2..1ab489cec3 100644 --- a/lib/python/qmk/build_targets.py +++ b/lib/python/qmk/build_targets.py @@ -10,6 +10,8 @@ from qmk.constants import QMK_FIRMWARE, INTERMEDIATE_OUTPUT_PREFIX from qmk.commands import find_make, get_make_parallel_args, parse_configurator_json from qmk.keyboard import keyboard_folder from qmk.info import keymap_json +from qmk.keymap import locate_keymap +from qmk.path import is_under_qmk_firmware, is_under_qmk_userspace class BuildTarget: @@ -158,6 +160,20 @@ class KeyboardKeymapBuildTarget(BuildTarget): for key, value in env_vars.items(): compile_args.append(f'{key}={value}') + # Need to override the keymap path if the keymap is a userspace directory. + # This also ensures keyboard aliases as per `keyboard_aliases.hjson` still work if the userspace has the keymap + # in an equivalent historical location. + keymap_location = locate_keymap(self.keyboard, self.keymap) + if is_under_qmk_userspace(keymap_location) and not is_under_qmk_firmware(keymap_location): + keymap_directory = keymap_location.parent + compile_args.extend([ + f'MAIN_KEYMAP_PATH_1={keymap_directory}', + f'MAIN_KEYMAP_PATH_2={keymap_directory}', + f'MAIN_KEYMAP_PATH_3={keymap_directory}', + f'MAIN_KEYMAP_PATH_4={keymap_directory}', + f'MAIN_KEYMAP_PATH_5={keymap_directory}', + ]) + return compile_args diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index 695a180066..cf60903687 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py @@ -81,6 +81,11 @@ subcommands = [ 'qmk.cli.new.keymap', 'qmk.cli.painter', 'qmk.cli.pytest', + 'qmk.cli.userspace.add', + 'qmk.cli.userspace.compile', + 'qmk.cli.userspace.doctor', + 'qmk.cli.userspace.list', + 'qmk.cli.userspace.remove', 'qmk.cli.via2json', ] diff --git a/lib/python/qmk/cli/compile.py b/lib/python/qmk/cli/compile.py index 71c1dec162..3c8f3664ea 100755 --- a/lib/python/qmk/cli/compile.py +++ b/lib/python/qmk/cli/compile.py @@ -37,7 +37,9 @@ def compile(cli): from .mass_compile import mass_compile cli.args.builds = [] cli.args.filter = [] - cli.args.no_temp = False + cli.config.mass_compile.keymap = cli.config.compile.keymap + cli.config.mass_compile.parallel = cli.config.compile.parallel + cli.config.mass_compile.no_temp = False return mass_compile(cli) # Build the environment vars diff --git a/lib/python/qmk/cli/doctor/main.py b/lib/python/qmk/cli/doctor/main.py index 6a6feb87d1..dd8b58b2c7 100755 --- a/lib/python/qmk/cli/doctor/main.py +++ b/lib/python/qmk/cli/doctor/main.py @@ -9,10 +9,11 @@ from milc import cli from milc.questions import yesno from qmk import submodules -from qmk.constants import QMK_FIRMWARE, QMK_FIRMWARE_UPSTREAM +from qmk.constants import QMK_FIRMWARE, QMK_FIRMWARE_UPSTREAM, QMK_USERSPACE, HAS_QMK_USERSPACE from .check import CheckStatus, check_binaries, check_binary_versions, check_submodules from qmk.git import git_check_repo, git_get_branch, git_get_tag, git_get_last_log_entry, git_get_common_ancestor, git_is_dirty, git_get_remotes, git_check_deviation from qmk.commands import in_virtualenv +from qmk.userspace import qmk_userspace_paths, qmk_userspace_validate, UserspaceValidationError def os_tests(): @@ -92,6 +93,25 @@ def output_submodule_status(): cli.log.error(f'- {sub_name}: <<< missing or unknown >>>') +def userspace_tests(qmk_firmware): + if qmk_firmware: + cli.log.info(f'QMK home: {{fg_cyan}}{qmk_firmware}') + + for path in qmk_userspace_paths(): + try: + qmk_userspace_validate(path) + cli.log.info(f'Testing userspace candidate: {{fg_cyan}}{path}{{fg_reset}} -- {{fg_green}}Valid `qmk.json`') + except FileNotFoundError: + cli.log.warn(f'Testing userspace candidate: {{fg_cyan}}{path}{{fg_reset}} -- {{fg_red}}Missing `qmk.json`') + except UserspaceValidationError as err: + cli.log.warn(f'Testing userspace candidate: {{fg_cyan}}{path}{{fg_reset}} -- {{fg_red}}Invalid `qmk.json`') + cli.log.warn(f' -- {{fg_cyan}}{path}/qmk.json{{fg_reset}} validation error: {err}') + + if QMK_USERSPACE is not None: + cli.log.info(f'QMK userspace: {{fg_cyan}}{QMK_USERSPACE}') + cli.log.info(f'Userspace enabled: {{fg_cyan}}{HAS_QMK_USERSPACE}') + + @cli.argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.') @cli.argument('-n', '--no', action='store_true', arg_only=True, help='Answer no to all questions.') @cli.subcommand('Basic QMK environment checks') @@ -108,6 +128,9 @@ def doctor(cli): cli.log.info('QMK home: {fg_cyan}%s', QMK_FIRMWARE) status = os_status = os_tests() + + userspace_tests(None) + git_status = git_tests() if git_status == CheckStatus.ERROR or (os_status == CheckStatus.OK and git_status == CheckStatus.WARNING): diff --git a/lib/python/qmk/cli/format/json.py b/lib/python/qmk/cli/format/json.py index 3299a0d807..283513254c 100755 --- a/lib/python/qmk/cli/format/json.py +++ b/lib/python/qmk/cli/format/json.py @@ -9,48 +9,74 @@ from milc import cli from qmk.info import info_json from qmk.json_schema import json_load, validate -from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder +from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder, UserspaceJSONEncoder from qmk.path import normpath -@cli.argument('json_file', arg_only=True, type=normpath, help='JSON file to format') -@cli.argument('-f', '--format', choices=['auto', 'keyboard', 'keymap'], default='auto', arg_only=True, help='JSON formatter to use (Default: autodetect)') -@cli.argument('-i', '--inplace', action='store_true', arg_only=True, help='If set, will operate in-place on the input file') -@cli.argument('-p', '--print', action='store_true', arg_only=True, help='If set, will print the formatted json to stdout ') -@cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True) -def format_json(cli): - """Format a json file. +def _detect_json_format(file, json_data): + """Detect the format of a json file. """ - json_file = json_load(cli.args.json_file) - - if cli.args.format == 'auto': + json_encoder = None + try: + validate(json_data, 'qmk.user_repo.v1') + json_encoder = UserspaceJSONEncoder + except ValidationError: + pass + + if json_encoder is None: try: - validate(json_file, 'qmk.keyboard.v1') + validate(json_data, '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) + cli.log.warning('File %s did not validate as a keyboard info.json or userspace qmk.json:\n\t%s', file, e) + cli.log.info('Treating %s as a keymap file.', file) json_encoder = KeymapJSONEncoder + + return json_encoder + + +def _get_json_encoder(file, json_data): + """Get the json encoder for a file. + """ + json_encoder = None + if cli.args.format == 'auto': + json_encoder = _detect_json_format(file, json_data) elif cli.args.format == 'keyboard': json_encoder = InfoJSONEncoder elif cli.args.format == 'keymap': json_encoder = KeymapJSONEncoder + elif cli.args.format == 'userspace': + json_encoder = UserspaceJSONEncoder else: # This should be impossible cli.log.error('Unknown format: %s', cli.args.format) + return json_encoder + + +@cli.argument('json_file', arg_only=True, type=normpath, help='JSON file to format') +@cli.argument('-f', '--format', choices=['auto', 'keyboard', 'keymap', 'userspace'], default='auto', arg_only=True, help='JSON formatter to use (Default: autodetect)') +@cli.argument('-i', '--inplace', action='store_true', arg_only=True, help='If set, will operate in-place on the input file') +@cli.argument('-p', '--print', action='store_true', arg_only=True, help='If set, will print the formatted json to stdout ') +@cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True) +def format_json(cli): + """Format a json file. + """ + json_data = json_load(cli.args.json_file) + + json_encoder = _get_json_encoder(cli.args.json_file, json_data) + if json_encoder is None: return False - if json_encoder == KeymapJSONEncoder and 'layout' in json_file: + if json_encoder == KeymapJSONEncoder and 'layout' in json_data: # Attempt to format the keycodes. - layout = json_file['layout'] - info_data = info_json(json_file['keyboard']) + layout = json_data['layout'] + info_data = info_json(json_data['keyboard']) if layout in info_data.get('layout_aliases', {}): - layout = json_file['layout'] = info_data['layout_aliases'][layout] + layout = json_data['layout'] = info_data['layout_aliases'][layout] if layout in info_data.get('layouts'): - for layer_num, layer in enumerate(json_file['layers']): + for layer_num, layer in enumerate(json_data['layers']): current_layer = [] last_row = 0 @@ -61,9 +87,9 @@ def format_json(cli): current_layer.append(keymap_key) - json_file['layers'][layer_num] = current_layer + json_data['layers'][layer_num] = current_layer - output = json.dumps(json_file, cls=json_encoder, sort_keys=True) + output = json.dumps(json_data, cls=json_encoder, sort_keys=True) if cli.args.inplace: with open(cli.args.json_file, 'w+', encoding='utf-8') as outfile: diff --git a/lib/python/qmk/cli/mass_compile.py b/lib/python/qmk/cli/mass_compile.py index 7968de53e7..b025f85701 100755 --- a/lib/python/qmk/cli/mass_compile.py +++ b/lib/python/qmk/cli/mass_compile.py @@ -72,7 +72,7 @@ all: {keyboard_safe}_{keymap_name}_binary # yapf: enable f.write('\n') - cli.run([make_cmd, *get_make_parallel_args(parallel), '-f', makefile.as_posix(), 'all'], capture_output=False, stdin=DEVNULL) + cli.run([find_make(), *get_make_parallel_args(parallel), '-f', makefile.as_posix(), 'all'], capture_output=False, stdin=DEVNULL) # Check for failures failures = [f for f in builddir.glob(f'failed.log.{os.getpid()}.*')] diff --git a/lib/python/qmk/cli/new/keymap.py b/lib/python/qmk/cli/new/keymap.py index 9b0ac221a4..d4339bc9ef 100755 --- a/lib/python/qmk/cli/new/keymap.py +++ b/lib/python/qmk/cli/new/keymap.py @@ -5,10 +5,12 @@ import shutil from milc import cli from milc.questions import question +from qmk.constants import HAS_QMK_USERSPACE, QMK_USERSPACE from qmk.path import is_keyboard, keymaps, keymap from qmk.git import git_get_username from qmk.decorators import automagic_keyboard, automagic_keymap from qmk.keyboard import keyboard_completer, keyboard_folder +from qmk.userspace import UserspaceDefs def prompt_keyboard(): @@ -68,3 +70,9 @@ def new_keymap(cli): # end message to user cli.log.info(f'{{fg_green}}Created a new keymap called {{fg_cyan}}{user_name}{{fg_green}} in: {{fg_cyan}}{keymap_path_new}.{{fg_reset}}') cli.log.info(f"Compile a firmware with your new keymap by typing: {{fg_yellow}}qmk compile -kb {kb_name} -km {user_name}{{fg_reset}}.") + + # Add to userspace compile if we have userspace available + if HAS_QMK_USERSPACE: + userspace = UserspaceDefs(QMK_USERSPACE / 'qmk.json') + userspace.add_target(keyboard=kb_name, keymap=user_name, do_print=False) + return userspace.save() diff --git a/lib/python/qmk/cli/userspace/__init__.py b/lib/python/qmk/cli/userspace/__init__.py new file mode 100644 index 0000000000..5757d3a4c9 --- /dev/null +++ b/lib/python/qmk/cli/userspace/__init__.py @@ -0,0 +1,5 @@ +from . import doctor +from . import add +from . import remove +from . import list +from . import compile diff --git a/lib/python/qmk/cli/userspace/add.py b/lib/python/qmk/cli/userspace/add.py new file mode 100644 index 0000000000..8993d54dba --- /dev/null +++ b/lib/python/qmk/cli/userspace/add.py @@ -0,0 +1,51 @@ +# Copyright 2023 Nick Brassel (@tzarc) +# SPDX-License-Identifier: GPL-2.0-or-later +from pathlib import Path +from milc import cli + +from qmk.constants import QMK_USERSPACE, HAS_QMK_USERSPACE +from qmk.keyboard import keyboard_completer, keyboard_folder_or_all +from qmk.keymap import keymap_completer, is_keymap_target +from qmk.userspace import UserspaceDefs + + +@cli.argument('builds', nargs='*', arg_only=True, help="List of builds in form :, or path to a keymap JSON file.") +@cli.argument('-kb', '--keyboard', type=keyboard_folder_or_all, completer=keyboard_completer, help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.') +@cli.argument('-km', '--keymap', completer=keymap_completer, help='The keymap to build a firmware for. Ignored when a configurator export is supplied.') +@cli.subcommand('Adds a build target to userspace `qmk.json`.') +def userspace_add(cli): + if not HAS_QMK_USERSPACE: + cli.log.error('Could not determine QMK userspace location. Please run `qmk doctor` or `qmk userspace-doctor` to diagnose.') + return False + + userspace = UserspaceDefs(QMK_USERSPACE / 'qmk.json') + + if len(cli.args.builds) > 0: + json_like_targets = list([Path(p) for p in filter(lambda e: Path(e).exists() and Path(e).suffix == '.json', cli.args.builds)]) + make_like_targets = list(filter(lambda e: Path(e) not in json_like_targets, cli.args.builds)) + + for e in json_like_targets: + userspace.add_target(json_path=e) + + for e in make_like_targets: + s = e.split(':') + userspace.add_target(keyboard=s[0], keymap=s[1]) + + else: + failed = False + try: + if not is_keymap_target(cli.args.keyboard, cli.args.keymap): + failed = True + except KeyError: + failed = True + + if failed: + from qmk.cli.new.keymap import new_keymap + cli.config.new_keymap.keyboard = cli.args.keyboard + cli.config.new_keymap.keymap = cli.args.keymap + if new_keymap(cli) is not False: + userspace.add_target(keyboard=cli.args.keyboard, keymap=cli.args.keymap) + else: + userspace.add_target(keyboard=cli.args.keyboard, keymap=cli.args.keymap) + + return userspace.save() diff --git a/lib/python/qmk/cli/userspace/compile.py b/lib/python/qmk/cli/userspace/compile.py new file mode 100644 index 0000000000..0a42dd5bf5 --- /dev/null +++ b/lib/python/qmk/cli/userspace/compile.py @@ -0,0 +1,38 @@ +# Copyright 2023 Nick Brassel (@tzarc) +# SPDX-License-Identifier: GPL-2.0-or-later +from pathlib import Path +from milc import cli + +from qmk.constants import QMK_USERSPACE, HAS_QMK_USERSPACE +from qmk.commands import build_environment +from qmk.userspace import UserspaceDefs +from qmk.build_targets import JsonKeymapBuildTarget +from qmk.search import search_keymap_targets +from qmk.cli.mass_compile import mass_compile_targets + + +@cli.argument('-t', '--no-temp', arg_only=True, action='store_true', help="Remove temporary files during build.") +@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.") +@cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.") +@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the commands to be run.") +@cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.") +@cli.subcommand('Compiles the build targets specified in userspace `qmk.json`.') +def userspace_compile(cli): + if not HAS_QMK_USERSPACE: + cli.log.error('Could not determine QMK userspace location. Please run `qmk doctor` or `qmk userspace-doctor` to diagnose.') + return False + + userspace = UserspaceDefs(QMK_USERSPACE / 'qmk.json') + + build_targets = [] + keyboard_keymap_targets = [] + for e in userspace.build_targets: + if isinstance(e, Path): + build_targets.append(JsonKeymapBuildTarget(e)) + elif isinstance(e, dict): + keyboard_keymap_targets.append((e['keyboard'], e['keymap'])) + + if len(keyboard_keymap_targets) > 0: + build_targets.extend(search_keymap_targets(keyboard_keymap_targets)) + + mass_compile_targets(list(set(build_targets)), cli.args.clean, cli.args.dry_run, cli.config.userspace_compile.no_temp, cli.config.userspace_compile.parallel, **build_environment(cli.args.env)) diff --git a/lib/python/qmk/cli/userspace/doctor.py b/lib/python/qmk/cli/userspace/doctor.py new file mode 100644 index 0000000000..2b7e29aa7e --- /dev/null +++ b/lib/python/qmk/cli/userspace/doctor.py @@ -0,0 +1,11 @@ +# Copyright 2023 Nick Brassel (@tzarc) +# SPDX-License-Identifier: GPL-2.0-or-later +from milc import cli + +from qmk.constants import QMK_FIRMWARE +from qmk.cli.doctor.main import userspace_tests + + +@cli.subcommand('Checks userspace configuration.') +def userspace_doctor(cli): + userspace_tests(QMK_FIRMWARE) diff --git a/lib/python/qmk/cli/userspace/list.py b/lib/python/qmk/cli/userspace/list.py new file mode 100644 index 0000000000..a63f669dd7 --- /dev/null +++ b/lib/python/qmk/cli/userspace/list.py @@ -0,0 +1,51 @@ +# Copyright 2023 Nick Brassel (@tzarc) +# SPDX-License-Identifier: GPL-2.0-or-later +from pathlib import Path +from dotty_dict import Dotty +from milc import cli + +from qmk.constants import QMK_USERSPACE, HAS_QMK_USERSPACE +from qmk.userspace import UserspaceDefs +from qmk.build_targets import BuildTarget +from qmk.keyboard import is_all_keyboards, keyboard_folder +from qmk.keymap import is_keymap_target +from qmk.search import search_keymap_targets + + +@cli.argument('-e', '--expand', arg_only=True, action='store_true', help="Expands any use of `all` for either keyboard or keymap.") +@cli.subcommand('Lists the build targets specified in userspace `qmk.json`.') +def userspace_list(cli): + if not HAS_QMK_USERSPACE: + cli.log.error('Could not determine QMK userspace location. Please run `qmk doctor` or `qmk userspace-doctor` to diagnose.') + return False + + userspace = UserspaceDefs(QMK_USERSPACE / 'qmk.json') + + if cli.args.expand: + build_targets = [] + for e in userspace.build_targets: + if isinstance(e, Path): + build_targets.append(e) + elif isinstance(e, dict) or isinstance(e, Dotty): + build_targets.extend(search_keymap_targets([(e['keyboard'], e['keymap'])])) + else: + build_targets = userspace.build_targets + + for e in build_targets: + if isinstance(e, Path): + # JSON keymap from userspace + cli.log.info(f'JSON keymap: {{fg_cyan}}{e}{{fg_reset}}') + continue + elif isinstance(e, dict) or isinstance(e, Dotty): + # keyboard/keymap dict from userspace + keyboard = e['keyboard'] + keymap = e['keymap'] + elif isinstance(e, BuildTarget): + # BuildTarget from search_keymap_targets() + keyboard = e.keyboard + keymap = e.keymap + + if is_all_keyboards(keyboard) or is_keymap_target(keyboard_folder(keyboard), keymap): + cli.log.info(f'Keyboard: {{fg_cyan}}{keyboard}{{fg_reset}}, keymap: {{fg_cyan}}{keymap}{{fg_reset}}') + else: + cli.log.warn(f'Keyboard: {{fg_cyan}}{keyboard}{{fg_reset}}, keymap: {{fg_cyan}}{keymap}{{fg_reset}} -- not found!') diff --git a/lib/python/qmk/cli/userspace/remove.py b/lib/python/qmk/cli/userspace/remove.py new file mode 100644 index 0000000000..c7d180bfd1 --- /dev/null +++ b/lib/python/qmk/cli/userspace/remove.py @@ -0,0 +1,37 @@ +# Copyright 2023 Nick Brassel (@tzarc) +# SPDX-License-Identifier: GPL-2.0-or-later +from pathlib import Path +from milc import cli + +from qmk.constants import QMK_USERSPACE, HAS_QMK_USERSPACE +from qmk.keyboard import keyboard_completer, keyboard_folder_or_all +from qmk.keymap import keymap_completer +from qmk.userspace import UserspaceDefs + + +@cli.argument('builds', nargs='*', arg_only=True, help="List of builds in form :, or path to a keymap JSON file.") +@cli.argument('-kb', '--keyboard', type=keyboard_folder_or_all, completer=keyboard_completer, help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.') +@cli.argument('-km', '--keymap', completer=keymap_completer, help='The keymap to build a firmware for. Ignored when a configurator export is supplied.') +@cli.subcommand('Removes a build target from userspace `qmk.json`.') +def userspace_remove(cli): + if not HAS_QMK_USERSPACE: + cli.log.error('Could not determine QMK userspace location. Please run `qmk doctor` or `qmk userspace-doctor` to diagnose.') + return False + + userspace = UserspaceDefs(QMK_USERSPACE / 'qmk.json') + + if len(cli.args.builds) > 0: + json_like_targets = list([Path(p) for p in filter(lambda e: Path(e).exists() and Path(e).suffix == '.json', cli.args.builds)]) + make_like_targets = list(filter(lambda e: Path(e) not in json_like_targets, cli.args.builds)) + + for e in json_like_targets: + userspace.remove_target(json_path=e) + + for e in make_like_targets: + s = e.split(':') + userspace.remove_target(keyboard=s[0], keymap=s[1]) + + else: + userspace.remove_target(keyboard=cli.args.keyboard, keymap=cli.args.keymap) + + return userspace.save() diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py index 519cb4c708..d95ff5f923 100644 --- a/lib/python/qmk/commands.py +++ b/lib/python/qmk/commands.py @@ -3,10 +3,12 @@ import os import sys import shutil +from pathlib import Path from milc import cli import jsonschema +from qmk.constants import QMK_USERSPACE, HAS_QMK_USERSPACE from qmk.json_schema import json_load, validate from qmk.keyboard import keyboard_alias_definitions @@ -75,6 +77,10 @@ def build_environment(args): envs[key] = value else: cli.log.warning('Invalid environment variable: %s', env) + + if HAS_QMK_USERSPACE: + envs['QMK_USERSPACE'] = Path(QMK_USERSPACE).resolve() + return envs diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py index 1967441fc8..90e4452f2b 100644 --- a/lib/python/qmk/constants.py +++ b/lib/python/qmk/constants.py @@ -4,9 +4,17 @@ from os import environ from datetime import date from pathlib import Path +from qmk.userspace import detect_qmk_userspace + # The root of the qmk_firmware tree. QMK_FIRMWARE = Path.cwd() +# The detected userspace tree +QMK_USERSPACE = detect_qmk_userspace() + +# Whether or not we have a separate userspace directory +HAS_QMK_USERSPACE = True if QMK_USERSPACE is not None else False + # Upstream repo url QMK_FIRMWARE_UPSTREAM = 'qmk/qmk_firmware' diff --git a/lib/python/qmk/json_encoders.py b/lib/python/qmk/json_encoders.py index 1e90f6a288..0e4ad1d220 100755 --- a/lib/python/qmk/json_encoders.py +++ b/lib/python/qmk/json_encoders.py @@ -217,3 +217,21 @@ class KeymapJSONEncoder(QMKJSONEncoder): return '50' + str(key) return key + + +class UserspaceJSONEncoder(QMKJSONEncoder): + """Custom encoder to make userspace qmk.json's a little nicer to work with. + """ + def sort_dict(self, item): + """Sorts the hashes in a nice way. + """ + key = item[0] + + if self.indentation_level == 1: + if key == 'userspace_version': + return '00userspace_version' + + if key == 'build_targets': + return '01build_targets' + + return key diff --git a/lib/python/qmk/keyboard.py b/lib/python/qmk/keyboard.py index 34257bee8d..b56505d649 100644 --- a/lib/python/qmk/keyboard.py +++ b/lib/python/qmk/keyboard.py @@ -78,13 +78,17 @@ def keyboard_alias_definitions(): def is_all_keyboards(keyboard): """Returns True if the keyboard is an AllKeyboards object. """ + if isinstance(keyboard, str): + return (keyboard == 'all') return isinstance(keyboard, AllKeyboards) def find_keyboard_from_dir(): """Returns a keyboard name based on the user's current directory. """ - relative_cwd = qmk.path.under_qmk_firmware() + relative_cwd = qmk.path.under_qmk_userspace() + if not relative_cwd: + relative_cwd = qmk.path.under_qmk_firmware() if relative_cwd and len(relative_cwd.parts) > 1 and relative_cwd.parts[0] == 'keyboards': # Attempt to extract the keyboard name from the current directory @@ -133,6 +137,22 @@ def keyboard_folder(keyboard): return keyboard +def keyboard_aliases(keyboard): + """Returns the list of aliases for the supplied keyboard. + + Includes the keyboard itself. + """ + aliases = json_load(Path('data/mappings/keyboard_aliases.hjson')) + + if keyboard in aliases: + keyboard = aliases[keyboard].get('target', keyboard) + + keyboards = set(filter(lambda k: aliases[k].get('target', '') == keyboard, aliases.keys())) + keyboards.add(keyboard) + keyboards = list(sorted(keyboards)) + return keyboards + + def keyboard_folder_or_all(keyboard): """Returns the actual keyboard folder. diff --git a/lib/python/qmk/keymap.py b/lib/python/qmk/keymap.py index 281c53cfda..b7bf897377 100644 --- a/lib/python/qmk/keymap.py +++ b/lib/python/qmk/keymap.py @@ -12,7 +12,8 @@ from pygments.token import Token from pygments import lex import qmk.path -from qmk.keyboard import find_keyboard_from_dir, keyboard_folder +from qmk.constants import QMK_FIRMWARE, QMK_USERSPACE, HAS_QMK_USERSPACE +from qmk.keyboard import find_keyboard_from_dir, keyboard_folder, keyboard_aliases from qmk.errors import CppError from qmk.info import info_json @@ -194,29 +195,38 @@ def _strip_any(keycode): def find_keymap_from_dir(*args): """Returns `(keymap_name, source)` for the directory provided (or cwd if not specified). """ - relative_path = qmk.path.under_qmk_firmware(*args) + def _impl_find_keymap_from_dir(relative_path): + if relative_path and len(relative_path.parts) > 1: + # If we're in `qmk_firmware/keyboards` and `keymaps` is in our path, try to find the keyboard name. + if relative_path.parts[0] == 'keyboards' and 'keymaps' in relative_path.parts: + current_path = Path('/'.join(relative_path.parts[1:])) # Strip 'keyboards' from the front - if relative_path and len(relative_path.parts) > 1: - # If we're in `qmk_firmware/keyboards` and `keymaps` is in our path, try to find the keyboard name. - if relative_path.parts[0] == 'keyboards' and 'keymaps' in relative_path.parts: - current_path = Path('/'.join(relative_path.parts[1:])) # Strip 'keyboards' from the front + if 'keymaps' in current_path.parts and current_path.name != 'keymaps': + while current_path.parent.name != 'keymaps': + current_path = current_path.parent - if 'keymaps' in current_path.parts and current_path.name != 'keymaps': - while current_path.parent.name != 'keymaps': - current_path = current_path.parent + return current_path.name, 'keymap_directory' - return current_path.name, 'keymap_directory' + # If we're in `qmk_firmware/layouts` guess the name from the community keymap they're in + elif relative_path.parts[0] == 'layouts' and is_keymap_dir(relative_path): + return relative_path.name, 'layouts_directory' - # If we're in `qmk_firmware/layouts` guess the name from the community keymap they're in - elif relative_path.parts[0] == 'layouts' and is_keymap_dir(relative_path): - return relative_path.name, 'layouts_directory' + # If we're in `qmk_firmware/users` guess the name from the userspace they're in + elif relative_path.parts[0] == 'users': + # Guess the keymap name based on which userspace they're in + return relative_path.parts[1], 'users_directory' + return None, None - # If we're in `qmk_firmware/users` guess the name from the userspace they're in - elif relative_path.parts[0] == 'users': - # Guess the keymap name based on which userspace they're in - return relative_path.parts[1], 'users_directory' + if HAS_QMK_USERSPACE: + name, source = _impl_find_keymap_from_dir(qmk.path.under_qmk_userspace(*args)) + if name and source: + return name, source - return None, None + name, source = _impl_find_keymap_from_dir(qmk.path.under_qmk_firmware(*args)) + if name and source: + return name, source + + return (None, None) def keymap_completer(prefix, action, parser, parsed_args): @@ -417,29 +427,45 @@ def locate_keymap(keyboard, keymap): raise KeyError('Invalid keyboard: ' + repr(keyboard)) # Check the keyboard folder first, last match wins - checked_dirs = '' keymap_path = '' - for dir in keyboard_folder(keyboard).split('/'): - if checked_dirs: - checked_dirs = '/'.join((checked_dirs, dir)) - else: - checked_dirs = dir + search_dirs = [QMK_FIRMWARE] + keyboard_dirs = [keyboard_folder(keyboard)] + if HAS_QMK_USERSPACE: + # When we've got userspace, check there _last_ as we want them to override anything in the main repo. + search_dirs.append(QMK_USERSPACE) + # We also want to search for any aliases as QMK's folder structure may have changed, with an alias, but the user + # hasn't updated their keymap location yet. + keyboard_dirs.extend(keyboard_aliases(keyboard)) + keyboard_dirs = list(set(keyboard_dirs)) + + for search_dir in search_dirs: + for keyboard_dir in keyboard_dirs: + checked_dirs = '' + for dir in keyboard_dir.split('/'): + if checked_dirs: + checked_dirs = '/'.join((checked_dirs, dir)) + else: + checked_dirs = dir - keymap_dir = Path('keyboards') / checked_dirs / 'keymaps' + keymap_dir = Path(search_dir) / Path('keyboards') / checked_dirs / 'keymaps' - if (keymap_dir / keymap / 'keymap.c').exists(): - keymap_path = keymap_dir / keymap / 'keymap.c' - if (keymap_dir / keymap / 'keymap.json').exists(): - keymap_path = keymap_dir / keymap / 'keymap.json' + if (keymap_dir / keymap / 'keymap.c').exists(): + keymap_path = keymap_dir / keymap / 'keymap.c' + if (keymap_dir / keymap / 'keymap.json').exists(): + keymap_path = keymap_dir / keymap / 'keymap.json' - if keymap_path: - return keymap_path + if keymap_path: + return keymap_path # Check community layouts as a fallback info = info_json(keyboard) - for community_parent in Path('layouts').glob('*/'): + community_parents = list(Path('layouts').glob('*/')) + if HAS_QMK_USERSPACE and (Path(QMK_USERSPACE) / "layouts").exists(): + community_parents.append(Path(QMK_USERSPACE) / "layouts") + + for community_parent in community_parents: for layout in info.get("community_layouts", []): community_layout = community_parent / layout / keymap if community_layout.exists(): @@ -449,6 +475,16 @@ def locate_keymap(keyboard, keymap): return community_layout / 'keymap.c' +def is_keymap_target(keyboard, keymap): + if keymap == 'all': + return True + + if locate_keymap(keyboard, keymap): + return True + + return False + + def list_keymaps(keyboard, c=True, json=True, additional_files=None, fullpath=False): """List the available keymaps for a keyboard. @@ -473,26 +509,30 @@ def list_keymaps(keyboard, c=True, json=True, additional_files=None, fullpath=Fa """ names = set() - keyboards_dir = Path('keyboards') - kb_path = keyboards_dir / keyboard - # walk up the directory tree until keyboards_dir # and collect all directories' name with keymap.c file in it - while kb_path != keyboards_dir: - keymaps_dir = kb_path / "keymaps" - - if keymaps_dir.is_dir(): - for keymap in keymaps_dir.iterdir(): - if is_keymap_dir(keymap, c, json, additional_files): - keymap = keymap if fullpath else keymap.name - names.add(keymap) + for search_dir in [QMK_FIRMWARE, QMK_USERSPACE] if HAS_QMK_USERSPACE else [QMK_FIRMWARE]: + keyboards_dir = search_dir / Path('keyboards') + kb_path = keyboards_dir / keyboard + + while kb_path != keyboards_dir: + keymaps_dir = kb_path / "keymaps" + if keymaps_dir.is_dir(): + for keymap in keymaps_dir.iterdir(): + if is_keymap_dir(keymap, c, json, additional_files): + keymap = keymap if fullpath else keymap.name + names.add(keymap) - kb_path = kb_path.parent + kb_path = kb_path.parent # Check community layouts as a fallback info = info_json(keyboard) - for community_parent in Path('layouts').glob('*/'): + community_parents = list(Path('layouts').glob('*/')) + if HAS_QMK_USERSPACE and (Path(QMK_USERSPACE) / "layouts").exists(): + community_parents.append(Path(QMK_USERSPACE) / "layouts") + + for community_parent in community_parents: for layout in info.get("community_layouts", []): cl_path = community_parent / layout if cl_path.is_dir(): diff --git a/lib/python/qmk/path.py b/lib/python/qmk/path.py index 94582a05e0..74364ee04b 100644 --- a/lib/python/qmk/path.py +++ b/lib/python/qmk/path.py @@ -5,7 +5,7 @@ import os import argparse from pathlib import Path -from qmk.constants import MAX_KEYBOARD_SUBFOLDERS, QMK_FIRMWARE +from qmk.constants import MAX_KEYBOARD_SUBFOLDERS, QMK_FIRMWARE, QMK_USERSPACE, HAS_QMK_USERSPACE from qmk.errors import NoSuchKeyboardError @@ -28,6 +28,40 @@ def under_qmk_firmware(path=Path(os.environ['ORIG_CWD'])): return None +def under_qmk_userspace(path=Path(os.environ['ORIG_CWD'])): + """Returns a Path object representing the relative path under $QMK_USERSPACE, or None. + """ + try: + if HAS_QMK_USERSPACE: + return path.relative_to(QMK_USERSPACE) + except ValueError: + pass + return None + + +def is_under_qmk_firmware(path=Path(os.environ['ORIG_CWD'])): + """Returns a boolean if the input path is a child under qmk_firmware. + """ + if path is None: + return False + try: + return Path(os.path.commonpath([Path(path), QMK_FIRMWARE])) == QMK_FIRMWARE + except ValueError: + return False + + +def is_under_qmk_userspace(path=Path(os.environ['ORIG_CWD'])): + """Returns a boolean if the input path is a child under $QMK_USERSPACE. + """ + if path is None: + return False + try: + if HAS_QMK_USERSPACE: + return Path(os.path.commonpath([Path(path), QMK_USERSPACE])) == QMK_USERSPACE + except ValueError: + return False + + def keyboard(keyboard_name): """Returns the path to a keyboard's directory relative to the qmk root. """ @@ -45,11 +79,28 @@ def keymaps(keyboard_name): keyboard_folder = keyboard(keyboard_name) found_dirs = [] + if HAS_QMK_USERSPACE: + this_keyboard_folder = Path(QMK_USERSPACE) / keyboard_folder + for _ in range(MAX_KEYBOARD_SUBFOLDERS): + if (this_keyboard_folder / 'keymaps').exists(): + found_dirs.append((this_keyboard_folder / 'keymaps').resolve()) + + this_keyboard_folder = this_keyboard_folder.parent + if this_keyboard_folder.resolve() == QMK_USERSPACE.resolve(): + break + + # We don't have any relevant keymap directories in userspace, so we'll use the fully-qualified path instead. + if len(found_dirs) == 0: + found_dirs.append((QMK_USERSPACE / keyboard_folder / 'keymaps').resolve()) + + this_keyboard_folder = QMK_FIRMWARE / keyboard_folder for _ in range(MAX_KEYBOARD_SUBFOLDERS): - if (keyboard_folder / 'keymaps').exists(): - found_dirs.append((keyboard_folder / 'keymaps').resolve()) + if (this_keyboard_folder / 'keymaps').exists(): + found_dirs.append((this_keyboard_folder / 'keymaps').resolve()) - keyboard_folder = keyboard_folder.parent + this_keyboard_folder = this_keyboard_folder.parent + if this_keyboard_folder.resolve() == QMK_FIRMWARE.resolve(): + break if len(found_dirs) > 0: return found_dirs diff --git a/lib/python/qmk/userspace.py b/lib/python/qmk/userspace.py new file mode 100644 index 0000000000..3783568006 --- /dev/null +++ b/lib/python/qmk/userspace.py @@ -0,0 +1,185 @@ +# Copyright 2023 Nick Brassel (@tzarc) +# SPDX-License-Identifier: GPL-2.0-or-later +from os import environ +from pathlib import Path +import json +import jsonschema + +from milc import cli + +from qmk.json_schema import validate, json_load +from qmk.json_encoders import UserspaceJSONEncoder + + +def qmk_userspace_paths(): + test_dirs = [] + + # If we're already in a directory with a qmk.json and a keyboards or layouts directory, interpret it as userspace + current_dir = Path(environ['ORIG_CWD']) + while len(current_dir.parts) > 1: + if (current_dir / 'qmk.json').is_file(): + test_dirs.append(current_dir) + current_dir = current_dir.parent + + # If we have a QMK_USERSPACE environment variable, use that + if environ.get('QMK_USERSPACE') is not None: + current_dir = Path(environ.get('QMK_USERSPACE')) + if current_dir.is_dir(): + test_dirs.append(current_dir) + + # If someone has configured a directory, use that + if cli.config.user.overlay_dir is not None: + current_dir = Path(cli.config.user.overlay_dir) + if current_dir.is_dir(): + test_dirs.append(current_dir) + + return test_dirs + + +def qmk_userspace_validate(path): + # Construct a UserspaceDefs object to ensure it validates correctly + if (path / 'qmk.json').is_file(): + UserspaceDefs(path / 'qmk.json') + return + + # No qmk.json file found + raise FileNotFoundError('No qmk.json file found.') + + +def detect_qmk_userspace(): + # Iterate through all the detected userspace paths and return the first one that validates correctly + test_dirs = qmk_userspace_paths() + for test_dir in test_dirs: + try: + qmk_userspace_validate(test_dir) + return test_dir + except FileNotFoundError: + continue + except UserspaceValidationError: + continue + return None + + +class UserspaceDefs: + def __init__(self, userspace_json: Path): + self.path = userspace_json + self.build_targets = [] + json = json_load(userspace_json) + + exception = UserspaceValidationError() + success = False + + try: + validate(json, 'qmk.user_repo.v0') # `qmk.json` must have a userspace_version at minimum + except jsonschema.ValidationError as err: + exception.add('qmk.user_repo.v0', err) + raise exception + + # Iterate through each version of the schema, starting with the latest and decreasing to v1 + try: + validate(json, 'qmk.user_repo.v1') + self.__load_v1(json) + success = True + except jsonschema.ValidationError as err: + exception.add('qmk.user_repo.v1', err) + + if not success: + raise exception + + def save(self): + target_json = { + "userspace_version": "1.0", # Needs to match latest version + "build_targets": [] + } + + for e in self.build_targets: + if isinstance(e, dict): + target_json['build_targets'].append([e['keyboard'], e['keymap']]) + elif isinstance(e, Path): + target_json['build_targets'].append(str(e.relative_to(self.path.parent))) + + try: + # Ensure what we're writing validates against the latest version of the schema + validate(target_json, 'qmk.user_repo.v1') + except jsonschema.ValidationError as err: + cli.log.error(f'Could not save userspace file: {err}') + return False + + # Only actually write out data if it changed + old_data = json.dumps(json.loads(self.path.read_text()), cls=UserspaceJSONEncoder, sort_keys=True) + new_data = json.dumps(target_json, cls=UserspaceJSONEncoder, sort_keys=True) + if old_data != new_data: + self.path.write_text(new_data) + cli.log.info(f'Saved userspace file to {self.path}.') + return True + + def add_target(self, keyboard=None, keymap=None, json_path=None, do_print=True): + if json_path is not None: + # Assume we're adding a json filename/path + json_path = Path(json_path) + if json_path not in self.build_targets: + self.build_targets.append(json_path) + if do_print: + cli.log.info(f'Added {json_path} to userspace build targets.') + else: + cli.log.info(f'{json_path} is already a userspace build target.') + + elif keyboard is not None and keymap is not None: + # Both keyboard/keymap specified + e = {"keyboard": keyboard, "keymap": keymap} + if e not in self.build_targets: + self.build_targets.append(e) + if do_print: + cli.log.info(f'Added {keyboard}:{keymap} to userspace build targets.') + else: + if do_print: + cli.log.info(f'{keyboard}:{keymap} is already a userspace build target.') + + def remove_target(self, keyboard=None, keymap=None, json_path=None, do_print=True): + if json_path is not None: + # Assume we're removing a json filename/path + json_path = Path(json_path) + if json_path in self.build_targets: + self.build_targets.remove(json_path) + if do_print: + cli.log.info(f'Removed {json_path} from userspace build targets.') + else: + cli.log.info(f'{json_path} is not a userspace build target.') + + elif keyboard is not None and keymap is not None: + # Both keyboard/keymap specified + e = {"keyboard": keyboard, "keymap": keymap} + if e in self.build_targets: + self.build_targets.remove(e) + if do_print: + cli.log.info(f'Removed {keyboard}:{keymap} from userspace build targets.') + else: + if do_print: + cli.log.info(f'{keyboard}:{keymap} is not a userspace build target.') + + def __load_v1(self, json): + for e in json['build_targets']: + if isinstance(e, list) and len(e) == 2: + self.add_target(keyboard=e[0], keymap=e[1], do_print=False) + if isinstance(e, str): + p = self.path.parent / e + if p.exists() and p.suffix == '.json': + self.add_target(json_path=p, do_print=False) + + +class UserspaceValidationError(Exception): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__exceptions = [] + + def __str__(self): + return self.message + + @property + def exceptions(self): + return self.__exceptions + + def add(self, schema, exception): + self.__exceptions.append((schema, exception)) + errorlist = "\n\n".join([f"{schema}: {exception}" for schema, exception in self.__exceptions]) + self.message = f'Could not validate against any version of the userspace schema. Errors:\n\n{errorlist}' -- cgit v1.2.3