diff options
Diffstat (limited to 'lib/python')
| -rw-r--r-- | lib/python/qmk/build_targets.py | 16 | ||||
| -rw-r--r-- | lib/python/qmk/cli/__init__.py | 5 | ||||
| -rwxr-xr-x | lib/python/qmk/cli/compile.py | 4 | ||||
| -rwxr-xr-x | lib/python/qmk/cli/doctor/main.py | 25 | ||||
| -rwxr-xr-x | lib/python/qmk/cli/format/json.py | 70 | ||||
| -rwxr-xr-x | lib/python/qmk/cli/mass_compile.py | 2 | ||||
| -rwxr-xr-x | lib/python/qmk/cli/new/keymap.py | 8 | ||||
| -rw-r--r-- | lib/python/qmk/cli/userspace/__init__.py | 5 | ||||
| -rw-r--r-- | lib/python/qmk/cli/userspace/add.py | 51 | ||||
| -rw-r--r-- | lib/python/qmk/cli/userspace/compile.py | 38 | ||||
| -rw-r--r-- | lib/python/qmk/cli/userspace/doctor.py | 11 | ||||
| -rw-r--r-- | lib/python/qmk/cli/userspace/list.py | 51 | ||||
| -rw-r--r-- | lib/python/qmk/cli/userspace/remove.py | 37 | ||||
| -rw-r--r-- | lib/python/qmk/commands.py | 6 | ||||
| -rw-r--r-- | lib/python/qmk/constants.py | 8 | ||||
| -rwxr-xr-x | lib/python/qmk/json_encoders.py | 18 | ||||
| -rw-r--r-- | lib/python/qmk/keyboard.py | 22 | ||||
| -rw-r--r-- | lib/python/qmk/keymap.py | 130 | ||||
| -rw-r--r-- | lib/python/qmk/path.py | 59 | ||||
| -rw-r--r-- | lib/python/qmk/userspace.py | 185 |
20 files changed, 676 insertions, 75 deletions
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 | |||
| 10 | from qmk.commands import find_make, get_make_parallel_args, parse_configurator_json | 10 | from qmk.commands import find_make, get_make_parallel_args, parse_configurator_json |
| 11 | from qmk.keyboard import keyboard_folder | 11 | from qmk.keyboard import keyboard_folder |
| 12 | from qmk.info import keymap_json | 12 | from qmk.info import keymap_json |
| 13 | from qmk.keymap import locate_keymap | ||
| 14 | from qmk.path import is_under_qmk_firmware, is_under_qmk_userspace | ||
| 13 | 15 | ||
| 14 | 16 | ||
| 15 | class BuildTarget: | 17 | class BuildTarget: |
| @@ -158,6 +160,20 @@ class KeyboardKeymapBuildTarget(BuildTarget): | |||
| 158 | for key, value in env_vars.items(): | 160 | for key, value in env_vars.items(): |
| 159 | compile_args.append(f'{key}={value}') | 161 | compile_args.append(f'{key}={value}') |
| 160 | 162 | ||
| 163 | # Need to override the keymap path if the keymap is a userspace directory. | ||
| 164 | # This also ensures keyboard aliases as per `keyboard_aliases.hjson` still work if the userspace has the keymap | ||
| 165 | # in an equivalent historical location. | ||
| 166 | keymap_location = locate_keymap(self.keyboard, self.keymap) | ||
| 167 | if is_under_qmk_userspace(keymap_location) and not is_under_qmk_firmware(keymap_location): | ||
| 168 | keymap_directory = keymap_location.parent | ||
| 169 | compile_args.extend([ | ||
| 170 | f'MAIN_KEYMAP_PATH_1={keymap_directory}', | ||
| 171 | f'MAIN_KEYMAP_PATH_2={keymap_directory}', | ||
| 172 | f'MAIN_KEYMAP_PATH_3={keymap_directory}', | ||
| 173 | f'MAIN_KEYMAP_PATH_4={keymap_directory}', | ||
| 174 | f'MAIN_KEYMAP_PATH_5={keymap_directory}', | ||
| 175 | ]) | ||
| 176 | |||
| 161 | return compile_args | 177 | return compile_args |
| 162 | 178 | ||
| 163 | 179 | ||
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 = [ | |||
| 81 | 'qmk.cli.new.keymap', | 81 | 'qmk.cli.new.keymap', |
| 82 | 'qmk.cli.painter', | 82 | 'qmk.cli.painter', |
| 83 | 'qmk.cli.pytest', | 83 | 'qmk.cli.pytest', |
| 84 | 'qmk.cli.userspace.add', | ||
| 85 | 'qmk.cli.userspace.compile', | ||
| 86 | 'qmk.cli.userspace.doctor', | ||
| 87 | 'qmk.cli.userspace.list', | ||
| 88 | 'qmk.cli.userspace.remove', | ||
| 84 | 'qmk.cli.via2json', | 89 | 'qmk.cli.via2json', |
| 85 | ] | 90 | ] |
| 86 | 91 | ||
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): | |||
| 37 | from .mass_compile import mass_compile | 37 | from .mass_compile import mass_compile |
| 38 | cli.args.builds = [] | 38 | cli.args.builds = [] |
| 39 | cli.args.filter = [] | 39 | cli.args.filter = [] |
| 40 | cli.args.no_temp = False | 40 | cli.config.mass_compile.keymap = cli.config.compile.keymap |
| 41 | cli.config.mass_compile.parallel = cli.config.compile.parallel | ||
| 42 | cli.config.mass_compile.no_temp = False | ||
| 41 | return mass_compile(cli) | 43 | return mass_compile(cli) |
| 42 | 44 | ||
| 43 | # Build the environment vars | 45 | # 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 | |||
| 9 | from milc.questions import yesno | 9 | from milc.questions import yesno |
| 10 | 10 | ||
| 11 | from qmk import submodules | 11 | from qmk import submodules |
| 12 | from qmk.constants import QMK_FIRMWARE, QMK_FIRMWARE_UPSTREAM | 12 | from qmk.constants import QMK_FIRMWARE, QMK_FIRMWARE_UPSTREAM, QMK_USERSPACE, HAS_QMK_USERSPACE |
| 13 | from .check import CheckStatus, check_binaries, check_binary_versions, check_submodules | 13 | from .check import CheckStatus, check_binaries, check_binary_versions, check_submodules |
| 14 | 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 | 14 | 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 |
| 15 | from qmk.commands import in_virtualenv | 15 | from qmk.commands import in_virtualenv |
| 16 | from qmk.userspace import qmk_userspace_paths, qmk_userspace_validate, UserspaceValidationError | ||
| 16 | 17 | ||
| 17 | 18 | ||
| 18 | def os_tests(): | 19 | def os_tests(): |
| @@ -92,6 +93,25 @@ def output_submodule_status(): | |||
| 92 | cli.log.error(f'- {sub_name}: <<< missing or unknown >>>') | 93 | cli.log.error(f'- {sub_name}: <<< missing or unknown >>>') |
| 93 | 94 | ||
| 94 | 95 | ||
| 96 | def userspace_tests(qmk_firmware): | ||
| 97 | if qmk_firmware: | ||
| 98 | cli.log.info(f'QMK home: {{fg_cyan}}{qmk_firmware}') | ||
| 99 | |||
| 100 | for path in qmk_userspace_paths(): | ||
| 101 | try: | ||
| 102 | qmk_userspace_validate(path) | ||
| 103 | cli.log.info(f'Testing userspace candidate: {{fg_cyan}}{path}{{fg_reset}} -- {{fg_green}}Valid `qmk.json`') | ||
| 104 | except FileNotFoundError: | ||
| 105 | cli.log.warn(f'Testing userspace candidate: {{fg_cyan}}{path}{{fg_reset}} -- {{fg_red}}Missing `qmk.json`') | ||
| 106 | except UserspaceValidationError as err: | ||
| 107 | cli.log.warn(f'Testing userspace candidate: {{fg_cyan}}{path}{{fg_reset}} -- {{fg_red}}Invalid `qmk.json`') | ||
| 108 | cli.log.warn(f' -- {{fg_cyan}}{path}/qmk.json{{fg_reset}} validation error: {err}') | ||
| 109 | |||
| 110 | if QMK_USERSPACE is not None: | ||
| 111 | cli.log.info(f'QMK userspace: {{fg_cyan}}{QMK_USERSPACE}') | ||
| 112 | cli.log.info(f'Userspace enabled: {{fg_cyan}}{HAS_QMK_USERSPACE}') | ||
| 113 | |||
| 114 | |||
| 95 | @cli.argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.') | 115 | @cli.argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.') |
| 96 | @cli.argument('-n', '--no', action='store_true', arg_only=True, help='Answer no to all questions.') | 116 | @cli.argument('-n', '--no', action='store_true', arg_only=True, help='Answer no to all questions.') |
| 97 | @cli.subcommand('Basic QMK environment checks') | 117 | @cli.subcommand('Basic QMK environment checks') |
| @@ -108,6 +128,9 @@ def doctor(cli): | |||
| 108 | cli.log.info('QMK home: {fg_cyan}%s', QMK_FIRMWARE) | 128 | cli.log.info('QMK home: {fg_cyan}%s', QMK_FIRMWARE) |
| 109 | 129 | ||
| 110 | status = os_status = os_tests() | 130 | status = os_status = os_tests() |
| 131 | |||
| 132 | userspace_tests(None) | ||
| 133 | |||
| 111 | git_status = git_tests() | 134 | git_status = git_tests() |
| 112 | 135 | ||
| 113 | if git_status == CheckStatus.ERROR or (os_status == CheckStatus.OK and git_status == CheckStatus.WARNING): | 136 | 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 | |||
| 9 | 9 | ||
| 10 | from qmk.info import info_json | 10 | from qmk.info import info_json |
| 11 | from qmk.json_schema import json_load, validate | 11 | from qmk.json_schema import json_load, validate |
| 12 | from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder | 12 | from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder, UserspaceJSONEncoder |
| 13 | from qmk.path import normpath | 13 | from qmk.path import normpath |
| 14 | 14 | ||
| 15 | 15 | ||
| 16 | @cli.argument('json_file', arg_only=True, type=normpath, help='JSON file to format') | 16 | def _detect_json_format(file, json_data): |
| 17 | @cli.argument('-f', '--format', choices=['auto', 'keyboard', 'keymap'], default='auto', arg_only=True, help='JSON formatter to use (Default: autodetect)') | 17 | """Detect the format of a json file. |
| 18 | @cli.argument('-i', '--inplace', action='store_true', arg_only=True, help='If set, will operate in-place on the input file') | ||
| 19 | @cli.argument('-p', '--print', action='store_true', arg_only=True, help='If set, will print the formatted json to stdout ') | ||
| 20 | @cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True) | ||
| 21 | def format_json(cli): | ||
| 22 | """Format a json file. | ||
| 23 | """ | 18 | """ |
| 24 | json_file = json_load(cli.args.json_file) | 19 | json_encoder = None |
| 25 | 20 | try: | |
| 26 | if cli.args.format == 'auto': | 21 | validate(json_data, 'qmk.user_repo.v1') |
| 22 | json_encoder = UserspaceJSONEncoder | ||
| 23 | except ValidationError: | ||
| 24 | pass | ||
| 25 | |||
| 26 | if json_encoder is None: | ||
| 27 | try: | 27 | try: |
| 28 | validate(json_file, 'qmk.keyboard.v1') | 28 | validate(json_data, 'qmk.keyboard.v1') |
| 29 | json_encoder = InfoJSONEncoder | 29 | json_encoder = InfoJSONEncoder |
| 30 | |||
| 31 | except ValidationError as e: | 30 | except ValidationError as e: |
| 32 | cli.log.warning('File %s did not validate as a keyboard:\n\t%s', cli.args.json_file, e) | 31 | cli.log.warning('File %s did not validate as a keyboard info.json or userspace qmk.json:\n\t%s', file, e) |
| 33 | cli.log.info('Treating %s as a keymap file.', cli.args.json_file) | 32 | cli.log.info('Treating %s as a keymap file.', file) |
| 34 | json_encoder = KeymapJSONEncoder | 33 | json_encoder = KeymapJSONEncoder |
| 34 | |||
| 35 | return json_encoder | ||
| 36 | |||
| 37 | |||
| 38 | def _get_json_encoder(file, json_data): | ||
| 39 | """Get the json encoder for a file. | ||
| 40 | """ | ||
| 41 | json_encoder = None | ||
| 42 | if cli.args.format == 'auto': | ||
| 43 | json_encoder = _detect_json_format(file, json_data) | ||
| 35 | elif cli.args.format == 'keyboard': | 44 | elif cli.args.format == 'keyboard': |
| 36 | json_encoder = InfoJSONEncoder | 45 | json_encoder = InfoJSONEncoder |
| 37 | elif cli.args.format == 'keymap': | 46 | elif cli.args.format == 'keymap': |
| 38 | json_encoder = KeymapJSONEncoder | 47 | json_encoder = KeymapJSONEncoder |
| 48 | elif cli.args.format == 'userspace': | ||
| 49 | json_encoder = UserspaceJSONEncoder | ||
| 39 | else: | 50 | else: |
| 40 | # This should be impossible | 51 | # This should be impossible |
| 41 | cli.log.error('Unknown format: %s', cli.args.format) | 52 | cli.log.error('Unknown format: %s', cli.args.format) |
| 53 | return json_encoder | ||
| 54 | |||
| 55 | |||
| 56 | @cli.argument('json_file', arg_only=True, type=normpath, help='JSON file to format') | ||
| 57 | @cli.argument('-f', '--format', choices=['auto', 'keyboard', 'keymap', 'userspace'], default='auto', arg_only=True, help='JSON formatter to use (Default: autodetect)') | ||
| 58 | @cli.argument('-i', '--inplace', action='store_true', arg_only=True, help='If set, will operate in-place on the input file') | ||
| 59 | @cli.argument('-p', '--print', action='store_true', arg_only=True, help='If set, will print the formatted json to stdout ') | ||
| 60 | @cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True) | ||
| 61 | def format_json(cli): | ||
| 62 | """Format a json file. | ||
| 63 | """ | ||
| 64 | json_data = json_load(cli.args.json_file) | ||
| 65 | |||
| 66 | json_encoder = _get_json_encoder(cli.args.json_file, json_data) | ||
| 67 | if json_encoder is None: | ||
| 42 | return False | 68 | return False |
| 43 | 69 | ||
| 44 | if json_encoder == KeymapJSONEncoder and 'layout' in json_file: | 70 | if json_encoder == KeymapJSONEncoder and 'layout' in json_data: |
| 45 | # Attempt to format the keycodes. | 71 | # Attempt to format the keycodes. |
| 46 | layout = json_file['layout'] | 72 | layout = json_data['layout'] |
| 47 | info_data = info_json(json_file['keyboard']) | 73 | info_data = info_json(json_data['keyboard']) |
| 48 | 74 | ||
| 49 | if layout in info_data.get('layout_aliases', {}): | 75 | if layout in info_data.get('layout_aliases', {}): |
| 50 | layout = json_file['layout'] = info_data['layout_aliases'][layout] | 76 | layout = json_data['layout'] = info_data['layout_aliases'][layout] |
| 51 | 77 | ||
| 52 | if layout in info_data.get('layouts'): | 78 | if layout in info_data.get('layouts'): |
| 53 | for layer_num, layer in enumerate(json_file['layers']): | 79 | for layer_num, layer in enumerate(json_data['layers']): |
| 54 | current_layer = [] | 80 | current_layer = [] |
| 55 | last_row = 0 | 81 | last_row = 0 |
| 56 | 82 | ||
| @@ -61,9 +87,9 @@ def format_json(cli): | |||
| 61 | 87 | ||
| 62 | current_layer.append(keymap_key) | 88 | current_layer.append(keymap_key) |
| 63 | 89 | ||
| 64 | json_file['layers'][layer_num] = current_layer | 90 | json_data['layers'][layer_num] = current_layer |
| 65 | 91 | ||
| 66 | output = json.dumps(json_file, cls=json_encoder, sort_keys=True) | 92 | output = json.dumps(json_data, cls=json_encoder, sort_keys=True) |
| 67 | 93 | ||
| 68 | if cli.args.inplace: | 94 | if cli.args.inplace: |
| 69 | with open(cli.args.json_file, 'w+', encoding='utf-8') as outfile: | 95 | 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 | |||
| 72 | # yapf: enable | 72 | # yapf: enable |
| 73 | f.write('\n') | 73 | f.write('\n') |
| 74 | 74 | ||
| 75 | cli.run([make_cmd, *get_make_parallel_args(parallel), '-f', makefile.as_posix(), 'all'], capture_output=False, stdin=DEVNULL) | 75 | cli.run([find_make(), *get_make_parallel_args(parallel), '-f', makefile.as_posix(), 'all'], capture_output=False, stdin=DEVNULL) |
| 76 | 76 | ||
| 77 | # Check for failures | 77 | # Check for failures |
| 78 | failures = [f for f in builddir.glob(f'failed.log.{os.getpid()}.*')] | 78 | 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 | |||
| 5 | from milc import cli | 5 | from milc import cli |
| 6 | from milc.questions import question | 6 | from milc.questions import question |
| 7 | 7 | ||
| 8 | from qmk.constants import HAS_QMK_USERSPACE, QMK_USERSPACE | ||
| 8 | from qmk.path import is_keyboard, keymaps, keymap | 9 | from qmk.path import is_keyboard, keymaps, keymap |
| 9 | from qmk.git import git_get_username | 10 | from qmk.git import git_get_username |
| 10 | from qmk.decorators import automagic_keyboard, automagic_keymap | 11 | from qmk.decorators import automagic_keyboard, automagic_keymap |
| 11 | from qmk.keyboard import keyboard_completer, keyboard_folder | 12 | from qmk.keyboard import keyboard_completer, keyboard_folder |
| 13 | from qmk.userspace import UserspaceDefs | ||
| 12 | 14 | ||
| 13 | 15 | ||
| 14 | def prompt_keyboard(): | 16 | def prompt_keyboard(): |
| @@ -68,3 +70,9 @@ def new_keymap(cli): | |||
| 68 | # end message to user | 70 | # end message to user |
| 69 | 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}}') | 71 | 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}}') |
| 70 | 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}}.") | 72 | 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}}.") |
| 73 | |||
| 74 | # Add to userspace compile if we have userspace available | ||
| 75 | if HAS_QMK_USERSPACE: | ||
| 76 | userspace = UserspaceDefs(QMK_USERSPACE / 'qmk.json') | ||
| 77 | userspace.add_target(keyboard=kb_name, keymap=user_name, do_print=False) | ||
| 78 | 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 @@ | |||
| 1 | from . import doctor | ||
| 2 | from . import add | ||
| 3 | from . import remove | ||
| 4 | from . import list | ||
| 5 | 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 @@ | |||
| 1 | # Copyright 2023 Nick Brassel (@tzarc) | ||
| 2 | # SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | from pathlib import Path | ||
| 4 | from milc import cli | ||
| 5 | |||
| 6 | from qmk.constants import QMK_USERSPACE, HAS_QMK_USERSPACE | ||
| 7 | from qmk.keyboard import keyboard_completer, keyboard_folder_or_all | ||
| 8 | from qmk.keymap import keymap_completer, is_keymap_target | ||
| 9 | from qmk.userspace import UserspaceDefs | ||
| 10 | |||
| 11 | |||
| 12 | @cli.argument('builds', nargs='*', arg_only=True, help="List of builds in form <keyboard>:<keymap>, or path to a keymap JSON file.") | ||
| 13 | @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.') | ||
| 14 | @cli.argument('-km', '--keymap', completer=keymap_completer, help='The keymap to build a firmware for. Ignored when a configurator export is supplied.') | ||
| 15 | @cli.subcommand('Adds a build target to userspace `qmk.json`.') | ||
| 16 | def userspace_add(cli): | ||
| 17 | if not HAS_QMK_USERSPACE: | ||
| 18 | cli.log.error('Could not determine QMK userspace location. Please run `qmk doctor` or `qmk userspace-doctor` to diagnose.') | ||
| 19 | return False | ||
| 20 | |||
| 21 | userspace = UserspaceDefs(QMK_USERSPACE / 'qmk.json') | ||
| 22 | |||
| 23 | if len(cli.args.builds) > 0: | ||
| 24 | json_like_targets = list([Path(p) for p in filter(lambda e: Path(e).exists() and Path(e).suffix == '.json', cli.args.builds)]) | ||
| 25 | make_like_targets = list(filter(lambda e: Path(e) not in json_like_targets, cli.args.builds)) | ||
| 26 | |||
| 27 | for e in json_like_targets: | ||
| 28 | userspace.add_target(json_path=e) | ||
| 29 | |||
| 30 | for e in make_like_targets: | ||
| 31 | s = e.split(':') | ||
| 32 | userspace.add_target(keyboard=s[0], keymap=s[1]) | ||
| 33 | |||
| 34 | else: | ||
| 35 | failed = False | ||
| 36 | try: | ||
| 37 | if not is_keymap_target(cli.args.keyboard, cli.args.keymap): | ||
| 38 | failed = True | ||
| 39 | except KeyError: | ||
| 40 | failed = True | ||
| 41 | |||
| 42 | if failed: | ||
| 43 | from qmk.cli.new.keymap import new_keymap | ||
| 44 | cli.config.new_keymap.keyboard = cli.args.keyboard | ||
| 45 | cli.config.new_keymap.keymap = cli.args.keymap | ||
| 46 | if new_keymap(cli) is not False: | ||
| 47 | userspace.add_target(keyboard=cli.args.keyboard, keymap=cli.args.keymap) | ||
| 48 | else: | ||
| 49 | userspace.add_target(keyboard=cli.args.keyboard, keymap=cli.args.keymap) | ||
| 50 | |||
| 51 | 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 @@ | |||
| 1 | # Copyright 2023 Nick Brassel (@tzarc) | ||
| 2 | # SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | from pathlib import Path | ||
| 4 | from milc import cli | ||
| 5 | |||
| 6 | from qmk.constants import QMK_USERSPACE, HAS_QMK_USERSPACE | ||
| 7 | from qmk.commands import build_environment | ||
| 8 | from qmk.userspace import UserspaceDefs | ||
| 9 | from qmk.build_targets import JsonKeymapBuildTarget | ||
| 10 | from qmk.search import search_keymap_targets | ||
| 11 | from qmk.cli.mass_compile import mass_compile_targets | ||
| 12 | |||
| 13 | |||
| 14 | @cli.argument('-t', '--no-temp', arg_only=True, action='store_true', help="Remove temporary files during build.") | ||
| 15 | @cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.") | ||
| 16 | @cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.") | ||
| 17 | @cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the commands to be run.") | ||
| 18 | @cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.") | ||
| 19 | @cli.subcommand('Compiles the build targets specified in userspace `qmk.json`.') | ||
| 20 | def userspace_compile(cli): | ||
| 21 | if not HAS_QMK_USERSPACE: | ||
| 22 | cli.log.error('Could not determine QMK userspace location. Please run `qmk doctor` or `qmk userspace-doctor` to diagnose.') | ||
| 23 | return False | ||
| 24 | |||
| 25 | userspace = UserspaceDefs(QMK_USERSPACE / 'qmk.json') | ||
| 26 | |||
| 27 | build_targets = [] | ||
| 28 | keyboard_keymap_targets = [] | ||
| 29 | for e in userspace.build_targets: | ||
| 30 | if isinstance(e, Path): | ||
| 31 | build_targets.append(JsonKeymapBuildTarget(e)) | ||
| 32 | elif isinstance(e, dict): | ||
| 33 | keyboard_keymap_targets.append((e['keyboard'], e['keymap'])) | ||
| 34 | |||
| 35 | if len(keyboard_keymap_targets) > 0: | ||
| 36 | build_targets.extend(search_keymap_targets(keyboard_keymap_targets)) | ||
| 37 | |||
| 38 | 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 @@ | |||
| 1 | # Copyright 2023 Nick Brassel (@tzarc) | ||
| 2 | # SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | from milc import cli | ||
| 4 | |||
| 5 | from qmk.constants import QMK_FIRMWARE | ||
| 6 | from qmk.cli.doctor.main import userspace_tests | ||
| 7 | |||
| 8 | |||
| 9 | @cli.subcommand('Checks userspace configuration.') | ||
| 10 | def userspace_doctor(cli): | ||
| 11 | 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 @@ | |||
| 1 | # Copyright 2023 Nick Brassel (@tzarc) | ||
| 2 | # SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | from pathlib import Path | ||
| 4 | from dotty_dict import Dotty | ||
| 5 | from milc import cli | ||
| 6 | |||
| 7 | from qmk.constants import QMK_USERSPACE, HAS_QMK_USERSPACE | ||
| 8 | from qmk.userspace import UserspaceDefs | ||
| 9 | from qmk.build_targets import BuildTarget | ||
| 10 | from qmk.keyboard import is_all_keyboards, keyboard_folder | ||
| 11 | from qmk.keymap import is_keymap_target | ||
| 12 | from qmk.search import search_keymap_targets | ||
| 13 | |||
| 14 | |||
| 15 | @cli.argument('-e', '--expand', arg_only=True, action='store_true', help="Expands any use of `all` for either keyboard or keymap.") | ||
| 16 | @cli.subcommand('Lists the build targets specified in userspace `qmk.json`.') | ||
| 17 | def userspace_list(cli): | ||
| 18 | if not HAS_QMK_USERSPACE: | ||
| 19 | cli.log.error('Could not determine QMK userspace location. Please run `qmk doctor` or `qmk userspace-doctor` to diagnose.') | ||
| 20 | return False | ||
| 21 | |||
| 22 | userspace = UserspaceDefs(QMK_USERSPACE / 'qmk.json') | ||
| 23 | |||
| 24 | if cli.args.expand: | ||
| 25 | build_targets = [] | ||
| 26 | for e in userspace.build_targets: | ||
| 27 | if isinstance(e, Path): | ||
| 28 | build_targets.append(e) | ||
| 29 | elif isinstance(e, dict) or isinstance(e, Dotty): | ||
| 30 | build_targets.extend(search_keymap_targets([(e['keyboard'], e['keymap'])])) | ||
| 31 | else: | ||
| 32 | build_targets = userspace.build_targets | ||
| 33 | |||
| 34 | for e in build_targets: | ||
| 35 | if isinstance(e, Path): | ||
| 36 | # JSON keymap from userspace | ||
| 37 | cli.log.info(f'JSON keymap: {{fg_cyan}}{e}{{fg_reset}}') | ||
| 38 | continue | ||
| 39 | elif isinstance(e, dict) or isinstance(e, Dotty): | ||
| 40 | # keyboard/keymap dict from userspace | ||
| 41 | keyboard = e['keyboard'] | ||
| 42 | keymap = e['keymap'] | ||
| 43 | elif isinstance(e, BuildTarget): | ||
| 44 | # BuildTarget from search_keymap_targets() | ||
| 45 | keyboard = e.keyboard | ||
| 46 | keymap = e.keymap | ||
| 47 | |||
| 48 | if is_all_keyboards(keyboard) or is_keymap_target(keyboard_folder(keyboard), keymap): | ||
| 49 | cli.log.info(f'Keyboard: {{fg_cyan}}{keyboard}{{fg_reset}}, keymap: {{fg_cyan}}{keymap}{{fg_reset}}') | ||
| 50 | else: | ||
| 51 | 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 @@ | |||
| 1 | # Copyright 2023 Nick Brassel (@tzarc) | ||
| 2 | # SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | from pathlib import Path | ||
| 4 | from milc import cli | ||
| 5 | |||
| 6 | from qmk.constants import QMK_USERSPACE, HAS_QMK_USERSPACE | ||
| 7 | from qmk.keyboard import keyboard_completer, keyboard_folder_or_all | ||
| 8 | from qmk.keymap import keymap_completer | ||
| 9 | from qmk.userspace import UserspaceDefs | ||
| 10 | |||
| 11 | |||
| 12 | @cli.argument('builds', nargs='*', arg_only=True, help="List of builds in form <keyboard>:<keymap>, or path to a keymap JSON file.") | ||
| 13 | @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.') | ||
| 14 | @cli.argument('-km', '--keymap', completer=keymap_completer, help='The keymap to build a firmware for. Ignored when a configurator export is supplied.') | ||
| 15 | @cli.subcommand('Removes a build target from userspace `qmk.json`.') | ||
| 16 | def userspace_remove(cli): | ||
| 17 | if not HAS_QMK_USERSPACE: | ||
| 18 | cli.log.error('Could not determine QMK userspace location. Please run `qmk doctor` or `qmk userspace-doctor` to diagnose.') | ||
| 19 | return False | ||
| 20 | |||
| 21 | userspace = UserspaceDefs(QMK_USERSPACE / 'qmk.json') | ||
| 22 | |||
| 23 | if len(cli.args.builds) > 0: | ||
| 24 | json_like_targets = list([Path(p) for p in filter(lambda e: Path(e).exists() and Path(e).suffix == '.json', cli.args.builds)]) | ||
| 25 | make_like_targets = list(filter(lambda e: Path(e) not in json_like_targets, cli.args.builds)) | ||
| 26 | |||
| 27 | for e in json_like_targets: | ||
| 28 | userspace.remove_target(json_path=e) | ||
| 29 | |||
| 30 | for e in make_like_targets: | ||
| 31 | s = e.split(':') | ||
| 32 | userspace.remove_target(keyboard=s[0], keymap=s[1]) | ||
| 33 | |||
| 34 | else: | ||
| 35 | userspace.remove_target(keyboard=cli.args.keyboard, keymap=cli.args.keymap) | ||
| 36 | |||
| 37 | 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 @@ | |||
| 3 | import os | 3 | import os |
| 4 | import sys | 4 | import sys |
| 5 | import shutil | 5 | import shutil |
| 6 | from pathlib import Path | ||
| 6 | 7 | ||
| 7 | from milc import cli | 8 | from milc import cli |
| 8 | import jsonschema | 9 | import jsonschema |
| 9 | 10 | ||
| 11 | from qmk.constants import QMK_USERSPACE, HAS_QMK_USERSPACE | ||
| 10 | from qmk.json_schema import json_load, validate | 12 | from qmk.json_schema import json_load, validate |
| 11 | from qmk.keyboard import keyboard_alias_definitions | 13 | from qmk.keyboard import keyboard_alias_definitions |
| 12 | 14 | ||
| @@ -75,6 +77,10 @@ def build_environment(args): | |||
| 75 | envs[key] = value | 77 | envs[key] = value |
| 76 | else: | 78 | else: |
| 77 | cli.log.warning('Invalid environment variable: %s', env) | 79 | cli.log.warning('Invalid environment variable: %s', env) |
| 80 | |||
| 81 | if HAS_QMK_USERSPACE: | ||
| 82 | envs['QMK_USERSPACE'] = Path(QMK_USERSPACE).resolve() | ||
| 83 | |||
| 78 | return envs | 84 | return envs |
| 79 | 85 | ||
| 80 | 86 | ||
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 | |||
| 4 | from datetime import date | 4 | from datetime import date |
| 5 | from pathlib import Path | 5 | from pathlib import Path |
| 6 | 6 | ||
| 7 | from qmk.userspace import detect_qmk_userspace | ||
| 8 | |||
| 7 | # The root of the qmk_firmware tree. | 9 | # The root of the qmk_firmware tree. |
| 8 | QMK_FIRMWARE = Path.cwd() | 10 | QMK_FIRMWARE = Path.cwd() |
| 9 | 11 | ||
| 12 | # The detected userspace tree | ||
| 13 | QMK_USERSPACE = detect_qmk_userspace() | ||
| 14 | |||
| 15 | # Whether or not we have a separate userspace directory | ||
| 16 | HAS_QMK_USERSPACE = True if QMK_USERSPACE is not None else False | ||
| 17 | |||
| 10 | # Upstream repo url | 18 | # Upstream repo url |
| 11 | QMK_FIRMWARE_UPSTREAM = 'qmk/qmk_firmware' | 19 | QMK_FIRMWARE_UPSTREAM = 'qmk/qmk_firmware' |
| 12 | 20 | ||
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): | |||
| 217 | return '50' + str(key) | 217 | return '50' + str(key) |
| 218 | 218 | ||
| 219 | return key | 219 | return key |
| 220 | |||
| 221 | |||
| 222 | class UserspaceJSONEncoder(QMKJSONEncoder): | ||
| 223 | """Custom encoder to make userspace qmk.json's a little nicer to work with. | ||
| 224 | """ | ||
| 225 | def sort_dict(self, item): | ||
| 226 | """Sorts the hashes in a nice way. | ||
| 227 | """ | ||
| 228 | key = item[0] | ||
| 229 | |||
| 230 | if self.indentation_level == 1: | ||
| 231 | if key == 'userspace_version': | ||
| 232 | return '00userspace_version' | ||
| 233 | |||
| 234 | if key == 'build_targets': | ||
| 235 | return '01build_targets' | ||
| 236 | |||
| 237 | 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(): | |||
| 78 | def is_all_keyboards(keyboard): | 78 | def is_all_keyboards(keyboard): |
| 79 | """Returns True if the keyboard is an AllKeyboards object. | 79 | """Returns True if the keyboard is an AllKeyboards object. |
| 80 | """ | 80 | """ |
| 81 | if isinstance(keyboard, str): | ||
| 82 | return (keyboard == 'all') | ||
| 81 | return isinstance(keyboard, AllKeyboards) | 83 | return isinstance(keyboard, AllKeyboards) |
| 82 | 84 | ||
| 83 | 85 | ||
| 84 | def find_keyboard_from_dir(): | 86 | def find_keyboard_from_dir(): |
| 85 | """Returns a keyboard name based on the user's current directory. | 87 | """Returns a keyboard name based on the user's current directory. |
| 86 | """ | 88 | """ |
| 87 | relative_cwd = qmk.path.under_qmk_firmware() | 89 | relative_cwd = qmk.path.under_qmk_userspace() |
| 90 | if not relative_cwd: | ||
| 91 | relative_cwd = qmk.path.under_qmk_firmware() | ||
| 88 | 92 | ||
| 89 | if relative_cwd and len(relative_cwd.parts) > 1 and relative_cwd.parts[0] == 'keyboards': | 93 | if relative_cwd and len(relative_cwd.parts) > 1 and relative_cwd.parts[0] == 'keyboards': |
| 90 | # Attempt to extract the keyboard name from the current directory | 94 | # Attempt to extract the keyboard name from the current directory |
| @@ -133,6 +137,22 @@ def keyboard_folder(keyboard): | |||
| 133 | return keyboard | 137 | return keyboard |
| 134 | 138 | ||
| 135 | 139 | ||
| 140 | def keyboard_aliases(keyboard): | ||
| 141 | """Returns the list of aliases for the supplied keyboard. | ||
| 142 | |||
| 143 | Includes the keyboard itself. | ||
| 144 | """ | ||
| 145 | aliases = json_load(Path('data/mappings/keyboard_aliases.hjson')) | ||
| 146 | |||
| 147 | if keyboard in aliases: | ||
| 148 | keyboard = aliases[keyboard].get('target', keyboard) | ||
| 149 | |||
| 150 | keyboards = set(filter(lambda k: aliases[k].get('target', '') == keyboard, aliases.keys())) | ||
| 151 | keyboards.add(keyboard) | ||
| 152 | keyboards = list(sorted(keyboards)) | ||
| 153 | return keyboards | ||
| 154 | |||
| 155 | |||
| 136 | def keyboard_folder_or_all(keyboard): | 156 | def keyboard_folder_or_all(keyboard): |
| 137 | """Returns the actual keyboard folder. | 157 | """Returns the actual keyboard folder. |
| 138 | 158 | ||
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 | |||
| 12 | from pygments import lex | 12 | from pygments import lex |
| 13 | 13 | ||
| 14 | import qmk.path | 14 | import qmk.path |
| 15 | from qmk.keyboard import find_keyboard_from_dir, keyboard_folder | 15 | from qmk.constants import QMK_FIRMWARE, QMK_USERSPACE, HAS_QMK_USERSPACE |
| 16 | from qmk.keyboard import find_keyboard_from_dir, keyboard_folder, keyboard_aliases | ||
| 16 | from qmk.errors import CppError | 17 | from qmk.errors import CppError |
| 17 | from qmk.info import info_json | 18 | from qmk.info import info_json |
| 18 | 19 | ||
| @@ -194,29 +195,38 @@ def _strip_any(keycode): | |||
| 194 | def find_keymap_from_dir(*args): | 195 | def find_keymap_from_dir(*args): |
| 195 | """Returns `(keymap_name, source)` for the directory provided (or cwd if not specified). | 196 | """Returns `(keymap_name, source)` for the directory provided (or cwd if not specified). |
| 196 | """ | 197 | """ |
| 197 | relative_path = qmk.path.under_qmk_firmware(*args) | 198 | def _impl_find_keymap_from_dir(relative_path): |
| 199 | if relative_path and len(relative_path.parts) > 1: | ||
| 200 | # If we're in `qmk_firmware/keyboards` and `keymaps` is in our path, try to find the keyboard name. | ||
| 201 | if relative_path.parts[0] == 'keyboards' and 'keymaps' in relative_path.parts: | ||
| 202 | current_path = Path('/'.join(relative_path.parts[1:])) # Strip 'keyboards' from the front | ||
| 198 | 203 | ||
| 199 | if relative_path and len(relative_path.parts) > 1: | 204 | if 'keymaps' in current_path.parts and current_path.name != 'keymaps': |
| 200 | # If we're in `qmk_firmware/keyboards` and `keymaps` is in our path, try to find the keyboard name. | 205 | while current_path.parent.name != 'keymaps': |
| 201 | if relative_path.parts[0] == 'keyboards' and 'keymaps' in relative_path.parts: | 206 | current_path = current_path.parent |
| 202 | current_path = Path('/'.join(relative_path.parts[1:])) # Strip 'keyboards' from the front | ||
| 203 | 207 | ||
| 204 | if 'keymaps' in current_path.parts and current_path.name != 'keymaps': | 208 | return current_path.name, 'keymap_directory' |
| 205 | while current_path.parent.name != 'keymaps': | ||
| 206 | current_path = current_path.parent | ||
| 207 | 209 | ||
| 208 | return current_path.name, 'keymap_directory' | 210 | # If we're in `qmk_firmware/layouts` guess the name from the community keymap they're in |
| 211 | elif relative_path.parts[0] == 'layouts' and is_keymap_dir(relative_path): | ||
| 212 | return relative_path.name, 'layouts_directory' | ||
| 209 | 213 | ||
| 210 | # If we're in `qmk_firmware/layouts` guess the name from the community keymap they're in | 214 | # If we're in `qmk_firmware/users` guess the name from the userspace they're in |
| 211 | elif relative_path.parts[0] == 'layouts' and is_keymap_dir(relative_path): | 215 | elif relative_path.parts[0] == 'users': |
| 212 | return relative_path.name, 'layouts_directory' | 216 | # Guess the keymap name based on which userspace they're in |
| 217 | return relative_path.parts[1], 'users_directory' | ||
| 218 | return None, None | ||
| 213 | 219 | ||
| 214 | # If we're in `qmk_firmware/users` guess the name from the userspace they're in | 220 | if HAS_QMK_USERSPACE: |
| 215 | elif relative_path.parts[0] == 'users': | 221 | name, source = _impl_find_keymap_from_dir(qmk.path.under_qmk_userspace(*args)) |
| 216 | # Guess the keymap name based on which userspace they're in | 222 | if name and source: |
| 217 | return relative_path.parts[1], 'users_directory' | 223 | return name, source |
| 218 | 224 | ||
| 219 | return None, None | 225 | name, source = _impl_find_keymap_from_dir(qmk.path.under_qmk_firmware(*args)) |
| 226 | if name and source: | ||
| 227 | return name, source | ||
| 228 | |||
| 229 | return (None, None) | ||
| 220 | 230 | ||
| 221 | 231 | ||
| 222 | def keymap_completer(prefix, action, parser, parsed_args): | 232 | def keymap_completer(prefix, action, parser, parsed_args): |
| @@ -417,29 +427,45 @@ def locate_keymap(keyboard, keymap): | |||
| 417 | raise KeyError('Invalid keyboard: ' + repr(keyboard)) | 427 | raise KeyError('Invalid keyboard: ' + repr(keyboard)) |
| 418 | 428 | ||
| 419 | # Check the keyboard folder first, last match wins | 429 | # Check the keyboard folder first, last match wins |
| 420 | checked_dirs = '' | ||
| 421 | keymap_path = '' | 430 | keymap_path = '' |
| 422 | 431 | ||
| 423 | for dir in keyboard_folder(keyboard).split('/'): | 432 | search_dirs = [QMK_FIRMWARE] |
| 424 | if checked_dirs: | 433 | keyboard_dirs = [keyboard_folder(keyboard)] |
| 425 | checked_dirs = '/'.join((checked_dirs, dir)) | 434 | if HAS_QMK_USERSPACE: |
| 426 | else: | 435 | # When we've got userspace, check there _last_ as we want them to override anything in the main repo. |
| 427 | checked_dirs = dir | 436 | search_dirs.append(QMK_USERSPACE) |
| 437 | # We also want to search for any aliases as QMK's folder structure may have changed, with an alias, but the user | ||
| 438 | # hasn't updated their keymap location yet. | ||
| 439 | keyboard_dirs.extend(keyboard_aliases(keyboard)) | ||
| 440 | keyboard_dirs = list(set(keyboard_dirs)) | ||
| 441 | |||
| 442 | for search_dir in search_dirs: | ||
| 443 | for keyboard_dir in keyboard_dirs: | ||
| 444 | checked_dirs = '' | ||
| 445 | for dir in keyboard_dir.split('/'): | ||
| 446 | if checked_dirs: | ||
| 447 | checked_dirs = '/'.join((checked_dirs, dir)) | ||
| 448 | else: | ||
| 449 | checked_dirs = dir | ||
| 428 | 450 | ||
| 429 | keymap_dir = Path('keyboards') / checked_dirs / 'keymaps' | 451 | keymap_dir = Path(search_dir) / Path('keyboards') / checked_dirs / 'keymaps' |
| 430 | 452 | ||
| 431 | if (keymap_dir / keymap / 'keymap.c').exists(): | 453 | if (keymap_dir / keymap / 'keymap.c').exists(): |
| 432 | keymap_path = keymap_dir / keymap / 'keymap.c' | 454 | keymap_path = keymap_dir / keymap / 'keymap.c' |
| 433 | if (keymap_dir / keymap / 'keymap.json').exists(): | 455 | if (keymap_dir / keymap / 'keymap.json').exists(): |
| 434 | keymap_path = keymap_dir / keymap / 'keymap.json' | 456 | keymap_path = keymap_dir / keymap / 'keymap.json' |
| 435 | 457 | ||
| 436 | if keymap_path: | 458 | if keymap_path: |
| 437 | return keymap_path | 459 | return keymap_path |
| 438 | 460 | ||
| 439 | # Check community layouts as a fallback | 461 | # Check community layouts as a fallback |
| 440 | info = info_json(keyboard) | 462 | info = info_json(keyboard) |
| 441 | 463 | ||
| 442 | for community_parent in Path('layouts').glob('*/'): | 464 | community_parents = list(Path('layouts').glob('*/')) |
| 465 | if HAS_QMK_USERSPACE and (Path(QMK_USERSPACE) / "layouts").exists(): | ||
| 466 | community_parents.append(Path(QMK_USERSPACE) / "layouts") | ||
| 467 | |||
| 468 | for community_parent in community_parents: | ||
| 443 | for layout in info.get("community_layouts", []): | 469 | for layout in info.get("community_layouts", []): |
| 444 | community_layout = community_parent / layout / keymap | 470 | community_layout = community_parent / layout / keymap |
| 445 | if community_layout.exists(): | 471 | if community_layout.exists(): |
| @@ -449,6 +475,16 @@ def locate_keymap(keyboard, keymap): | |||
| 449 | return community_layout / 'keymap.c' | 475 | return community_layout / 'keymap.c' |
| 450 | 476 | ||
| 451 | 477 | ||
| 478 | def is_keymap_target(keyboard, keymap): | ||
| 479 | if keymap == 'all': | ||
| 480 | return True | ||
| 481 | |||
| 482 | if locate_keymap(keyboard, keymap): | ||
| 483 | return True | ||
| 484 | |||
| 485 | return False | ||
| 486 | |||
| 487 | |||
| 452 | def list_keymaps(keyboard, c=True, json=True, additional_files=None, fullpath=False): | 488 | def list_keymaps(keyboard, c=True, json=True, additional_files=None, fullpath=False): |
| 453 | """List the available keymaps for a keyboard. | 489 | """List the available keymaps for a keyboard. |
| 454 | 490 | ||
| @@ -473,26 +509,30 @@ def list_keymaps(keyboard, c=True, json=True, additional_files=None, fullpath=Fa | |||
| 473 | """ | 509 | """ |
| 474 | names = set() | 510 | names = set() |
| 475 | 511 | ||
| 476 | keyboards_dir = Path('keyboards') | ||
| 477 | kb_path = keyboards_dir / keyboard | ||
| 478 | |||
| 479 | # walk up the directory tree until keyboards_dir | 512 | # walk up the directory tree until keyboards_dir |
| 480 | # and collect all directories' name with keymap.c file in it | 513 | # and collect all directories' name with keymap.c file in it |
| 481 | while kb_path != keyboards_dir: | 514 | for search_dir in [QMK_FIRMWARE, QMK_USERSPACE] if HAS_QMK_USERSPACE else [QMK_FIRMWARE]: |
| 482 | keymaps_dir = kb_path / "keymaps" | 515 | keyboards_dir = search_dir / Path('keyboards') |
| 483 | 516 | kb_path = keyboards_dir / keyboard | |
| 484 | if keymaps_dir.is_dir(): | 517 | |
| 485 | for keymap in keymaps_dir.iterdir(): | 518 | while kb_path != keyboards_dir: |
| 486 | if is_keymap_dir(keymap, c, json, additional_files): | 519 | keymaps_dir = kb_path / "keymaps" |
| 487 | keymap = keymap if fullpath else keymap.name | 520 | if keymaps_dir.is_dir(): |
| 488 | names.add(keymap) | 521 | for keymap in keymaps_dir.iterdir(): |
| 522 | if is_keymap_dir(keymap, c, json, additional_files): | ||
| 523 | keymap = keymap if fullpath else keymap.name | ||
| 524 | names.add(keymap) | ||
| 489 | 525 | ||
| 490 | kb_path = kb_path.parent | 526 | kb_path = kb_path.parent |
| 491 | 527 | ||
| 492 | # Check community layouts as a fallback | 528 | # Check community layouts as a fallback |
| 493 | info = info_json(keyboard) | 529 | info = info_json(keyboard) |
| 494 | 530 | ||
| 495 | for community_parent in Path('layouts').glob('*/'): | 531 | community_parents = list(Path('layouts').glob('*/')) |
| 532 | if HAS_QMK_USERSPACE and (Path(QMK_USERSPACE) / "layouts").exists(): | ||
| 533 | community_parents.append(Path(QMK_USERSPACE) / "layouts") | ||
| 534 | |||
| 535 | for community_parent in community_parents: | ||
| 496 | for layout in info.get("community_layouts", []): | 536 | for layout in info.get("community_layouts", []): |
| 497 | cl_path = community_parent / layout | 537 | cl_path = community_parent / layout |
| 498 | if cl_path.is_dir(): | 538 | 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 | |||
| 5 | import argparse | 5 | import argparse |
| 6 | from pathlib import Path | 6 | from pathlib import Path |
| 7 | 7 | ||
| 8 | from qmk.constants import MAX_KEYBOARD_SUBFOLDERS, QMK_FIRMWARE | 8 | from qmk.constants import MAX_KEYBOARD_SUBFOLDERS, QMK_FIRMWARE, QMK_USERSPACE, HAS_QMK_USERSPACE |
| 9 | from qmk.errors import NoSuchKeyboardError | 9 | from qmk.errors import NoSuchKeyboardError |
| 10 | 10 | ||
| 11 | 11 | ||
| @@ -28,6 +28,40 @@ def under_qmk_firmware(path=Path(os.environ['ORIG_CWD'])): | |||
| 28 | return None | 28 | return None |
| 29 | 29 | ||
| 30 | 30 | ||
| 31 | def under_qmk_userspace(path=Path(os.environ['ORIG_CWD'])): | ||
| 32 | """Returns a Path object representing the relative path under $QMK_USERSPACE, or None. | ||
| 33 | """ | ||
| 34 | try: | ||
| 35 | if HAS_QMK_USERSPACE: | ||
| 36 | return path.relative_to(QMK_USERSPACE) | ||
| 37 | except ValueError: | ||
| 38 | pass | ||
| 39 | return None | ||
| 40 | |||
| 41 | |||
| 42 | def is_under_qmk_firmware(path=Path(os.environ['ORIG_CWD'])): | ||
| 43 | """Returns a boolean if the input path is a child under qmk_firmware. | ||
| 44 | """ | ||
| 45 | if path is None: | ||
| 46 | return False | ||
| 47 | try: | ||
| 48 | return Path(os.path.commonpath([Path(path), QMK_FIRMWARE])) == QMK_FIRMWARE | ||
| 49 | except ValueError: | ||
| 50 | return False | ||
| 51 | |||
| 52 | |||
| 53 | def is_under_qmk_userspace(path=Path(os.environ['ORIG_CWD'])): | ||
| 54 | """Returns a boolean if the input path is a child under $QMK_USERSPACE. | ||
| 55 | """ | ||
| 56 | if path is None: | ||
| 57 | return False | ||
| 58 | try: | ||
| 59 | if HAS_QMK_USERSPACE: | ||
| 60 | return Path(os.path.commonpath([Path(path), QMK_USERSPACE])) == QMK_USERSPACE | ||
| 61 | except ValueError: | ||
| 62 | return False | ||
| 63 | |||
| 64 | |||
| 31 | def keyboard(keyboard_name): | 65 | def keyboard(keyboard_name): |
| 32 | """Returns the path to a keyboard's directory relative to the qmk root. | 66 | """Returns the path to a keyboard's directory relative to the qmk root. |
| 33 | """ | 67 | """ |
| @@ -45,11 +79,28 @@ def keymaps(keyboard_name): | |||
| 45 | keyboard_folder = keyboard(keyboard_name) | 79 | keyboard_folder = keyboard(keyboard_name) |
| 46 | found_dirs = [] | 80 | found_dirs = [] |
| 47 | 81 | ||
| 82 | if HAS_QMK_USERSPACE: | ||
| 83 | this_keyboard_folder = Path(QMK_USERSPACE) / keyboard_folder | ||
| 84 | for _ in range(MAX_KEYBOARD_SUBFOLDERS): | ||
| 85 | if (this_keyboard_folder / 'keymaps').exists(): | ||
| 86 | found_dirs.append((this_keyboard_folder / 'keymaps').resolve()) | ||
| 87 | |||
| 88 | this_keyboard_folder = this_keyboard_folder.parent | ||
| 89 | if this_keyboard_folder.resolve() == QMK_USERSPACE.resolve(): | ||
| 90 | break | ||
| 91 | |||
| 92 | # We don't have any relevant keymap directories in userspace, so we'll use the fully-qualified path instead. | ||
| 93 | if len(found_dirs) == 0: | ||
| 94 | found_dirs.append((QMK_USERSPACE / keyboard_folder / 'keymaps').resolve()) | ||
| 95 | |||
| 96 | this_keyboard_folder = QMK_FIRMWARE / keyboard_folder | ||
| 48 | for _ in range(MAX_KEYBOARD_SUBFOLDERS): | 97 | for _ in range(MAX_KEYBOARD_SUBFOLDERS): |
| 49 | if (keyboard_folder / 'keymaps').exists(): | 98 | if (this_keyboard_folder / 'keymaps').exists(): |
| 50 | found_dirs.append((keyboard_folder / 'keymaps').resolve()) | 99 | found_dirs.append((this_keyboard_folder / 'keymaps').resolve()) |
| 51 | 100 | ||
| 52 | keyboard_folder = keyboard_folder.parent | 101 | this_keyboard_folder = this_keyboard_folder.parent |
| 102 | if this_keyboard_folder.resolve() == QMK_FIRMWARE.resolve(): | ||
| 103 | break | ||
| 53 | 104 | ||
| 54 | if len(found_dirs) > 0: | 105 | if len(found_dirs) > 0: |
| 55 | return found_dirs | 106 | 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 @@ | |||
| 1 | # Copyright 2023 Nick Brassel (@tzarc) | ||
| 2 | # SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | from os import environ | ||
| 4 | from pathlib import Path | ||
| 5 | import json | ||
| 6 | import jsonschema | ||
| 7 | |||
| 8 | from milc import cli | ||
| 9 | |||
| 10 | from qmk.json_schema import validate, json_load | ||
| 11 | from qmk.json_encoders import UserspaceJSONEncoder | ||
| 12 | |||
| 13 | |||
| 14 | def qmk_userspace_paths(): | ||
| 15 | test_dirs = [] | ||
| 16 | |||
| 17 | # If we're already in a directory with a qmk.json and a keyboards or layouts directory, interpret it as userspace | ||
| 18 | current_dir = Path(environ['ORIG_CWD']) | ||
| 19 | while len(current_dir.parts) > 1: | ||
| 20 | if (current_dir / 'qmk.json').is_file(): | ||
| 21 | test_dirs.append(current_dir) | ||
| 22 | current_dir = current_dir.parent | ||
| 23 | |||
| 24 | # If we have a QMK_USERSPACE environment variable, use that | ||
| 25 | if environ.get('QMK_USERSPACE') is not None: | ||
| 26 | current_dir = Path(environ.get('QMK_USERSPACE')) | ||
| 27 | if current_dir.is_dir(): | ||
| 28 | test_dirs.append(current_dir) | ||
| 29 | |||
| 30 | # If someone has configured a directory, use that | ||
| 31 | if cli.config.user.overlay_dir is not None: | ||
| 32 | current_dir = Path(cli.config.user.overlay_dir) | ||
| 33 | if current_dir.is_dir(): | ||
| 34 | test_dirs.append(current_dir) | ||
| 35 | |||
| 36 | return test_dirs | ||
| 37 | |||
| 38 | |||
| 39 | def qmk_userspace_validate(path): | ||
| 40 | # Construct a UserspaceDefs object to ensure it validates correctly | ||
| 41 | if (path / 'qmk.json').is_file(): | ||
| 42 | UserspaceDefs(path / 'qmk.json') | ||
| 43 | return | ||
| 44 | |||
| 45 | # No qmk.json file found | ||
| 46 | raise FileNotFoundError('No qmk.json file found.') | ||
| 47 | |||
| 48 | |||
| 49 | def detect_qmk_userspace(): | ||
| 50 | # Iterate through all the detected userspace paths and return the first one that validates correctly | ||
| 51 | test_dirs = qmk_userspace_paths() | ||
| 52 | for test_dir in test_dirs: | ||
| 53 | try: | ||
| 54 | qmk_userspace_validate(test_dir) | ||
| 55 | return test_dir | ||
| 56 | except FileNotFoundError: | ||
| 57 | continue | ||
| 58 | except UserspaceValidationError: | ||
| 59 | continue | ||
| 60 | return None | ||
| 61 | |||
| 62 | |||
| 63 | class UserspaceDefs: | ||
| 64 | def __init__(self, userspace_json: Path): | ||
| 65 | self.path = userspace_json | ||
| 66 | self.build_targets = [] | ||
| 67 | json = json_load(userspace_json) | ||
| 68 | |||
| 69 | exception = UserspaceValidationError() | ||
| 70 | success = False | ||
| 71 | |||
| 72 | try: | ||
| 73 | validate(json, 'qmk.user_repo.v0') # `qmk.json` must have a userspace_version at minimum | ||
| 74 | except jsonschema.ValidationError as err: | ||
| 75 | exception.add('qmk.user_repo.v0', err) | ||
| 76 | raise exception | ||
| 77 | |||
| 78 | # Iterate through each version of the schema, starting with the latest and decreasing to v1 | ||
| 79 | try: | ||
| 80 | validate(json, 'qmk.user_repo.v1') | ||
| 81 | self.__load_v1(json) | ||
| 82 | success = True | ||
| 83 | except jsonschema.ValidationError as err: | ||
| 84 | exception.add('qmk.user_repo.v1', err) | ||
| 85 | |||
| 86 | if not success: | ||
| 87 | raise exception | ||
| 88 | |||
| 89 | def save(self): | ||
| 90 | target_json = { | ||
| 91 | "userspace_version": "1.0", # Needs to match latest version | ||
| 92 | "build_targets": [] | ||
| 93 | } | ||
| 94 | |||
| 95 | for e in self.build_targets: | ||
| 96 | if isinstance(e, dict): | ||
| 97 | target_json['build_targets'].append([e['keyboard'], e['keymap']]) | ||
| 98 | elif isinstance(e, Path): | ||
| 99 | target_json['build_targets'].append(str(e.relative_to(self.path.parent))) | ||
| 100 | |||
| 101 | try: | ||
| 102 | # Ensure what we're writing validates against the latest version of the schema | ||
| 103 | validate(target_json, 'qmk.user_repo.v1') | ||
| 104 | except jsonschema.ValidationError as err: | ||
| 105 | cli.log.error(f'Could not save userspace file: {err}') | ||
| 106 | return False | ||
| 107 | |||
| 108 | # Only actually write out data if it changed | ||
| 109 | old_data = json.dumps(json.loads(self.path.read_text()), cls=UserspaceJSONEncoder, sort_keys=True) | ||
| 110 | new_data = json.dumps(target_json, cls=UserspaceJSONEncoder, sort_keys=True) | ||
| 111 | if old_data != new_data: | ||
| 112 | self.path.write_text(new_data) | ||
| 113 | cli.log.info(f'Saved userspace file to {self.path}.') | ||
| 114 | return True | ||
| 115 | |||
| 116 | def add_target(self, keyboard=None, keymap=None, json_path=None, do_print=True): | ||
| 117 | if json_path is not None: | ||
| 118 | # Assume we're adding a json filename/path | ||
| 119 | json_path = Path(json_path) | ||
| 120 | if json_path not in self.build_targets: | ||
| 121 | self.build_targets.append(json_path) | ||
| 122 | if do_print: | ||
| 123 | cli.log.info(f'Added {json_path} to userspace build targets.') | ||
| 124 | else: | ||
| 125 | cli.log.info(f'{json_path} is already a userspace build target.') | ||
| 126 | |||
| 127 | elif keyboard is not None and keymap is not None: | ||
| 128 | # Both keyboard/keymap specified | ||
| 129 | e = {"keyboard": keyboard, "keymap": keymap} | ||
| 130 | if e not in self.build_targets: | ||
| 131 | self.build_targets.append(e) | ||
| 132 | if do_print: | ||
| 133 | cli.log.info(f'Added {keyboard}:{keymap} to userspace build targets.') | ||
| 134 | else: | ||
| 135 | if do_print: | ||
| 136 | cli.log.info(f'{keyboard}:{keymap} is already a userspace build target.') | ||
| 137 | |||
| 138 | def remove_target(self, keyboard=None, keymap=None, json_path=None, do_print=True): | ||
| 139 | if json_path is not None: | ||
| 140 | # Assume we're removing a json filename/path | ||
| 141 | json_path = Path(json_path) | ||
| 142 | if json_path in self.build_targets: | ||
| 143 | self.build_targets.remove(json_path) | ||
| 144 | if do_print: | ||
| 145 | cli.log.info(f'Removed {json_path} from userspace build targets.') | ||
| 146 | else: | ||
| 147 | cli.log.info(f'{json_path} is not a userspace build target.') | ||
| 148 | |||
| 149 | elif keyboard is not None and keymap is not None: | ||
| 150 | # Both keyboard/keymap specified | ||
| 151 | e = {"keyboard": keyboard, "keymap": keymap} | ||
| 152 | if e in self.build_targets: | ||
| 153 | self.build_targets.remove(e) | ||
| 154 | if do_print: | ||
| 155 | cli.log.info(f'Removed {keyboard}:{keymap} from userspace build targets.') | ||
| 156 | else: | ||
| 157 | if do_print: | ||
| 158 | cli.log.info(f'{keyboard}:{keymap} is not a userspace build target.') | ||
| 159 | |||
| 160 | def __load_v1(self, json): | ||
| 161 | for e in json['build_targets']: | ||
| 162 | if isinstance(e, list) and len(e) == 2: | ||
| 163 | self.add_target(keyboard=e[0], keymap=e[1], do_print=False) | ||
| 164 | if isinstance(e, str): | ||
| 165 | p = self.path.parent / e | ||
| 166 | if p.exists() and p.suffix == '.json': | ||
| 167 | self.add_target(json_path=p, do_print=False) | ||
| 168 | |||
| 169 | |||
| 170 | class UserspaceValidationError(Exception): | ||
| 171 | def __init__(self, *args, **kwargs): | ||
| 172 | super().__init__(*args, **kwargs) | ||
| 173 | self.__exceptions = [] | ||
| 174 | |||
| 175 | def __str__(self): | ||
| 176 | return self.message | ||
| 177 | |||
| 178 | @property | ||
| 179 | def exceptions(self): | ||
| 180 | return self.__exceptions | ||
| 181 | |||
| 182 | def add(self, schema, exception): | ||
| 183 | self.__exceptions.append((schema, exception)) | ||
| 184 | errorlist = "\n\n".join([f"{schema}: {exception}" for schema, exception in self.__exceptions]) | ||
| 185 | self.message = f'Could not validate against any version of the userspace schema. Errors:\n\n{errorlist}' | ||
