diff options
| author | QMK Bot <hello@qmk.fm> | 2024-08-12 12:34:54 +0000 |
|---|---|---|
| committer | QMK Bot <hello@qmk.fm> | 2024-08-12 12:34:54 +0000 |
| commit | 2c6409fdd8deed5b10410b5e9a74748499e0739c (patch) | |
| tree | 240efdcdff73fa87a47c5cd9b87b63a7b9263291 /lib/python | |
| parent | 783f97ff32de1d6febceb09f46dfa624e4fc56ec (diff) | |
| parent | 380e0c9cad72ac29f858bef85c8b8eb35b6931f0 (diff) | |
Merge remote-tracking branch 'origin/master' into develop
Diffstat (limited to 'lib/python')
| -rw-r--r-- | lib/python/qmk/build_targets.py | 107 | ||||
| -rwxr-xr-x | lib/python/qmk/cli/format/json.py | 9 | ||||
| -rwxr-xr-x | lib/python/qmk/cli/mass_compile.py | 24 | ||||
| -rw-r--r-- | lib/python/qmk/cli/userspace/add.py | 10 | ||||
| -rw-r--r-- | lib/python/qmk/cli/userspace/compile.py | 10 | ||||
| -rw-r--r-- | lib/python/qmk/cli/userspace/list.py | 23 | ||||
| -rw-r--r-- | lib/python/qmk/cli/userspace/remove.py | 10 | ||||
| -rw-r--r-- | lib/python/qmk/commands.py | 7 | ||||
| -rw-r--r-- | lib/python/qmk/info.py | 19 | ||||
| -rw-r--r-- | lib/python/qmk/keymap.py | 4 | ||||
| -rw-r--r-- | lib/python/qmk/search.py | 159 | ||||
| -rw-r--r-- | lib/python/qmk/userspace.py | 66 |
12 files changed, 304 insertions, 144 deletions
diff --git a/lib/python/qmk/build_targets.py b/lib/python/qmk/build_targets.py index d974d04020..e2df029490 100644 --- a/lib/python/qmk/build_targets.py +++ b/lib/python/qmk/build_targets.py | |||
| @@ -1,8 +1,8 @@ | |||
| 1 | # Copyright 2023 Nick Brassel (@tzarc) | 1 | # Copyright 2023-2024 Nick Brassel (@tzarc) |
| 2 | # SPDX-License-Identifier: GPL-2.0-or-later | 2 | # SPDX-License-Identifier: GPL-2.0-or-later |
| 3 | import json | 3 | import json |
| 4 | import shutil | 4 | import shutil |
| 5 | from typing import List, Union | 5 | from typing import Dict, List, Union |
| 6 | from pathlib import Path | 6 | from pathlib import Path |
| 7 | from dotty_dict import dotty, Dotty | 7 | from dotty_dict import dotty, Dotty |
| 8 | from milc import cli | 8 | from milc import cli |
| @@ -13,6 +13,9 @@ from qmk.info import keymap_json | |||
| 13 | from qmk.keymap import locate_keymap | 13 | from qmk.keymap import locate_keymap |
| 14 | from qmk.path import is_under_qmk_firmware, is_under_qmk_userspace | 14 | from qmk.path import is_under_qmk_firmware, is_under_qmk_userspace |
| 15 | 15 | ||
| 16 | # These must be kept in the order in which they're applied to $(TARGET) in the makefiles in order to ensure consistency. | ||
| 17 | TARGET_FILENAME_MODIFIERS = ['FORCE_LAYOUT', 'CONVERT_TO'] | ||
| 18 | |||
| 16 | 19 | ||
| 17 | class BuildTarget: | 20 | class BuildTarget: |
| 18 | def __init__(self, keyboard: str, keymap: str, json: Union[dict, Dotty] = None): | 21 | def __init__(self, keyboard: str, keymap: str, json: Union[dict, Dotty] = None): |
| @@ -22,25 +25,25 @@ class BuildTarget: | |||
| 22 | self._parallel = 1 | 25 | self._parallel = 1 |
| 23 | self._clean = False | 26 | self._clean = False |
| 24 | self._compiledb = False | 27 | self._compiledb = False |
| 25 | self._target = f'{self._keyboard_safe}_{self.keymap}' | 28 | self._extra_args = {} |
| 26 | self._intermediate_output = Path(f'{INTERMEDIATE_OUTPUT_PREFIX}{self._target}') | ||
| 27 | self._generated_files_path = self._intermediate_output / 'src' | ||
| 28 | self._json = json.to_dict() if isinstance(json, Dotty) else json | 29 | self._json = json.to_dict() if isinstance(json, Dotty) else json |
| 29 | 30 | ||
| 30 | def __str__(self): | 31 | def __str__(self): |
| 31 | return f'{self.keyboard}:{self.keymap}' | 32 | return f'{self.keyboard}:{self.keymap}' |
| 32 | 33 | ||
| 33 | def __repr__(self): | 34 | def __repr__(self): |
| 35 | if len(self._extra_args.items()) > 0: | ||
| 36 | return f'BuildTarget(keyboard={self.keyboard}, keymap={self.keymap}, extra_args={json.dumps(self._extra_args, sort_keys=True)})' | ||
| 34 | return f'BuildTarget(keyboard={self.keyboard}, keymap={self.keymap})' | 37 | return f'BuildTarget(keyboard={self.keyboard}, keymap={self.keymap})' |
| 35 | 38 | ||
| 39 | def __lt__(self, __value: object) -> bool: | ||
| 40 | return self.__repr__() < __value.__repr__() | ||
| 41 | |||
| 36 | def __eq__(self, __value: object) -> bool: | 42 | def __eq__(self, __value: object) -> bool: |
| 37 | if not isinstance(__value, BuildTarget): | 43 | if not isinstance(__value, BuildTarget): |
| 38 | return False | 44 | return False |
| 39 | return self.__repr__() == __value.__repr__() | 45 | return self.__repr__() == __value.__repr__() |
| 40 | 46 | ||
| 41 | def __ne__(self, __value: object) -> bool: | ||
| 42 | return not self.__eq__(__value) | ||
| 43 | |||
| 44 | def __hash__(self) -> int: | 47 | def __hash__(self) -> int: |
| 45 | return self.__repr__().__hash__() | 48 | return self.__repr__().__hash__() |
| 46 | 49 | ||
| @@ -72,7 +75,34 @@ class BuildTarget: | |||
| 72 | def dotty(self) -> Dotty: | 75 | def dotty(self) -> Dotty: |
| 73 | return dotty(self.json) | 76 | return dotty(self.json) |
| 74 | 77 | ||
| 75 | def _common_make_args(self, dry_run: bool = False, build_target: str = None): | 78 | @property |
| 79 | def extra_args(self) -> Dict[str, str]: | ||
| 80 | return {k: v for k, v in self._extra_args.items()} | ||
| 81 | |||
| 82 | @extra_args.setter | ||
| 83 | def extra_args(self, ex_args: Dict[str, str]): | ||
| 84 | if ex_args is not None and isinstance(ex_args, dict): | ||
| 85 | self._extra_args = {k: v for k, v in ex_args.items()} | ||
| 86 | |||
| 87 | def target_name(self, **env_vars) -> str: | ||
| 88 | # Work out the intended target name | ||
| 89 | target = f'{self._keyboard_safe}_{self.keymap}' | ||
| 90 | vars = self._all_vars(**env_vars) | ||
| 91 | for modifier in TARGET_FILENAME_MODIFIERS: | ||
| 92 | if modifier in vars: | ||
| 93 | target += f"_{vars[modifier]}" | ||
| 94 | return target | ||
| 95 | |||
| 96 | def _all_vars(self, **env_vars) -> Dict[str, str]: | ||
| 97 | vars = {k: v for k, v in env_vars.items()} | ||
| 98 | for k, v in self._extra_args.items(): | ||
| 99 | vars[k] = v | ||
| 100 | return vars | ||
| 101 | |||
| 102 | def _intermediate_output(self, **env_vars) -> Path: | ||
| 103 | return Path(f'{INTERMEDIATE_OUTPUT_PREFIX}{self.target_name(**env_vars)}') | ||
| 104 | |||
| 105 | def _common_make_args(self, dry_run: bool = False, build_target: str = None, **env_vars): | ||
| 76 | compile_args = [ | 106 | compile_args = [ |
| 77 | find_make(), | 107 | find_make(), |
| 78 | *get_make_parallel_args(self._parallel), | 108 | *get_make_parallel_args(self._parallel), |
| @@ -98,14 +128,17 @@ class BuildTarget: | |||
| 98 | f'KEYBOARD={self.keyboard}', | 128 | f'KEYBOARD={self.keyboard}', |
| 99 | f'KEYMAP={self.keymap}', | 129 | f'KEYMAP={self.keymap}', |
| 100 | f'KEYBOARD_FILESAFE={self._keyboard_safe}', | 130 | f'KEYBOARD_FILESAFE={self._keyboard_safe}', |
| 101 | f'TARGET={self._target}', | 131 | f'TARGET={self._keyboard_safe}_{self.keymap}', # don't use self.target_name() here, it's rebuilt on the makefile side |
| 102 | f'INTERMEDIATE_OUTPUT={self._intermediate_output}', | ||
| 103 | f'VERBOSE={verbose}', | 132 | f'VERBOSE={verbose}', |
| 104 | f'COLOR={color}', | 133 | f'COLOR={color}', |
| 105 | 'SILENT=false', | 134 | 'SILENT=false', |
| 106 | 'QMK_BIN="qmk"', | 135 | 'QMK_BIN="qmk"', |
| 107 | ]) | 136 | ]) |
| 108 | 137 | ||
| 138 | vars = self._all_vars(**env_vars) | ||
| 139 | for k, v in vars.items(): | ||
| 140 | compile_args.append(f'{k}={v}') | ||
| 141 | |||
| 109 | return compile_args | 142 | return compile_args |
| 110 | 143 | ||
| 111 | def prepare_build(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None: | 144 | def prepare_build(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None: |
| @@ -150,6 +183,8 @@ class KeyboardKeymapBuildTarget(BuildTarget): | |||
| 150 | super().__init__(keyboard=keyboard, keymap=keymap, json=json) | 183 | super().__init__(keyboard=keyboard, keymap=keymap, json=json) |
| 151 | 184 | ||
| 152 | def __repr__(self): | 185 | def __repr__(self): |
| 186 | if len(self._extra_args.items()) > 0: | ||
| 187 | return f'KeyboardKeymapTarget(keyboard={self.keyboard}, keymap={self.keymap}, extra_args={self._extra_args})' | ||
| 153 | return f'KeyboardKeymapTarget(keyboard={self.keyboard}, keymap={self.keymap})' | 188 | return f'KeyboardKeymapTarget(keyboard={self.keyboard}, keymap={self.keymap})' |
| 154 | 189 | ||
| 155 | def _load_json(self): | 190 | def _load_json(self): |
| @@ -159,15 +194,13 @@ class KeyboardKeymapBuildTarget(BuildTarget): | |||
| 159 | pass | 194 | pass |
| 160 | 195 | ||
| 161 | def compile_command(self, build_target: str = None, dry_run: bool = False, **env_vars) -> List[str]: | 196 | def compile_command(self, build_target: str = None, dry_run: bool = False, **env_vars) -> List[str]: |
| 162 | compile_args = self._common_make_args(dry_run=dry_run, build_target=build_target) | 197 | compile_args = self._common_make_args(dry_run=dry_run, build_target=build_target, **env_vars) |
| 163 | |||
| 164 | for key, value in env_vars.items(): | ||
| 165 | compile_args.append(f'{key}={value}') | ||
| 166 | 198 | ||
| 167 | # Need to override the keymap path if the keymap is a userspace directory. | 199 | # Need to override the keymap path if the keymap is a userspace directory. |
| 168 | # This also ensures keyboard aliases as per `keyboard_aliases.hjson` still work if the userspace has the keymap | 200 | # This also ensures keyboard aliases as per `keyboard_aliases.hjson` still work if the userspace has the keymap |
| 169 | # in an equivalent historical location. | 201 | # in an equivalent historical location. |
| 170 | keymap_location = locate_keymap(self.keyboard, self.keymap) | 202 | vars = self._all_vars(**env_vars) |
| 203 | keymap_location = locate_keymap(self.keyboard, self.keymap, force_layout=vars.get('FORCE_LAYOUT')) | ||
| 171 | if is_under_qmk_userspace(keymap_location) and not is_under_qmk_firmware(keymap_location): | 204 | if is_under_qmk_userspace(keymap_location) and not is_under_qmk_firmware(keymap_location): |
| 172 | keymap_directory = keymap_location.parent | 205 | keymap_directory = keymap_location.parent |
| 173 | compile_args.extend([ | 206 | compile_args.extend([ |
| @@ -196,47 +229,51 @@ class JsonKeymapBuildTarget(BuildTarget): | |||
| 196 | 229 | ||
| 197 | super().__init__(keyboard=json['keyboard'], keymap=json['keymap'], json=json) | 230 | super().__init__(keyboard=json['keyboard'], keymap=json['keymap'], json=json) |
| 198 | 231 | ||
| 199 | self._keymap_json = self._generated_files_path / 'keymap.json' | ||
| 200 | |||
| 201 | def __repr__(self): | 232 | def __repr__(self): |
| 233 | if len(self._extra_args.items()) > 0: | ||
| 234 | return f'JsonKeymapTarget(keyboard={self.keyboard}, keymap={self.keymap}, path={self.json_path}, extra_args={self._extra_args})' | ||
| 202 | return f'JsonKeymapTarget(keyboard={self.keyboard}, keymap={self.keymap}, path={self.json_path})' | 235 | return f'JsonKeymapTarget(keyboard={self.keyboard}, keymap={self.keymap}, path={self.json_path})' |
| 203 | 236 | ||
| 204 | def _load_json(self): | 237 | def _load_json(self): |
| 205 | pass # Already loaded in constructor | 238 | pass # Already loaded in constructor |
| 206 | 239 | ||
| 207 | def prepare_build(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None: | 240 | def prepare_build(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None: |
| 241 | intermediate_output = self._intermediate_output(**env_vars) | ||
| 242 | generated_files_path = intermediate_output / 'src' | ||
| 243 | keymap_json = generated_files_path / 'keymap.json' | ||
| 244 | |||
| 208 | if self._clean: | 245 | if self._clean: |
| 209 | if self._intermediate_output.exists(): | 246 | if intermediate_output.exists(): |
| 210 | shutil.rmtree(self._intermediate_output) | 247 | shutil.rmtree(intermediate_output) |
| 211 | 248 | ||
| 212 | # begin with making the deepest folder in the tree | 249 | # begin with making the deepest folder in the tree |
| 213 | self._generated_files_path.mkdir(exist_ok=True, parents=True) | 250 | generated_files_path.mkdir(exist_ok=True, parents=True) |
| 214 | 251 | ||
| 215 | # Compare minified to ensure consistent comparison | 252 | # Compare minified to ensure consistent comparison |
| 216 | new_content = json.dumps(self.json, separators=(',', ':')) | 253 | new_content = json.dumps(self.json, separators=(',', ':')) |
| 217 | if self._keymap_json.exists(): | 254 | if keymap_json.exists(): |
| 218 | old_content = json.dumps(json.loads(self._keymap_json.read_text(encoding='utf-8')), separators=(',', ':')) | 255 | old_content = json.dumps(json.loads(keymap_json.read_text(encoding='utf-8')), separators=(',', ':')) |
| 219 | if old_content == new_content: | 256 | if old_content == new_content: |
| 220 | new_content = None | 257 | new_content = None |
| 221 | 258 | ||
| 222 | # Write the keymap.json file if different so timestamps are only updated | 259 | # Write the keymap.json file if different so timestamps are only updated |
| 223 | # if the content changes -- running `make` won't treat it as modified. | 260 | # if the content changes -- running `make` won't treat it as modified. |
| 224 | if new_content: | 261 | if new_content: |
| 225 | self._keymap_json.write_text(new_content, encoding='utf-8') | 262 | keymap_json.write_text(new_content, encoding='utf-8') |
| 226 | 263 | ||
| 227 | def compile_command(self, build_target: str = None, dry_run: bool = False, **env_vars) -> List[str]: | 264 | def compile_command(self, build_target: str = None, dry_run: bool = False, **env_vars) -> List[str]: |
| 228 | compile_args = self._common_make_args(dry_run=dry_run, build_target=build_target) | 265 | compile_args = self._common_make_args(dry_run=dry_run, build_target=build_target, **env_vars) |
| 266 | intermediate_output = self._intermediate_output(**env_vars) | ||
| 267 | generated_files_path = intermediate_output / 'src' | ||
| 268 | keymap_json = generated_files_path / 'keymap.json' | ||
| 229 | compile_args.extend([ | 269 | compile_args.extend([ |
| 230 | f'MAIN_KEYMAP_PATH_1={self._intermediate_output}', | 270 | f'MAIN_KEYMAP_PATH_1={intermediate_output}', |
| 231 | f'MAIN_KEYMAP_PATH_2={self._intermediate_output}', | 271 | f'MAIN_KEYMAP_PATH_2={intermediate_output}', |
| 232 | f'MAIN_KEYMAP_PATH_3={self._intermediate_output}', | 272 | f'MAIN_KEYMAP_PATH_3={intermediate_output}', |
| 233 | f'MAIN_KEYMAP_PATH_4={self._intermediate_output}', | 273 | f'MAIN_KEYMAP_PATH_4={intermediate_output}', |
| 234 | f'MAIN_KEYMAP_PATH_5={self._intermediate_output}', | 274 | f'MAIN_KEYMAP_PATH_5={intermediate_output}', |
| 235 | f'KEYMAP_JSON={self._keymap_json}', | 275 | f'KEYMAP_JSON={keymap_json}', |
| 236 | f'KEYMAP_PATH={self._generated_files_path}', | 276 | f'KEYMAP_PATH={generated_files_path}', |
| 237 | ]) | 277 | ]) |
| 238 | 278 | ||
| 239 | for key, value in env_vars.items(): | ||
| 240 | compile_args.append(f'{key}={value}') | ||
| 241 | |||
| 242 | return compile_args | 279 | return compile_args |
diff --git a/lib/python/qmk/cli/format/json.py b/lib/python/qmk/cli/format/json.py index 87a3837d10..3670294434 100755 --- a/lib/python/qmk/cli/format/json.py +++ b/lib/python/qmk/cli/format/json.py | |||
| @@ -18,13 +18,20 @@ def _detect_json_format(file, json_data): | |||
| 18 | """ | 18 | """ |
| 19 | json_encoder = None | 19 | json_encoder = None |
| 20 | try: | 20 | try: |
| 21 | validate(json_data, 'qmk.user_repo.v1') | 21 | validate(json_data, 'qmk.user_repo.v1_1') |
| 22 | json_encoder = UserspaceJSONEncoder | 22 | json_encoder = UserspaceJSONEncoder |
| 23 | except ValidationError: | 23 | except ValidationError: |
| 24 | pass | 24 | pass |
| 25 | 25 | ||
| 26 | if json_encoder is None: | 26 | if json_encoder is None: |
| 27 | try: | 27 | try: |
| 28 | validate(json_data, 'qmk.user_repo.v1') | ||
| 29 | json_encoder = UserspaceJSONEncoder | ||
| 30 | except ValidationError: | ||
| 31 | pass | ||
| 32 | |||
| 33 | if json_encoder is None: | ||
| 34 | try: | ||
| 28 | validate(json_data, 'qmk.keyboard.v1') | 35 | validate(json_data, 'qmk.keyboard.v1') |
| 29 | json_encoder = InfoJSONEncoder | 36 | json_encoder = InfoJSONEncoder |
| 30 | except ValidationError as e: | 37 | except ValidationError as e: |
diff --git a/lib/python/qmk/cli/mass_compile.py b/lib/python/qmk/cli/mass_compile.py index d13afc6143..cf9be0fd1e 100755 --- a/lib/python/qmk/cli/mass_compile.py +++ b/lib/python/qmk/cli/mass_compile.py | |||
| @@ -7,6 +7,7 @@ from typing import List | |||
| 7 | from pathlib import Path | 7 | from pathlib import Path |
| 8 | from subprocess import DEVNULL | 8 | from subprocess import DEVNULL |
| 9 | from milc import cli | 9 | from milc import cli |
| 10 | import shlex | ||
| 10 | 11 | ||
| 11 | from qmk.constants import QMK_FIRMWARE | 12 | from qmk.constants import QMK_FIRMWARE |
| 12 | from qmk.commands import find_make, get_make_parallel_args, build_environment | 13 | from qmk.commands import find_make, get_make_parallel_args, build_environment |
| @@ -26,7 +27,8 @@ def mass_compile_targets(targets: List[BuildTarget], clean: bool, dry_run: bool, | |||
| 26 | if dry_run: | 27 | if dry_run: |
| 27 | cli.log.info('Compilation targets:') | 28 | cli.log.info('Compilation targets:') |
| 28 | for target in sorted(targets, key=lambda t: (t.keyboard, t.keymap)): | 29 | for target in sorted(targets, key=lambda t: (t.keyboard, t.keymap)): |
| 29 | cli.log.info(f"{{fg_cyan}}qmk compile -kb {target.keyboard} -km {target.keymap}{{fg_reset}}") | 30 | extra_args = ' '.join([f"-e {shlex.quote(f'{k}={v}')}" for k, v in target.extra_args.items()]) |
| 31 | cli.log.info(f"{{fg_cyan}}qmk compile -kb {target.keyboard} -km {target.keymap} {extra_args}{{fg_reset}}") | ||
| 30 | else: | 32 | else: |
| 31 | if clean: | 33 | if clean: |
| 32 | cli.run([make_cmd, 'clean'], capture_output=False, stdin=DEVNULL) | 34 | cli.run([make_cmd, 'clean'], capture_output=False, stdin=DEVNULL) |
| @@ -36,18 +38,26 @@ def mass_compile_targets(targets: List[BuildTarget], clean: bool, dry_run: bool, | |||
| 36 | for target in sorted(targets, key=lambda t: (t.keyboard, t.keymap)): | 38 | for target in sorted(targets, key=lambda t: (t.keyboard, t.keymap)): |
| 37 | keyboard_name = target.keyboard | 39 | keyboard_name = target.keyboard |
| 38 | keymap_name = target.keymap | 40 | keymap_name = target.keymap |
| 41 | keyboard_safe = keyboard_name.replace('/', '_') | ||
| 42 | target_filename = target.target_name(**env) | ||
| 39 | target.configure(parallel=1) # We ignore parallelism on a per-build basis as we defer to the parent make invocation | 43 | target.configure(parallel=1) # We ignore parallelism on a per-build basis as we defer to the parent make invocation |
| 40 | 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` | 44 | 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` |
| 41 | command = target.compile_command(**env) | 45 | command = target.compile_command(**env) |
| 42 | command[0] = '+@$(MAKE)' # Override the make so that we can use jobserver to handle parallelism | 46 | command[0] = '+@$(MAKE)' # Override the make so that we can use jobserver to handle parallelism |
| 43 | keyboard_safe = keyboard_name.replace('/', '_') | 47 | extra_args = '_'.join([f"{k}_{v}" for k, v in target.extra_args.items()]) |
| 44 | build_log = f"{QMK_FIRMWARE}/.build/build.log.{os.getpid()}.{keyboard_safe}.{keymap_name}" | 48 | build_log = f"{QMK_FIRMWARE}/.build/build.log.{os.getpid()}.{keyboard_safe}.{keymap_name}" |
| 45 | failed_log = f"{QMK_FIRMWARE}/.build/failed.log.{os.getpid()}.{keyboard_safe}.{keymap_name}" | 49 | failed_log = f"{QMK_FIRMWARE}/.build/failed.log.{os.getpid()}.{keyboard_safe}.{keymap_name}" |
| 50 | target_suffix = '' | ||
| 51 | if len(extra_args) > 0: | ||
| 52 | build_log += f".{extra_args}" | ||
| 53 | failed_log += f".{extra_args}" | ||
| 54 | target_suffix = f"_{extra_args}" | ||
| 46 | # yapf: disable | 55 | # yapf: disable |
| 47 | f.write( | 56 | f.write( |
| 48 | f"""\ | 57 | f"""\ |
| 49 | all: {keyboard_safe}_{keymap_name}_binary | 58 | .PHONY: {target_filename}{target_suffix}_binary |
| 50 | {keyboard_safe}_{keymap_name}_binary: | 59 | all: {target_filename}{target_suffix}_binary |
| 60 | {target_filename}{target_suffix}_binary: | ||
| 51 | @rm -f "{build_log}" || true | 61 | @rm -f "{build_log}" || true |
| 52 | @echo "Compiling QMK Firmware for target: '{keyboard_name}:{keymap_name}'..." >>"{build_log}" | 62 | @echo "Compiling QMK Firmware for target: '{keyboard_name}:{keymap_name}'..." >>"{build_log}" |
| 53 | {' '.join(command)} \\ | 63 | {' '.join(command)} \\ |
| @@ -65,9 +75,9 @@ all: {keyboard_safe}_{keymap_name}_binary | |||
| 65 | # yapf: disable | 75 | # yapf: disable |
| 66 | f.write( | 76 | f.write( |
| 67 | f"""\ | 77 | f"""\ |
| 68 | @rm -rf "{QMK_FIRMWARE}/.build/{keyboard_safe}_{keymap_name}.elf" 2>/dev/null || true | 78 | @rm -rf "{QMK_FIRMWARE}/.build/{target_filename}.elf" 2>/dev/null || true |
| 69 | @rm -rf "{QMK_FIRMWARE}/.build/{keyboard_safe}_{keymap_name}.map" 2>/dev/null || true | 79 | @rm -rf "{QMK_FIRMWARE}/.build/{target_filename}.map" 2>/dev/null || true |
| 70 | @rm -rf "{QMK_FIRMWARE}/.build/obj_{keyboard_safe}_{keymap_name}" || true | 80 | @rm -rf "{QMK_FIRMWARE}/.build/obj_{target_filename}" || true |
| 71 | """# noqa | 81 | """# noqa |
| 72 | ) | 82 | ) |
| 73 | # yapf: enable | 83 | # yapf: enable |
diff --git a/lib/python/qmk/cli/userspace/add.py b/lib/python/qmk/cli/userspace/add.py index 8993d54dba..0d6f32cd11 100644 --- a/lib/python/qmk/cli/userspace/add.py +++ b/lib/python/qmk/cli/userspace/add.py | |||
| @@ -1,8 +1,9 @@ | |||
| 1 | # Copyright 2023 Nick Brassel (@tzarc) | 1 | # Copyright 2023-2024 Nick Brassel (@tzarc) |
| 2 | # SPDX-License-Identifier: GPL-2.0-or-later | 2 | # SPDX-License-Identifier: GPL-2.0-or-later |
| 3 | from pathlib import Path | 3 | from pathlib import Path |
| 4 | from milc import cli | 4 | from milc import cli |
| 5 | 5 | ||
| 6 | from qmk.commands import parse_env_vars | ||
| 6 | from qmk.constants import QMK_USERSPACE, HAS_QMK_USERSPACE | 7 | from qmk.constants import QMK_USERSPACE, HAS_QMK_USERSPACE |
| 7 | from qmk.keyboard import keyboard_completer, keyboard_folder_or_all | 8 | from qmk.keyboard import keyboard_completer, keyboard_folder_or_all |
| 8 | from qmk.keymap import keymap_completer, is_keymap_target | 9 | from qmk.keymap import keymap_completer, is_keymap_target |
| @@ -12,12 +13,15 @@ from qmk.userspace import UserspaceDefs | |||
| 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('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('-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.argument('-km', '--keymap', completer=keymap_completer, help='The keymap to build a firmware for. Ignored when a configurator export is supplied.') |
| 16 | @cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Extra variables to set during build. May be passed multiple times.") | ||
| 15 | @cli.subcommand('Adds a build target to userspace `qmk.json`.') | 17 | @cli.subcommand('Adds a build target to userspace `qmk.json`.') |
| 16 | def userspace_add(cli): | 18 | def userspace_add(cli): |
| 17 | if not HAS_QMK_USERSPACE: | 19 | 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.') | 20 | cli.log.error('Could not determine QMK userspace location. Please run `qmk doctor` or `qmk userspace-doctor` to diagnose.') |
| 19 | return False | 21 | return False |
| 20 | 22 | ||
| 23 | build_env = None if len(cli.args.env) == 0 else parse_env_vars(cli.args.env) | ||
| 24 | |||
| 21 | userspace = UserspaceDefs(QMK_USERSPACE / 'qmk.json') | 25 | userspace = UserspaceDefs(QMK_USERSPACE / 'qmk.json') |
| 22 | 26 | ||
| 23 | if len(cli.args.builds) > 0: | 27 | if len(cli.args.builds) > 0: |
| @@ -44,8 +48,8 @@ def userspace_add(cli): | |||
| 44 | cli.config.new_keymap.keyboard = cli.args.keyboard | 48 | cli.config.new_keymap.keyboard = cli.args.keyboard |
| 45 | cli.config.new_keymap.keymap = cli.args.keymap | 49 | cli.config.new_keymap.keymap = cli.args.keymap |
| 46 | if new_keymap(cli) is not False: | 50 | if new_keymap(cli) is not False: |
| 47 | userspace.add_target(keyboard=cli.args.keyboard, keymap=cli.args.keymap) | 51 | userspace.add_target(keyboard=cli.args.keyboard, keymap=cli.args.keymap, build_env=build_env) |
| 48 | else: | 52 | else: |
| 49 | userspace.add_target(keyboard=cli.args.keyboard, keymap=cli.args.keymap) | 53 | userspace.add_target(keyboard=cli.args.keyboard, keymap=cli.args.keymap, build_env=build_env) |
| 50 | 54 | ||
| 51 | return userspace.save() | 55 | return userspace.save() |
diff --git a/lib/python/qmk/cli/userspace/compile.py b/lib/python/qmk/cli/userspace/compile.py index e8cdf6cd97..f164ca2ef1 100644 --- a/lib/python/qmk/cli/userspace/compile.py +++ b/lib/python/qmk/cli/userspace/compile.py | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | # Copyright 2023 Nick Brassel (@tzarc) | 1 | # Copyright 2023-2024 Nick Brassel (@tzarc) |
| 2 | # SPDX-License-Identifier: GPL-2.0-or-later | 2 | # SPDX-License-Identifier: GPL-2.0-or-later |
| 3 | from pathlib import Path | 3 | from pathlib import Path |
| 4 | from milc import cli | 4 | from milc import cli |
| @@ -12,6 +12,10 @@ from qmk.cli.mass_compile import mass_compile_targets | |||
| 12 | from qmk.util import maybe_exit_config | 12 | from qmk.util import maybe_exit_config |
| 13 | 13 | ||
| 14 | 14 | ||
| 15 | def _extra_arg_setter(target, extra_args): | ||
| 16 | target.extra_args = extra_args | ||
| 17 | |||
| 18 | |||
| 15 | @cli.argument('-t', '--no-temp', arg_only=True, action='store_true', help="Remove temporary files during build.") | 19 | @cli.argument('-t', '--no-temp', arg_only=True, action='store_true', help="Remove temporary files during build.") |
| 16 | @cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.") | 20 | @cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.") |
| 17 | @cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.") | 21 | @cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.") |
| @@ -33,8 +37,8 @@ def userspace_compile(cli): | |||
| 33 | if isinstance(e, Path): | 37 | if isinstance(e, Path): |
| 34 | build_targets.append(JsonKeymapBuildTarget(e)) | 38 | build_targets.append(JsonKeymapBuildTarget(e)) |
| 35 | elif isinstance(e, dict): | 39 | elif isinstance(e, dict): |
| 36 | keyboard_keymap_targets.append((e['keyboard'], e['keymap'])) | 40 | f = e['env'] if 'env' in e else None |
| 37 | 41 | keyboard_keymap_targets.append((e['keyboard'], e['keymap'], f)) | |
| 38 | if len(keyboard_keymap_targets) > 0: | 42 | if len(keyboard_keymap_targets) > 0: |
| 39 | build_targets.extend(search_keymap_targets(keyboard_keymap_targets)) | 43 | build_targets.extend(search_keymap_targets(keyboard_keymap_targets)) |
| 40 | 44 | ||
diff --git a/lib/python/qmk/cli/userspace/list.py b/lib/python/qmk/cli/userspace/list.py index 8689c80a76..9f83a14a2a 100644 --- a/lib/python/qmk/cli/userspace/list.py +++ b/lib/python/qmk/cli/userspace/list.py | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | # Copyright 2023 Nick Brassel (@tzarc) | 1 | # Copyright 2023-2024 Nick Brassel (@tzarc) |
| 2 | # SPDX-License-Identifier: GPL-2.0-or-later | 2 | # SPDX-License-Identifier: GPL-2.0-or-later |
| 3 | from pathlib import Path | 3 | from pathlib import Path |
| 4 | from dotty_dict import Dotty | 4 | from dotty_dict import Dotty |
| @@ -13,6 +13,10 @@ from qmk.search import search_keymap_targets | |||
| 13 | from qmk.util import maybe_exit_config | 13 | from qmk.util import maybe_exit_config |
| 14 | 14 | ||
| 15 | 15 | ||
| 16 | def _extra_arg_setter(target, extra_args): | ||
| 17 | target.extra_args = extra_args | ||
| 18 | |||
| 19 | |||
| 16 | @cli.argument('-e', '--expand', arg_only=True, action='store_true', help="Expands any use of `all` for either keyboard or keymap.") | 20 | @cli.argument('-e', '--expand', arg_only=True, action='store_true', help="Expands any use of `all` for either keyboard or keymap.") |
| 17 | @cli.subcommand('Lists the build targets specified in userspace `qmk.json`.') | 21 | @cli.subcommand('Lists the build targets specified in userspace `qmk.json`.') |
| 18 | def userspace_list(cli): | 22 | def userspace_list(cli): |
| @@ -26,11 +30,15 @@ def userspace_list(cli): | |||
| 26 | 30 | ||
| 27 | if cli.args.expand: | 31 | if cli.args.expand: |
| 28 | build_targets = [] | 32 | build_targets = [] |
| 33 | keyboard_keymap_targets = [] | ||
| 29 | for e in userspace.build_targets: | 34 | for e in userspace.build_targets: |
| 30 | if isinstance(e, Path): | 35 | if isinstance(e, Path): |
| 31 | build_targets.append(e) | 36 | build_targets.append(e) |
| 32 | elif isinstance(e, dict) or isinstance(e, Dotty): | 37 | elif isinstance(e, dict) or isinstance(e, Dotty): |
| 33 | build_targets.extend(search_keymap_targets([(e['keyboard'], e['keymap'])])) | 38 | f = e['env'] if 'env' in e else None |
| 39 | keyboard_keymap_targets.append((e['keyboard'], e['keymap'], f)) | ||
| 40 | if len(keyboard_keymap_targets) > 0: | ||
| 41 | build_targets.extend(search_keymap_targets(keyboard_keymap_targets)) | ||
| 34 | else: | 42 | else: |
| 35 | build_targets = userspace.build_targets | 43 | build_targets = userspace.build_targets |
| 36 | 44 | ||
| @@ -43,12 +51,19 @@ def userspace_list(cli): | |||
| 43 | # keyboard/keymap dict from userspace | 51 | # keyboard/keymap dict from userspace |
| 44 | keyboard = e['keyboard'] | 52 | keyboard = e['keyboard'] |
| 45 | keymap = e['keymap'] | 53 | keymap = e['keymap'] |
| 54 | extra_args = e.get('env') | ||
| 46 | elif isinstance(e, BuildTarget): | 55 | elif isinstance(e, BuildTarget): |
| 47 | # BuildTarget from search_keymap_targets() | 56 | # BuildTarget from search_keymap_targets() |
| 48 | keyboard = e.keyboard | 57 | keyboard = e.keyboard |
| 49 | keymap = e.keymap | 58 | keymap = e.keymap |
| 59 | extra_args = e.extra_args | ||
| 60 | |||
| 61 | extra_args_str = '' | ||
| 62 | if extra_args is not None and len(extra_args) > 0: | ||
| 63 | extra_args_str = ', '.join([f'{{fg_cyan}}{k}={v}{{fg_reset}}' for k, v in extra_args.items()]) | ||
| 64 | extra_args_str = f' ({{fg_cyan}}{extra_args_str}{{fg_reset}})' | ||
| 50 | 65 | ||
| 51 | if is_all_keyboards(keyboard) or is_keymap_target(keyboard_folder(keyboard), keymap): | 66 | if is_all_keyboards(keyboard) or is_keymap_target(keyboard_folder(keyboard), keymap): |
| 52 | cli.log.info(f'Keyboard: {{fg_cyan}}{keyboard}{{fg_reset}}, keymap: {{fg_cyan}}{keymap}{{fg_reset}}') | 67 | cli.log.info(f'Keyboard: {{fg_cyan}}{keyboard}{{fg_reset}}, keymap: {{fg_cyan}}{keymap}{{fg_reset}}{extra_args_str}') |
| 53 | else: | 68 | else: |
| 54 | cli.log.warn(f'Keyboard: {{fg_cyan}}{keyboard}{{fg_reset}}, keymap: {{fg_cyan}}{keymap}{{fg_reset}} -- not found!') | 69 | cli.log.warn(f'Keyboard: {{fg_cyan}}{keyboard}{{fg_reset}}, keymap: {{fg_cyan}}{keymap}{{fg_reset}}{extra_args_str} -- not found!') |
diff --git a/lib/python/qmk/cli/userspace/remove.py b/lib/python/qmk/cli/userspace/remove.py index c7d180bfd1..b2da08a98e 100644 --- a/lib/python/qmk/cli/userspace/remove.py +++ b/lib/python/qmk/cli/userspace/remove.py | |||
| @@ -1,8 +1,9 @@ | |||
| 1 | # Copyright 2023 Nick Brassel (@tzarc) | 1 | # Copyright 2023-2024 Nick Brassel (@tzarc) |
| 2 | # SPDX-License-Identifier: GPL-2.0-or-later | 2 | # SPDX-License-Identifier: GPL-2.0-or-later |
| 3 | from pathlib import Path | 3 | from pathlib import Path |
| 4 | from milc import cli | 4 | from milc import cli |
| 5 | 5 | ||
| 6 | from qmk.commands import parse_env_vars | ||
| 6 | from qmk.constants import QMK_USERSPACE, HAS_QMK_USERSPACE | 7 | from qmk.constants import QMK_USERSPACE, HAS_QMK_USERSPACE |
| 7 | from qmk.keyboard import keyboard_completer, keyboard_folder_or_all | 8 | from qmk.keyboard import keyboard_completer, keyboard_folder_or_all |
| 8 | from qmk.keymap import keymap_completer | 9 | from qmk.keymap import keymap_completer |
| @@ -12,12 +13,15 @@ from qmk.userspace import UserspaceDefs | |||
| 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('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('-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.argument('-km', '--keymap', completer=keymap_completer, help='The keymap to build a firmware for. Ignored when a configurator export is supplied.') |
| 16 | @cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Extra variables to set during build. May be passed multiple times.") | ||
| 15 | @cli.subcommand('Removes a build target from userspace `qmk.json`.') | 17 | @cli.subcommand('Removes a build target from userspace `qmk.json`.') |
| 16 | def userspace_remove(cli): | 18 | def userspace_remove(cli): |
| 17 | if not HAS_QMK_USERSPACE: | 19 | 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.') | 20 | cli.log.error('Could not determine QMK userspace location. Please run `qmk doctor` or `qmk userspace-doctor` to diagnose.') |
| 19 | return False | 21 | return False |
| 20 | 22 | ||
| 23 | build_env = None if len(cli.args.env) == 0 else parse_env_vars(cli.args.env) | ||
| 24 | |||
| 21 | userspace = UserspaceDefs(QMK_USERSPACE / 'qmk.json') | 25 | userspace = UserspaceDefs(QMK_USERSPACE / 'qmk.json') |
| 22 | 26 | ||
| 23 | if len(cli.args.builds) > 0: | 27 | if len(cli.args.builds) > 0: |
| @@ -29,9 +33,9 @@ def userspace_remove(cli): | |||
| 29 | 33 | ||
| 30 | for e in make_like_targets: | 34 | for e in make_like_targets: |
| 31 | s = e.split(':') | 35 | s = e.split(':') |
| 32 | userspace.remove_target(keyboard=s[0], keymap=s[1]) | 36 | userspace.remove_target(keyboard=s[0], keymap=s[1], build_env=build_env) |
| 33 | 37 | ||
| 34 | else: | 38 | else: |
| 35 | userspace.remove_target(keyboard=cli.args.keyboard, keymap=cli.args.keymap) | 39 | userspace.remove_target(keyboard=cli.args.keyboard, keymap=cli.args.keymap, build_env=build_env) |
| 36 | 40 | ||
| 37 | return userspace.save() | 41 | return userspace.save() |
diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py index b0b8ccb244..97d9c5032c 100644 --- a/lib/python/qmk/commands.py +++ b/lib/python/qmk/commands.py | |||
| @@ -68,7 +68,7 @@ def parse_configurator_json(configurator_file): | |||
| 68 | return user_keymap | 68 | return user_keymap |
| 69 | 69 | ||
| 70 | 70 | ||
| 71 | def build_environment(args): | 71 | def parse_env_vars(args): |
| 72 | """Common processing for cli.args.env | 72 | """Common processing for cli.args.env |
| 73 | """ | 73 | """ |
| 74 | envs = {} | 74 | envs = {} |
| @@ -78,6 +78,11 @@ def build_environment(args): | |||
| 78 | envs[key] = value | 78 | envs[key] = value |
| 79 | else: | 79 | else: |
| 80 | cli.log.warning('Invalid environment variable: %s', env) | 80 | cli.log.warning('Invalid environment variable: %s', env) |
| 81 | return envs | ||
| 82 | |||
| 83 | |||
| 84 | def build_environment(args): | ||
| 85 | envs = parse_env_vars(args) | ||
| 81 | 86 | ||
| 82 | if HAS_QMK_USERSPACE: | 87 | if HAS_QMK_USERSPACE: |
| 83 | envs['QMK_USERSPACE'] = Path(QMK_USERSPACE).resolve() | 88 | envs['QMK_USERSPACE'] = Path(QMK_USERSPACE).resolve() |
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index 5948b66b5e..72b19a9fec 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py | |||
| @@ -212,7 +212,7 @@ def _validate(keyboard, info_data): | |||
| 212 | maybe_exit(1) | 212 | maybe_exit(1) |
| 213 | 213 | ||
| 214 | 214 | ||
| 215 | def info_json(keyboard): | 215 | def info_json(keyboard, force_layout=None): |
| 216 | """Generate the info.json data for a specific keyboard. | 216 | """Generate the info.json data for a specific keyboard. |
| 217 | """ | 217 | """ |
| 218 | cur_dir = Path('keyboards') | 218 | cur_dir = Path('keyboards') |
| @@ -256,6 +256,11 @@ def info_json(keyboard): | |||
| 256 | # Merge in data from <keyboard.c> | 256 | # Merge in data from <keyboard.c> |
| 257 | info_data = _extract_led_config(info_data, str(keyboard)) | 257 | info_data = _extract_led_config(info_data, str(keyboard)) |
| 258 | 258 | ||
| 259 | # Force a community layout if requested | ||
| 260 | community_layouts = info_data.get("community_layouts", []) | ||
| 261 | if force_layout in community_layouts: | ||
| 262 | info_data["community_layouts"] = [force_layout] | ||
| 263 | |||
| 259 | # Validate | 264 | # Validate |
| 260 | _validate(keyboard, info_data) | 265 | _validate(keyboard, info_data) |
| 261 | 266 | ||
| @@ -1008,25 +1013,25 @@ def find_info_json(keyboard): | |||
| 1008 | return [info_json for info_json in info_jsons if info_json.exists()] | 1013 | return [info_json for info_json in info_jsons if info_json.exists()] |
| 1009 | 1014 | ||
| 1010 | 1015 | ||
| 1011 | def keymap_json_config(keyboard, keymap): | 1016 | def keymap_json_config(keyboard, keymap, force_layout=None): |
| 1012 | """Extract keymap level config | 1017 | """Extract keymap level config |
| 1013 | """ | 1018 | """ |
| 1014 | # TODO: resolve keymap.py and info.py circular dependencies | 1019 | # TODO: resolve keymap.py and info.py circular dependencies |
| 1015 | from qmk.keymap import locate_keymap | 1020 | from qmk.keymap import locate_keymap |
| 1016 | 1021 | ||
| 1017 | keymap_folder = locate_keymap(keyboard, keymap).parent | 1022 | keymap_folder = locate_keymap(keyboard, keymap, force_layout=force_layout).parent |
| 1018 | 1023 | ||
| 1019 | km_info_json = parse_configurator_json(keymap_folder / 'keymap.json') | 1024 | km_info_json = parse_configurator_json(keymap_folder / 'keymap.json') |
| 1020 | return km_info_json.get('config', {}) | 1025 | return km_info_json.get('config', {}) |
| 1021 | 1026 | ||
| 1022 | 1027 | ||
| 1023 | def keymap_json(keyboard, keymap): | 1028 | def keymap_json(keyboard, keymap, force_layout=None): |
| 1024 | """Generate the info.json data for a specific keymap. | 1029 | """Generate the info.json data for a specific keymap. |
| 1025 | """ | 1030 | """ |
| 1026 | # TODO: resolve keymap.py and info.py circular dependencies | 1031 | # TODO: resolve keymap.py and info.py circular dependencies |
| 1027 | from qmk.keymap import locate_keymap | 1032 | from qmk.keymap import locate_keymap |
| 1028 | 1033 | ||
| 1029 | keymap_folder = locate_keymap(keyboard, keymap).parent | 1034 | keymap_folder = locate_keymap(keyboard, keymap, force_layout=force_layout).parent |
| 1030 | 1035 | ||
| 1031 | # Files to scan | 1036 | # Files to scan |
| 1032 | keymap_config = keymap_folder / 'config.h' | 1037 | keymap_config = keymap_folder / 'config.h' |
| @@ -1034,10 +1039,10 @@ def keymap_json(keyboard, keymap): | |||
| 1034 | keymap_file = keymap_folder / 'keymap.json' | 1039 | keymap_file = keymap_folder / 'keymap.json' |
| 1035 | 1040 | ||
| 1036 | # Build the info.json file | 1041 | # Build the info.json file |
| 1037 | kb_info_json = info_json(keyboard) | 1042 | kb_info_json = info_json(keyboard, force_layout=force_layout) |
| 1038 | 1043 | ||
| 1039 | # Merge in the data from keymap.json | 1044 | # Merge in the data from keymap.json |
| 1040 | km_info_json = keymap_json_config(keyboard, keymap) if keymap_file.exists() else {} | 1045 | km_info_json = keymap_json_config(keyboard, keymap, force_layout=force_layout) if keymap_file.exists() else {} |
| 1041 | deep_update(kb_info_json, km_info_json) | 1046 | deep_update(kb_info_json, km_info_json) |
| 1042 | 1047 | ||
| 1043 | # Merge in the data from config.h, and rules.mk | 1048 | # Merge in the data from config.h, and rules.mk |
diff --git a/lib/python/qmk/keymap.py b/lib/python/qmk/keymap.py index f3505d324e..97c358788a 100644 --- a/lib/python/qmk/keymap.py +++ b/lib/python/qmk/keymap.py | |||
| @@ -356,7 +356,7 @@ def write(keymap_json): | |||
| 356 | return write_file(keymap_file, keymap_content) | 356 | return write_file(keymap_file, keymap_content) |
| 357 | 357 | ||
| 358 | 358 | ||
| 359 | def locate_keymap(keyboard, keymap): | 359 | def locate_keymap(keyboard, keymap, force_layout=None): |
| 360 | """Returns the path to a keymap for a specific keyboard. | 360 | """Returns the path to a keymap for a specific keyboard. |
| 361 | """ | 361 | """ |
| 362 | if not qmk.path.is_keyboard(keyboard): | 362 | if not qmk.path.is_keyboard(keyboard): |
| @@ -395,7 +395,7 @@ def locate_keymap(keyboard, keymap): | |||
| 395 | return keymap_path | 395 | return keymap_path |
| 396 | 396 | ||
| 397 | # Check community layouts as a fallback | 397 | # Check community layouts as a fallback |
| 398 | info = info_json(keyboard) | 398 | info = info_json(keyboard, force_layout=force_layout) |
| 399 | 399 | ||
| 400 | community_parents = list(Path('layouts').glob('*/')) | 400 | community_parents = list(Path('layouts').glob('*/')) |
| 401 | if HAS_QMK_USERSPACE and (Path(QMK_USERSPACE) / "layouts").exists(): | 401 | if HAS_QMK_USERSPACE and (Path(QMK_USERSPACE) / "layouts").exists(): |
diff --git a/lib/python/qmk/search.py b/lib/python/qmk/search.py index 2afb3033fc..baaf11eb34 100644 --- a/lib/python/qmk/search.py +++ b/lib/python/qmk/search.py | |||
| @@ -1,11 +1,13 @@ | |||
| 1 | """Functions for searching through QMK keyboards and keymaps. | 1 | """Functions for searching through QMK keyboards and keymaps. |
| 2 | """ | 2 | """ |
| 3 | from dataclasses import dataclass | ||
| 3 | import contextlib | 4 | import contextlib |
| 4 | import functools | 5 | import functools |
| 5 | import fnmatch | 6 | import fnmatch |
| 7 | import json | ||
| 6 | import logging | 8 | import logging |
| 7 | import re | 9 | import re |
| 8 | from typing import Callable, List, Optional, Tuple | 10 | from typing import Callable, Dict, List, Optional, Tuple, Union |
| 9 | from dotty_dict import dotty, Dotty | 11 | from dotty_dict import dotty, Dotty |
| 10 | from milc import cli | 12 | from milc import cli |
| 11 | 13 | ||
| @@ -15,7 +17,32 @@ from qmk.keyboard import list_keyboards, keyboard_folder | |||
| 15 | from qmk.keymap import list_keymaps, locate_keymap | 17 | from qmk.keymap import list_keymaps, locate_keymap |
| 16 | from qmk.build_targets import KeyboardKeymapBuildTarget, BuildTarget | 18 | from qmk.build_targets import KeyboardKeymapBuildTarget, BuildTarget |
| 17 | 19 | ||
| 18 | TargetInfo = Tuple[str, str, dict] | 20 | |
| 21 | @dataclass | ||
| 22 | class KeyboardKeymapDesc: | ||
| 23 | keyboard: str | ||
| 24 | keymap: str | ||
| 25 | data: dict = None | ||
| 26 | extra_args: dict = None | ||
| 27 | |||
| 28 | def __hash__(self) -> int: | ||
| 29 | return self.keyboard.__hash__() ^ self.keymap.__hash__() ^ json.dumps(self.extra_args, sort_keys=True).__hash__() | ||
| 30 | |||
| 31 | def __lt__(self, other) -> bool: | ||
| 32 | return (self.keyboard, self.keymap, json.dumps(self.extra_args, sort_keys=True)) < (other.keyboard, other.keymap, json.dumps(other.extra_args, sort_keys=True)) | ||
| 33 | |||
| 34 | def load_data(self): | ||
| 35 | data = keymap_json(self.keyboard, self.keymap) | ||
| 36 | self.data = data.to_dict() if isinstance(data, Dotty) else data | ||
| 37 | |||
| 38 | @property | ||
| 39 | def dotty(self) -> Dotty: | ||
| 40 | return dotty(self.data) if self.data is not None else None | ||
| 41 | |||
| 42 | def to_build_target(self) -> KeyboardKeymapBuildTarget: | ||
| 43 | target = KeyboardKeymapBuildTarget(keyboard=self.keyboard, keymap=self.keymap, json=self.data) | ||
| 44 | target.extra_args = self.extra_args | ||
| 45 | return target | ||
| 19 | 46 | ||
| 20 | 47 | ||
| 21 | # by using a class for filters, we dont need to worry about capturing values | 48 | # by using a class for filters, we dont need to worry about capturing values |
| @@ -36,7 +63,7 @@ class FilterFunction: | |||
| 36 | value: Optional[str] | 63 | value: Optional[str] |
| 37 | 64 | ||
| 38 | func_name: str | 65 | func_name: str |
| 39 | apply: Callable[[TargetInfo], bool] | 66 | apply: Callable[[KeyboardKeymapDesc], bool] |
| 40 | 67 | ||
| 41 | def __init__(self, key, value): | 68 | def __init__(self, key, value): |
| 42 | self.key = key | 69 | self.key = key |
| @@ -46,33 +73,29 @@ class FilterFunction: | |||
| 46 | class Exists(FilterFunction): | 73 | class Exists(FilterFunction): |
| 47 | func_name = "exists" | 74 | func_name = "exists" |
| 48 | 75 | ||
| 49 | def apply(self, target_info: TargetInfo) -> bool: | 76 | def apply(self, target_info: KeyboardKeymapDesc) -> bool: |
| 50 | _kb, _km, info = target_info | 77 | return self.key in target_info.data |
| 51 | return self.key in info | ||
| 52 | 78 | ||
| 53 | 79 | ||
| 54 | class Absent(FilterFunction): | 80 | class Absent(FilterFunction): |
| 55 | func_name = "absent" | 81 | func_name = "absent" |
| 56 | 82 | ||
| 57 | def apply(self, target_info: TargetInfo) -> bool: | 83 | def apply(self, target_info: KeyboardKeymapDesc) -> bool: |
| 58 | _kb, _km, info = target_info | 84 | return self.key not in target_info.data |
| 59 | return self.key not in info | ||
| 60 | 85 | ||
| 61 | 86 | ||
| 62 | class Length(FilterFunction): | 87 | class Length(FilterFunction): |
| 63 | func_name = "length" | 88 | func_name = "length" |
| 64 | 89 | ||
| 65 | def apply(self, target_info: TargetInfo) -> bool: | 90 | def apply(self, target_info: KeyboardKeymapDesc) -> bool: |
| 66 | _kb, _km, info = target_info | 91 | return (self.key in target_info.data and len(target_info.data[self.key]) == int(self.value)) |
| 67 | return (self.key in info and len(info[self.key]) == int(self.value)) | ||
| 68 | 92 | ||
| 69 | 93 | ||
| 70 | class Contains(FilterFunction): | 94 | class Contains(FilterFunction): |
| 71 | func_name = "contains" | 95 | func_name = "contains" |
| 72 | 96 | ||
| 73 | def apply(self, target_info: TargetInfo) -> bool: | 97 | def apply(self, target_info: KeyboardKeymapDesc) -> bool: |
| 74 | _kb, _km, info = target_info | 98 | return (self.key in target_info.data and self.value in target_info.data[self.key]) |
| 75 | return (self.key in info and self.value in info[self.key]) | ||
| 76 | 99 | ||
| 77 | 100 | ||
| 78 | def _get_filter_class(func_name: str, key: str, value: str) -> Optional[FilterFunction]: | 101 | def _get_filter_class(func_name: str, key: str, value: str) -> Optional[FilterFunction]: |
| @@ -109,12 +132,12 @@ def ignore_logging(): | |||
| 109 | _set_log_level(old) | 132 | _set_log_level(old) |
| 110 | 133 | ||
| 111 | 134 | ||
| 112 | def _all_keymaps(keyboard): | 135 | def _all_keymaps(keyboard) -> List[KeyboardKeymapDesc]: |
| 113 | """Returns a list of tuples of (keyboard, keymap) for all keymaps for the given keyboard. | 136 | """Returns a list of KeyboardKeymapDesc for all keymaps for the given keyboard. |
| 114 | """ | 137 | """ |
| 115 | with ignore_logging(): | 138 | with ignore_logging(): |
| 116 | keyboard = keyboard_folder(keyboard) | 139 | keyboard = keyboard_folder(keyboard) |
| 117 | return [(keyboard, keymap) for keymap in list_keymaps(keyboard)] | 140 | return [KeyboardKeymapDesc(keyboard, keymap) for keymap in list_keymaps(keyboard)] |
| 118 | 141 | ||
| 119 | 142 | ||
| 120 | def _keymap_exists(keyboard, keymap): | 143 | def _keymap_exists(keyboard, keymap): |
| @@ -124,85 +147,91 @@ def _keymap_exists(keyboard, keymap): | |||
| 124 | return keyboard if locate_keymap(keyboard, keymap) is not None else None | 147 | return keyboard if locate_keymap(keyboard, keymap) is not None else None |
| 125 | 148 | ||
| 126 | 149 | ||
| 127 | def _load_keymap_info(target: Tuple[str, str]) -> TargetInfo: | 150 | def _load_keymap_info(target: KeyboardKeymapDesc) -> KeyboardKeymapDesc: |
| 128 | """Returns a tuple of (keyboard, keymap, info.json) for the given keyboard/keymap combination. | 151 | """Ensures a KeyboardKeymapDesc has its data loaded. |
| 129 | """ | 152 | """ |
| 130 | kb, km = target | ||
| 131 | with ignore_logging(): | 153 | with ignore_logging(): |
| 132 | return (kb, km, keymap_json(kb, km)) | 154 | target.load_data() # Ensure we load the data first |
| 155 | return target | ||
| 133 | 156 | ||
| 134 | 157 | ||
| 135 | def expand_make_targets(targets: List[str]) -> List[Tuple[str, str]]: | 158 | def expand_make_targets(targets: List[Union[str, Tuple[str, Dict[str, str]]]]) -> List[KeyboardKeymapDesc]: |
| 136 | """Expand a list of make targets into a list of (keyboard, keymap) tuples. | 159 | """Expand a list of make targets into a list of KeyboardKeymapDesc. |
| 137 | 160 | ||
| 138 | Caters for 'all' in either keyboard or keymap, or both. | 161 | Caters for 'all' in either keyboard or keymap, or both. |
| 139 | """ | 162 | """ |
| 140 | split_targets = [] | 163 | split_targets = [] |
| 141 | for target in targets: | 164 | for target in targets: |
| 142 | split_target = target.split(':') | 165 | extra_args = None |
| 166 | if isinstance(target, tuple): | ||
| 167 | split_target = target[0].split(':') | ||
| 168 | extra_args = target[1] | ||
| 169 | else: | ||
| 170 | split_target = target.split(':') | ||
| 143 | if len(split_target) != 2: | 171 | if len(split_target) != 2: |
| 144 | cli.log.error(f"Invalid build target: {target}") | 172 | cli.log.error(f"Invalid build target: {target}") |
| 145 | return [] | 173 | return [] |
| 146 | split_targets.append((split_target[0], split_target[1])) | 174 | split_targets.append(KeyboardKeymapDesc(split_target[0], split_target[1], extra_args=extra_args)) |
| 147 | return expand_keymap_targets(split_targets) | 175 | return expand_keymap_targets(split_targets) |
| 148 | 176 | ||
| 149 | 177 | ||
| 150 | def _expand_keymap_target(keyboard: str, keymap: str, all_keyboards: List[str] = None) -> List[Tuple[str, str]]: | 178 | def _expand_keymap_target(target: KeyboardKeymapDesc, all_keyboards: List[str] = None) -> List[KeyboardKeymapDesc]: |
| 151 | """Expand a keyboard input and keymap input into a list of (keyboard, keymap) tuples. | 179 | """Expand a keyboard input and keymap input into a list of KeyboardKeymapDesc. |
| 152 | 180 | ||
| 153 | Caters for 'all' in either keyboard or keymap, or both. | 181 | Caters for 'all' in either keyboard or keymap, or both. |
| 154 | """ | 182 | """ |
| 155 | if all_keyboards is None: | 183 | if all_keyboards is None: |
| 156 | all_keyboards = list_keyboards() | 184 | all_keyboards = list_keyboards() |
| 157 | 185 | ||
| 158 | if keyboard == 'all': | 186 | if target.keyboard == 'all': |
| 159 | if keymap == 'all': | 187 | if target.keymap == 'all': |
| 160 | cli.log.info('Retrieving list of all keyboards and keymaps...') | 188 | cli.log.info('Retrieving list of all keyboards and keymaps...') |
| 161 | targets = [] | 189 | targets = [] |
| 162 | for kb in parallel_map(_all_keymaps, all_keyboards): | 190 | for kb in parallel_map(_all_keymaps, all_keyboards): |
| 163 | targets.extend(kb) | 191 | targets.extend(kb) |
| 192 | for t in targets: | ||
| 193 | t.extra_args = target.extra_args | ||
| 164 | return targets | 194 | return targets |
| 165 | else: | 195 | else: |
| 166 | cli.log.info(f'Retrieving list of keyboards with keymap "{keymap}"...') | 196 | cli.log.info(f'Retrieving list of keyboards with keymap "{target.keymap}"...') |
| 167 | keyboard_filter = functools.partial(_keymap_exists, keymap=keymap) | 197 | keyboard_filter = functools.partial(_keymap_exists, keymap=target.keymap) |
| 168 | return [(kb, keymap) for kb in filter(lambda e: e is not None, parallel_map(keyboard_filter, all_keyboards))] | 198 | return [KeyboardKeymapDesc(kb, target.keymap, extra_args=target.extra_args) for kb in filter(lambda e: e is not None, parallel_map(keyboard_filter, all_keyboards))] |
| 169 | else: | 199 | else: |
| 170 | if keymap == 'all': | 200 | if target.keymap == 'all': |
| 171 | cli.log.info(f'Retrieving list of keymaps for keyboard "{keyboard}"...') | 201 | cli.log.info(f'Retrieving list of keymaps for keyboard "{target.keyboard}"...') |
| 172 | return _all_keymaps(keyboard) | 202 | targets = _all_keymaps(target.keyboard) |
| 203 | for t in targets: | ||
| 204 | t.extra_args = target.extra_args | ||
| 205 | return targets | ||
| 173 | else: | 206 | else: |
| 174 | return [(keyboard, keymap)] | 207 | return [target] |
| 175 | 208 | ||
| 176 | 209 | ||
| 177 | def expand_keymap_targets(targets: List[Tuple[str, str]]) -> List[Tuple[str, str]]: | 210 | def expand_keymap_targets(targets: List[KeyboardKeymapDesc]) -> List[KeyboardKeymapDesc]: |
| 178 | """Expand a list of (keyboard, keymap) tuples inclusive of 'all', into a list of explicit (keyboard, keymap) tuples. | 211 | """Expand a list of KeyboardKeymapDesc inclusive of 'all', into a list of explicit KeyboardKeymapDesc. |
| 179 | """ | 212 | """ |
| 180 | overall_targets = [] | 213 | overall_targets = [] |
| 181 | all_keyboards = list_keyboards() | 214 | all_keyboards = list_keyboards() |
| 182 | for target in targets: | 215 | for target in targets: |
| 183 | overall_targets.extend(_expand_keymap_target(target[0], target[1], all_keyboards)) | 216 | overall_targets.extend(_expand_keymap_target(target, all_keyboards)) |
| 184 | return list(sorted(set(overall_targets))) | 217 | return list(sorted(set(overall_targets))) |
| 185 | 218 | ||
| 186 | 219 | ||
| 187 | def _construct_build_target_kb_km(e): | 220 | def _construct_build_target(e: KeyboardKeymapDesc): |
| 188 | return KeyboardKeymapBuildTarget(keyboard=e[0], keymap=e[1]) | 221 | return e.to_build_target() |
| 189 | |||
| 190 | 222 | ||
| 191 | def _construct_build_target_kb_km_json(e): | ||
| 192 | return KeyboardKeymapBuildTarget(keyboard=e[0], keymap=e[1], json=e[2]) | ||
| 193 | 223 | ||
| 194 | 224 | def _filter_keymap_targets(target_list: List[KeyboardKeymapDesc], filters: List[str] = []) -> List[KeyboardKeymapDesc]: | |
| 195 | def _filter_keymap_targets(target_list: List[Tuple[str, str]], filters: List[str] = []) -> List[BuildTarget]: | 225 | """Filter a list of KeyboardKeymapDesc based on the supplied filters. |
| 196 | """Filter a list of (keyboard, keymap) tuples based on the supplied filters. | ||
| 197 | 226 | ||
| 198 | Optionally includes the values of the queried info.json keys. | 227 | Optionally includes the values of the queried info.json keys. |
| 199 | """ | 228 | """ |
| 200 | if len(filters) == 0: | 229 | if len(filters) == 0: |
| 201 | cli.log.info('Preparing target list...') | 230 | cli.log.info('Preparing target list...') |
| 202 | targets = list(set(parallel_map(_construct_build_target_kb_km, target_list))) | 231 | targets = target_list |
| 203 | else: | 232 | else: |
| 204 | cli.log.info('Parsing data for all matching keyboard/keymap combinations...') | 233 | cli.log.info('Parsing data for all matching keyboard/keymap combinations...') |
| 205 | valid_keymaps = [(e[0], e[1], dotty(e[2])) for e in parallel_map(_load_keymap_info, target_list)] | 234 | valid_targets = parallel_map(_load_keymap_info, target_list) |
| 206 | 235 | ||
| 207 | function_re = re.compile(r'^(?P<function>[a-zA-Z]+)\((?P<key>[a-zA-Z0-9_\.]+)(,\s*(?P<value>[^#]+))?\)$') | 236 | function_re = re.compile(r'^(?P<function>[a-zA-Z]+)\((?P<key>[a-zA-Z0-9_\.]+)(,\s*(?P<value>[^#]+))?\)$') |
| 208 | equals_re = re.compile(r'^(?P<key>[a-zA-Z0-9_\.]+)\s*=\s*(?P<value>[^#]+)$') | 237 | equals_re = re.compile(r'^(?P<key>[a-zA-Z0-9_\.]+)\s*=\s*(?P<value>[^#]+)$') |
| @@ -220,7 +249,7 @@ def _filter_keymap_targets(target_list: List[Tuple[str, str]], filters: List[str | |||
| 220 | if filter_class is None: | 249 | if filter_class is None: |
| 221 | cli.log.warning(f'Unrecognized filter expression: {function_match.group(0)}') | 250 | cli.log.warning(f'Unrecognized filter expression: {function_match.group(0)}') |
| 222 | continue | 251 | continue |
| 223 | valid_keymaps = filter(filter_class.apply, valid_keymaps) | 252 | valid_targets = filter(filter_class.apply, valid_targets) |
| 224 | 253 | ||
| 225 | value_str = f", {{fg_cyan}}{value}{{fg_reset}}" if value is not None else "" | 254 | value_str = f", {{fg_cyan}}{value}{{fg_reset}}" if value is not None else "" |
| 226 | cli.log.info(f'Filtering on condition: {{fg_green}}{func_name}{{fg_reset}}({{fg_cyan}}{key}{{fg_reset}}{value_str})...') | 255 | cli.log.info(f'Filtering on condition: {{fg_green}}{func_name}{{fg_reset}}({{fg_cyan}}{key}{{fg_reset}}{value_str})...') |
| @@ -234,32 +263,42 @@ def _filter_keymap_targets(target_list: List[Tuple[str, str]], filters: List[str | |||
| 234 | expr = fnmatch.translate(v) | 263 | expr = fnmatch.translate(v) |
| 235 | rule = re.compile(f'^{expr}$', re.IGNORECASE) | 264 | rule = re.compile(f'^{expr}$', re.IGNORECASE) |
| 236 | 265 | ||
| 237 | def f(e): | 266 | def f(e: KeyboardKeymapDesc): |
| 238 | lhs = e[2].get(k) | 267 | lhs = e.dotty.get(k) |
| 239 | lhs = str(False if lhs is None else lhs) | 268 | lhs = str(False if lhs is None else lhs) |
| 240 | return rule.search(lhs) is not None | 269 | return rule.search(lhs) is not None |
| 241 | 270 | ||
| 242 | return f | 271 | return f |
| 243 | 272 | ||
| 244 | valid_keymaps = filter(_make_filter(key, value), valid_keymaps) | 273 | valid_targets = filter(_make_filter(key, value), valid_targets) |
| 245 | else: | 274 | else: |
| 246 | cli.log.warning(f'Unrecognized filter expression: {filter_expr}') | 275 | cli.log.warning(f'Unrecognized filter expression: {filter_expr}') |
| 247 | continue | 276 | continue |
| 248 | 277 | ||
| 249 | cli.log.info('Preparing target list...') | 278 | cli.log.info('Preparing target list...') |
| 250 | 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 | 279 | targets = list(sorted(set(valid_targets))) |
| 251 | targets = list(set(parallel_map(_construct_build_target_kb_km_json, list(valid_keymaps)))) | ||
| 252 | 280 | ||
| 253 | return targets | 281 | return targets |
| 254 | 282 | ||
| 255 | 283 | ||
| 256 | def search_keymap_targets(targets: List[Tuple[str, str]] = [('all', 'default')], filters: List[str] = []) -> List[BuildTarget]: | 284 | def search_keymap_targets(targets: List[Union[Tuple[str, str], Tuple[str, str, Dict[str, str]]]] = [('all', 'default')], filters: List[str] = []) -> List[BuildTarget]: |
| 257 | """Search for build targets matching the supplied criteria. | 285 | """Search for build targets matching the supplied criteria. |
| 258 | """ | 286 | """ |
| 259 | return _filter_keymap_targets(expand_keymap_targets(targets), filters) | 287 | def _make_desc(e): |
| 288 | if len(e) == 3: | ||
| 289 | return KeyboardKeymapDesc(keyboard=e[0], keymap=e[1], extra_args=e[2]) | ||
| 290 | else: | ||
| 291 | return KeyboardKeymapDesc(keyboard=e[0], keymap=e[1]) | ||
| 292 | |||
| 293 | targets = map(_make_desc, targets) | ||
| 294 | targets = _filter_keymap_targets(expand_keymap_targets(targets), filters) | ||
| 295 | targets = list(set(parallel_map(_construct_build_target, list(targets)))) | ||
| 296 | return sorted(targets) | ||
| 260 | 297 | ||
| 261 | 298 | ||
| 262 | def search_make_targets(targets: List[str], filters: List[str] = []) -> List[BuildTarget]: | 299 | def search_make_targets(targets: List[Union[str, Tuple[str, Dict[str, str]]]], filters: List[str] = []) -> List[BuildTarget]: |
| 263 | """Search for build targets matching the supplied criteria. | 300 | """Search for build targets matching the supplied criteria. |
| 264 | """ | 301 | """ |
| 265 | return _filter_keymap_targets(expand_make_targets(targets), filters) | 302 | targets = _filter_keymap_targets(expand_make_targets(targets), filters) |
| 303 | targets = list(set(parallel_map(_construct_build_target, list(targets)))) | ||
| 304 | return sorted(targets) | ||
diff --git a/lib/python/qmk/userspace.py b/lib/python/qmk/userspace.py index 1e5823b229..1c2a97f9c1 100644 --- a/lib/python/qmk/userspace.py +++ b/lib/python/qmk/userspace.py | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | # Copyright 2023 Nick Brassel (@tzarc) | 1 | # Copyright 2023-2024 Nick Brassel (@tzarc) |
| 2 | # SPDX-License-Identifier: GPL-2.0-or-later | 2 | # SPDX-License-Identifier: GPL-2.0-or-later |
| 3 | from os import environ | 3 | from os import environ |
| 4 | from pathlib import Path | 4 | from pathlib import Path |
| @@ -77,31 +77,43 @@ class UserspaceDefs: | |||
| 77 | raise exception | 77 | raise exception |
| 78 | 78 | ||
| 79 | # Iterate through each version of the schema, starting with the latest and decreasing to v1 | 79 | # Iterate through each version of the schema, starting with the latest and decreasing to v1 |
| 80 | try: | 80 | schema_versions = [ |
| 81 | validate(json, 'qmk.user_repo.v1') | 81 | ('qmk.user_repo.v1_1', self.__load_v1_1), # |
| 82 | self.__load_v1(json) | 82 | ('qmk.user_repo.v1', self.__load_v1) # |
| 83 | success = True | 83 | ] |
| 84 | except jsonschema.ValidationError as err: | 84 | |
| 85 | exception.add('qmk.user_repo.v1', err) | 85 | for v in schema_versions: |
| 86 | schema = v[0] | ||
| 87 | loader = v[1] | ||
| 88 | try: | ||
| 89 | validate(json, schema) | ||
| 90 | loader(json) | ||
| 91 | success = True | ||
| 92 | break | ||
| 93 | except jsonschema.ValidationError as err: | ||
| 94 | exception.add(schema, err) | ||
| 86 | 95 | ||
| 87 | if not success: | 96 | if not success: |
| 88 | raise exception | 97 | raise exception |
| 89 | 98 | ||
| 90 | def save(self): | 99 | def save(self): |
| 91 | target_json = { | 100 | target_json = { |
| 92 | "userspace_version": "1.0", # Needs to match latest version | 101 | "userspace_version": "1.1", # Needs to match latest version |
| 93 | "build_targets": [] | 102 | "build_targets": [] |
| 94 | } | 103 | } |
| 95 | 104 | ||
| 96 | for e in self.build_targets: | 105 | for e in self.build_targets: |
| 97 | if isinstance(e, dict): | 106 | if isinstance(e, dict): |
| 98 | target_json['build_targets'].append([e['keyboard'], e['keymap']]) | 107 | entry = [e['keyboard'], e['keymap']] |
| 108 | if 'env' in e: | ||
| 109 | entry.append(e['env']) | ||
| 110 | target_json['build_targets'].append(entry) | ||
| 99 | elif isinstance(e, Path): | 111 | elif isinstance(e, Path): |
| 100 | target_json['build_targets'].append(str(e.relative_to(self.path.parent))) | 112 | target_json['build_targets'].append(str(e.relative_to(self.path.parent))) |
| 101 | 113 | ||
| 102 | try: | 114 | try: |
| 103 | # Ensure what we're writing validates against the latest version of the schema | 115 | # Ensure what we're writing validates against the latest version of the schema |
| 104 | validate(target_json, 'qmk.user_repo.v1') | 116 | validate(target_json, 'qmk.user_repo.v1_1') |
| 105 | except jsonschema.ValidationError as err: | 117 | except jsonschema.ValidationError as err: |
| 106 | cli.log.error(f'Could not save userspace file: {err}') | 118 | cli.log.error(f'Could not save userspace file: {err}') |
| 107 | return False | 119 | return False |
| @@ -114,7 +126,7 @@ class UserspaceDefs: | |||
| 114 | cli.log.info(f'Saved userspace file to {self.path}.') | 126 | cli.log.info(f'Saved userspace file to {self.path}.') |
| 115 | return True | 127 | return True |
| 116 | 128 | ||
| 117 | def add_target(self, keyboard=None, keymap=None, json_path=None, do_print=True): | 129 | def add_target(self, keyboard=None, keymap=None, build_env=None, json_path=None, do_print=True): |
| 118 | if json_path is not None: | 130 | if json_path is not None: |
| 119 | # Assume we're adding a json filename/path | 131 | # Assume we're adding a json filename/path |
| 120 | json_path = Path(json_path) | 132 | json_path = Path(json_path) |
| @@ -128,6 +140,8 @@ class UserspaceDefs: | |||
| 128 | elif keyboard is not None and keymap is not None: | 140 | elif keyboard is not None and keymap is not None: |
| 129 | # Both keyboard/keymap specified | 141 | # Both keyboard/keymap specified |
| 130 | e = {"keyboard": keyboard, "keymap": keymap} | 142 | e = {"keyboard": keyboard, "keymap": keymap} |
| 143 | if build_env is not None: | ||
| 144 | e['env'] = build_env | ||
| 131 | if e not in self.build_targets: | 145 | if e not in self.build_targets: |
| 132 | self.build_targets.append(e) | 146 | self.build_targets.append(e) |
| 133 | if do_print: | 147 | if do_print: |
| @@ -136,7 +150,7 @@ class UserspaceDefs: | |||
| 136 | if do_print: | 150 | if do_print: |
| 137 | cli.log.info(f'{keyboard}:{keymap} is already a userspace build target.') | 151 | cli.log.info(f'{keyboard}:{keymap} is already a userspace build target.') |
| 138 | 152 | ||
| 139 | def remove_target(self, keyboard=None, keymap=None, json_path=None, do_print=True): | 153 | def remove_target(self, keyboard=None, keymap=None, build_env=None, json_path=None, do_print=True): |
| 140 | if json_path is not None: | 154 | if json_path is not None: |
| 141 | # Assume we're removing a json filename/path | 155 | # Assume we're removing a json filename/path |
| 142 | json_path = Path(json_path) | 156 | json_path = Path(json_path) |
| @@ -150,6 +164,8 @@ class UserspaceDefs: | |||
| 150 | elif keyboard is not None and keymap is not None: | 164 | elif keyboard is not None and keymap is not None: |
| 151 | # Both keyboard/keymap specified | 165 | # Both keyboard/keymap specified |
| 152 | e = {"keyboard": keyboard, "keymap": keymap} | 166 | e = {"keyboard": keyboard, "keymap": keymap} |
| 167 | if build_env is not None: | ||
| 168 | e['env'] = build_env | ||
| 153 | if e in self.build_targets: | 169 | if e in self.build_targets: |
| 154 | self.build_targets.remove(e) | 170 | self.build_targets.remove(e) |
| 155 | if do_print: | 171 | if do_print: |
| @@ -160,12 +176,26 @@ class UserspaceDefs: | |||
| 160 | 176 | ||
| 161 | def __load_v1(self, json): | 177 | def __load_v1(self, json): |
| 162 | for e in json['build_targets']: | 178 | for e in json['build_targets']: |
| 163 | if isinstance(e, list) and len(e) == 2: | 179 | self.__load_v1_target(e) |
| 164 | self.add_target(keyboard=e[0], keymap=e[1], do_print=False) | 180 | |
| 165 | if isinstance(e, str): | 181 | def __load_v1_1(self, json): |
| 166 | p = self.path.parent / e | 182 | for e in json['build_targets']: |
| 167 | if p.exists() and p.suffix == '.json': | 183 | self.__load_v1_1_target(e) |
| 168 | self.add_target(json_path=p, do_print=False) | 184 | |
| 185 | def __load_v1_target(self, e): | ||
| 186 | if isinstance(e, list) and len(e) == 2: | ||
| 187 | self.add_target(keyboard=e[0], keymap=e[1], do_print=False) | ||
| 188 | if isinstance(e, str): | ||
| 189 | p = self.path.parent / e | ||
| 190 | if p.exists() and p.suffix == '.json': | ||
| 191 | self.add_target(json_path=p, do_print=False) | ||
| 192 | |||
| 193 | def __load_v1_1_target(self, e): | ||
| 194 | # v1.1 adds support for a third item in the build target tuple; kvp's for environment | ||
| 195 | if isinstance(e, list) and len(e) == 3: | ||
| 196 | self.add_target(keyboard=e[0], keymap=e[1], build_env=e[2], do_print=False) | ||
| 197 | else: | ||
| 198 | self.__load_v1_target(e) | ||
| 169 | 199 | ||
| 170 | 200 | ||
| 171 | class UserspaceValidationError(Exception): | 201 | class UserspaceValidationError(Exception): |
