summaryrefslogtreecommitdiff
path: root/lib/python/qmk/build_targets.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/python/qmk/build_targets.py')
-rw-r--r--lib/python/qmk/build_targets.py211
1 files changed, 211 insertions, 0 deletions
diff --git a/lib/python/qmk/build_targets.py b/lib/python/qmk/build_targets.py
new file mode 100644
index 0000000000..236c2eaa03
--- /dev/null
+++ b/lib/python/qmk/build_targets.py
@@ -0,0 +1,211 @@
+# Copyright 2023 Nick Brassel (@tzarc)
+# SPDX-License-Identifier: GPL-2.0-or-later
+import json
+import shutil
+from typing import List, Union
+from pathlib import Path
+from dotty_dict import dotty, Dotty
+from milc import cli
+from qmk.constants import QMK_FIRMWARE, INTERMEDIATE_OUTPUT_PREFIX
+from qmk.commands import find_make, get_make_parallel_args, parse_configurator_json
+from qmk.keyboard import keyboard_folder
+from qmk.info import keymap_json
+from qmk.cli.generate.compilation_database import write_compilation_database
+
+
+class BuildTarget:
+ def __init__(self, keyboard: str, keymap: str, json: Union[dict, Dotty] = None):
+ self._keyboard = keyboard_folder(keyboard)
+ self._keyboard_safe = self._keyboard.replace('/', '_')
+ self._keymap = keymap
+ self._parallel = 1
+ self._clean = False
+ self._compiledb = False
+ self._target = f'{self._keyboard_safe}_{self.keymap}'
+ self._intermediate_output = Path(f'{INTERMEDIATE_OUTPUT_PREFIX}{self._target}')
+ self._generated_files_path = self._intermediate_output / 'src'
+ self._json = json.to_dict() if isinstance(json, Dotty) else json
+
+ def __str__(self):
+ return f'{self.keyboard}:{self.keymap}'
+
+ def __repr__(self):
+ return f'BuildTarget(keyboard={self.keyboard}, keymap={self.keymap})'
+
+ def configure(self, parallel: int = None, clean: bool = None, compiledb: bool = None) -> None:
+ if parallel is not None:
+ self._parallel = parallel
+ if clean is not None:
+ self._clean = clean
+ if compiledb is not None:
+ self._compiledb = compiledb
+
+ @property
+ def keyboard(self) -> str:
+ return self._keyboard
+
+ @property
+ def keymap(self) -> str:
+ return self._keymap
+
+ @property
+ def json(self) -> dict:
+ if not self._json:
+ self._load_json()
+ if not self._json:
+ return {}
+ return self._json
+
+ @property
+ def dotty(self) -> Dotty:
+ return dotty(self.json)
+
+ def _common_make_args(self, dry_run: bool = False, build_target: str = None):
+ compile_args = [
+ find_make(),
+ *get_make_parallel_args(self._parallel),
+ '-r',
+ '-R',
+ '-f',
+ 'builddefs/build_keyboard.mk',
+ ]
+
+ if not cli.config.general.verbose:
+ compile_args.append('-s')
+
+ verbose = 'true' if cli.config.general.verbose else 'false'
+ color = 'true' if cli.config.general.color else 'false'
+
+ if dry_run:
+ compile_args.append('-n')
+
+ if build_target:
+ compile_args.append(build_target)
+
+ compile_args.extend([
+ f'KEYBOARD={self.keyboard}',
+ f'KEYMAP={self.keymap}',
+ f'KEYBOARD_FILESAFE={self._keyboard_safe}',
+ f'TARGET={self._target}',
+ f'INTERMEDIATE_OUTPUT={self._intermediate_output}',
+ f'VERBOSE={verbose}',
+ f'COLOR={color}',
+ 'SILENT=false',
+ 'QMK_BIN="qmk"',
+ ])
+
+ return compile_args
+
+ def prepare_build(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None:
+ raise NotImplementedError("prepare_build() not implemented in base class")
+
+ def compile_command(self, build_target: str = None, dry_run: bool = False, **env_vars) -> List[str]:
+ raise NotImplementedError("compile_command() not implemented in base class")
+
+ def generate_compilation_database(self, build_target: str = None, skip_clean: bool = False, **env_vars) -> None:
+ self.prepare_build(build_target=build_target, **env_vars)
+ command = self.compile_command(build_target=build_target, dry_run=True, **env_vars)
+ write_compilation_database(command=command, output_path=QMK_FIRMWARE / 'compile_commands.json', skip_clean=skip_clean, **env_vars)
+
+ def compile(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None:
+ if self._clean or self._compiledb:
+ command = [find_make(), "clean"]
+ if dry_run:
+ command.append('-n')
+ cli.log.info('Cleaning with {fg_cyan}%s', ' '.join(command))
+ cli.run(command, capture_output=False)
+
+ if self._compiledb and not dry_run:
+ self.generate_compilation_database(build_target=build_target, skip_clean=True, **env_vars)
+
+ self.prepare_build(build_target=build_target, dry_run=dry_run, **env_vars)
+ command = self.compile_command(build_target=build_target, **env_vars)
+ cli.log.info('Compiling keymap with {fg_cyan}%s', ' '.join(command))
+ if not dry_run:
+ cli.echo('\n')
+ ret = cli.run(command, capture_output=False)
+ if ret.returncode:
+ return ret.returncode
+
+
+class KeyboardKeymapBuildTarget(BuildTarget):
+ def __init__(self, keyboard: str, keymap: str, json: dict = None):
+ super().__init__(keyboard=keyboard, keymap=keymap, json=json)
+
+ def __repr__(self):
+ return f'KeyboardKeymapTarget(keyboard={self.keyboard}, keymap={self.keymap})'
+
+ def _load_json(self):
+ self._json = keymap_json(self.keyboard, self.keymap)
+
+ def prepare_build(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None:
+ pass
+
+ def compile_command(self, build_target: str = None, dry_run: bool = False, **env_vars) -> List[str]:
+ compile_args = self._common_make_args(dry_run=dry_run, build_target=build_target)
+
+ for key, value in env_vars.items():
+ compile_args.append(f'{key}={value}')
+
+ return compile_args
+
+
+class JsonKeymapBuildTarget(BuildTarget):
+ def __init__(self, json_path):
+ if isinstance(json_path, Path):
+ self.json_path = json_path
+ else:
+ self.json_path = None
+
+ json = parse_configurator_json(json_path) # Will load from stdin if provided
+
+ # In case the user passes a keymap.json from a keymap directory directly to the CLI.
+ # e.g.: qmk compile - < keyboards/clueboard/california/keymaps/default/keymap.json
+ json["keymap"] = json.get("keymap", "default_json")
+
+ super().__init__(keyboard=json['keyboard'], keymap=json['keymap'], json=json)
+
+ self._keymap_json = self._generated_files_path / 'keymap.json'
+
+ def __repr__(self):
+ return f'JsonKeymapTarget(keyboard={self.keyboard}, keymap={self.keymap}, path={self.json_path})'
+
+ def _load_json(self):
+ pass # Already loaded in constructor
+
+ def prepare_build(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None:
+ if self._clean:
+ if self._intermediate_output.exists():
+ shutil.rmtree(self._intermediate_output)
+
+ # begin with making the deepest folder in the tree
+ self._generated_files_path.mkdir(exist_ok=True, parents=True)
+
+ # Compare minified to ensure consistent comparison
+ new_content = json.dumps(self.json, separators=(',', ':'))
+ if self._keymap_json.exists():
+ old_content = json.dumps(json.loads(self._keymap_json.read_text(encoding='utf-8')), separators=(',', ':'))
+ if old_content == new_content:
+ new_content = None
+
+ # Write the keymap.json file if different so timestamps are only updated
+ # if the content changes -- running `make` won't treat it as modified.
+ if new_content:
+ self._keymap_json.write_text(new_content, encoding='utf-8')
+
+ def compile_command(self, build_target: str = None, dry_run: bool = False, **env_vars) -> List[str]:
+ compile_args = self._common_make_args(dry_run=dry_run, build_target=build_target)
+ compile_args.extend([
+ f'MAIN_KEYMAP_PATH_1={self._intermediate_output}',
+ f'MAIN_KEYMAP_PATH_2={self._intermediate_output}',
+ f'MAIN_KEYMAP_PATH_3={self._intermediate_output}',
+ f'MAIN_KEYMAP_PATH_4={self._intermediate_output}',
+ f'MAIN_KEYMAP_PATH_5={self._intermediate_output}',
+ f'KEYMAP_JSON={self._keymap_json}',
+ f'KEYMAP_PATH={self._generated_files_path}',
+ ])
+
+ for key, value in env_vars.items():
+ compile_args.append(f'{key}={value}')
+
+ return compile_args