build_targets.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. # Copyright 2023 Nick Brassel (@tzarc)
  2. # SPDX-License-Identifier: GPL-2.0-or-later
  3. import json
  4. import shutil
  5. from typing import List, Union
  6. from pathlib import Path
  7. from dotty_dict import dotty, Dotty
  8. from milc import cli
  9. from qmk.constants import QMK_FIRMWARE, INTERMEDIATE_OUTPUT_PREFIX
  10. from qmk.commands import find_make, get_make_parallel_args, parse_configurator_json
  11. from qmk.keyboard import keyboard_folder
  12. from qmk.info import keymap_json
  13. class BuildTarget:
  14. def __init__(self, keyboard: str, keymap: str, json: Union[dict, Dotty] = None):
  15. self._keyboard = keyboard_folder(keyboard)
  16. self._keyboard_safe = self._keyboard.replace('/', '_')
  17. self._keymap = keymap
  18. self._parallel = 1
  19. self._clean = False
  20. self._compiledb = False
  21. self._target = f'{self._keyboard_safe}_{self.keymap}'
  22. self._intermediate_output = Path(f'{INTERMEDIATE_OUTPUT_PREFIX}{self._target}')
  23. self._generated_files_path = self._intermediate_output / 'src'
  24. self._json = json.to_dict() if isinstance(json, Dotty) else json
  25. def __str__(self):
  26. return f'{self.keyboard}:{self.keymap}'
  27. def __repr__(self):
  28. return f'BuildTarget(keyboard={self.keyboard}, keymap={self.keymap})'
  29. def __eq__(self, __value: object) -> bool:
  30. if not isinstance(__value, BuildTarget):
  31. return False
  32. return self.__repr__() == __value.__repr__()
  33. def __ne__(self, __value: object) -> bool:
  34. return not self.__eq__(__value)
  35. def __hash__(self) -> int:
  36. return self.__repr__().__hash__()
  37. def configure(self, parallel: int = None, clean: bool = None, compiledb: bool = None) -> None:
  38. if parallel is not None:
  39. self._parallel = parallel
  40. if clean is not None:
  41. self._clean = clean
  42. if compiledb is not None:
  43. self._compiledb = compiledb
  44. @property
  45. def keyboard(self) -> str:
  46. return self._keyboard
  47. @property
  48. def keymap(self) -> str:
  49. return self._keymap
  50. @property
  51. def json(self) -> dict:
  52. if not self._json:
  53. self._load_json()
  54. if not self._json:
  55. return {}
  56. return self._json
  57. @property
  58. def dotty(self) -> Dotty:
  59. return dotty(self.json)
  60. def _common_make_args(self, dry_run: bool = False, build_target: str = None):
  61. compile_args = [
  62. find_make(),
  63. *get_make_parallel_args(self._parallel),
  64. '-r',
  65. '-R',
  66. '-f',
  67. 'builddefs/build_keyboard.mk',
  68. ]
  69. if not cli.config.general.verbose:
  70. compile_args.append('-s')
  71. verbose = 'true' if cli.config.general.verbose else 'false'
  72. color = 'true' if cli.config.general.color else 'false'
  73. if dry_run:
  74. compile_args.append('-n')
  75. if build_target:
  76. compile_args.append(build_target)
  77. compile_args.extend([
  78. f'KEYBOARD={self.keyboard}',
  79. f'KEYMAP={self.keymap}',
  80. f'KEYBOARD_FILESAFE={self._keyboard_safe}',
  81. f'TARGET={self._target}',
  82. f'INTERMEDIATE_OUTPUT={self._intermediate_output}',
  83. f'VERBOSE={verbose}',
  84. f'COLOR={color}',
  85. 'SILENT=false',
  86. 'QMK_BIN="qmk"',
  87. ])
  88. return compile_args
  89. def prepare_build(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None:
  90. raise NotImplementedError("prepare_build() not implemented in base class")
  91. def compile_command(self, build_target: str = None, dry_run: bool = False, **env_vars) -> List[str]:
  92. raise NotImplementedError("compile_command() not implemented in base class")
  93. def generate_compilation_database(self, build_target: str = None, skip_clean: bool = False, **env_vars) -> None:
  94. self.prepare_build(build_target=build_target, **env_vars)
  95. command = self.compile_command(build_target=build_target, dry_run=True, **env_vars)
  96. from qmk.cli.generate.compilation_database import write_compilation_database # Lazy load due to circular references
  97. write_compilation_database(command=command, output_path=QMK_FIRMWARE / 'compile_commands.json', skip_clean=skip_clean, **env_vars)
  98. def compile(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None:
  99. if self._clean or self._compiledb:
  100. command = [find_make(), "clean"]
  101. if dry_run:
  102. command.append('-n')
  103. cli.log.info('Cleaning with {fg_cyan}%s', ' '.join(command))
  104. cli.run(command, capture_output=False)
  105. if self._compiledb and not dry_run:
  106. self.generate_compilation_database(build_target=build_target, skip_clean=True, **env_vars)
  107. self.prepare_build(build_target=build_target, dry_run=dry_run, **env_vars)
  108. command = self.compile_command(build_target=build_target, **env_vars)
  109. cli.log.info('Compiling keymap with {fg_cyan}%s', ' '.join(command))
  110. if not dry_run:
  111. cli.echo('\n')
  112. ret = cli.run(command, capture_output=False)
  113. if ret.returncode:
  114. return ret.returncode
  115. class KeyboardKeymapBuildTarget(BuildTarget):
  116. def __init__(self, keyboard: str, keymap: str, json: dict = None):
  117. super().__init__(keyboard=keyboard, keymap=keymap, json=json)
  118. def __repr__(self):
  119. return f'KeyboardKeymapTarget(keyboard={self.keyboard}, keymap={self.keymap})'
  120. def _load_json(self):
  121. self._json = keymap_json(self.keyboard, self.keymap)
  122. def prepare_build(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None:
  123. pass
  124. def compile_command(self, build_target: str = None, dry_run: bool = False, **env_vars) -> List[str]:
  125. compile_args = self._common_make_args(dry_run=dry_run, build_target=build_target)
  126. for key, value in env_vars.items():
  127. compile_args.append(f'{key}={value}')
  128. return compile_args
  129. class JsonKeymapBuildTarget(BuildTarget):
  130. def __init__(self, json_path):
  131. if isinstance(json_path, Path):
  132. self.json_path = json_path
  133. else:
  134. self.json_path = None
  135. json = parse_configurator_json(json_path) # Will load from stdin if provided
  136. # In case the user passes a keymap.json from a keymap directory directly to the CLI.
  137. # e.g.: qmk compile - < keyboards/clueboard/california/keymaps/default/keymap.json
  138. json["keymap"] = json.get("keymap", "default_json")
  139. super().__init__(keyboard=json['keyboard'], keymap=json['keymap'], json=json)
  140. self._keymap_json = self._generated_files_path / 'keymap.json'
  141. def __repr__(self):
  142. return f'JsonKeymapTarget(keyboard={self.keyboard}, keymap={self.keymap}, path={self.json_path})'
  143. def _load_json(self):
  144. pass # Already loaded in constructor
  145. def prepare_build(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None:
  146. if self._clean:
  147. if self._intermediate_output.exists():
  148. shutil.rmtree(self._intermediate_output)
  149. # begin with making the deepest folder in the tree
  150. self._generated_files_path.mkdir(exist_ok=True, parents=True)
  151. # Compare minified to ensure consistent comparison
  152. new_content = json.dumps(self.json, separators=(',', ':'))
  153. if self._keymap_json.exists():
  154. old_content = json.dumps(json.loads(self._keymap_json.read_text(encoding='utf-8')), separators=(',', ':'))
  155. if old_content == new_content:
  156. new_content = None
  157. # Write the keymap.json file if different so timestamps are only updated
  158. # if the content changes -- running `make` won't treat it as modified.
  159. if new_content:
  160. self._keymap_json.write_text(new_content, encoding='utf-8')
  161. 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)
  163. compile_args.extend([
  164. f'MAIN_KEYMAP_PATH_1={self._intermediate_output}',
  165. f'MAIN_KEYMAP_PATH_2={self._intermediate_output}',
  166. f'MAIN_KEYMAP_PATH_3={self._intermediate_output}',
  167. f'MAIN_KEYMAP_PATH_4={self._intermediate_output}',
  168. f'MAIN_KEYMAP_PATH_5={self._intermediate_output}',
  169. f'KEYMAP_JSON={self._keymap_json}',
  170. f'KEYMAP_PATH={self._generated_files_path}',
  171. ])
  172. for key, value in env_vars.items():
  173. compile_args.append(f'{key}={value}')
  174. return compile_args