commands.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. """Helper functions for commands.
  2. """
  3. import os
  4. import sys
  5. import json
  6. import shutil
  7. from pathlib import Path
  8. from milc import cli
  9. import jsonschema
  10. from qmk.constants import KEYBOARD_OUTPUT_PREFIX
  11. from qmk.json_schema import json_load, validate
  12. def _find_make():
  13. """Returns the correct make command for this environment.
  14. """
  15. make_cmd = os.environ.get('MAKE')
  16. if not make_cmd:
  17. make_cmd = 'gmake' if shutil.which('gmake') else 'make'
  18. return make_cmd
  19. def create_make_target(target, dry_run=False, parallel=1, **env_vars):
  20. """Create a make command
  21. Args:
  22. target
  23. Usually a make rule, such as 'clean' or 'all'.
  24. dry_run
  25. make -n -- don't actually build
  26. parallel
  27. The number of make jobs to run in parallel
  28. **env_vars
  29. Environment variables to be passed to make.
  30. Returns:
  31. A command that can be run to make the specified keyboard and keymap
  32. """
  33. env = []
  34. make_cmd = _find_make()
  35. for key, value in env_vars.items():
  36. env.append(f'{key}={value}')
  37. return [make_cmd, *(['-n'] if dry_run else []), *get_make_parallel_args(parallel), *env, target]
  38. def create_make_command(keyboard, keymap, target=None, dry_run=False, parallel=1, **env_vars):
  39. """Create a make compile command
  40. Args:
  41. keyboard
  42. The path of the keyboard, for example 'plank'
  43. keymap
  44. The name of the keymap, for example 'algernon'
  45. target
  46. Usually a bootloader.
  47. dry_run
  48. make -n -- don't actually build
  49. parallel
  50. The number of make jobs to run in parallel
  51. **env_vars
  52. Environment variables to be passed to make.
  53. Returns:
  54. A command that can be run to make the specified keyboard and keymap
  55. """
  56. make_args = [keyboard, keymap]
  57. if target:
  58. make_args.append(target)
  59. return create_make_target(':'.join(make_args), dry_run=dry_run, parallel=parallel, **env_vars)
  60. def get_make_parallel_args(parallel=1):
  61. """Returns the arguments for running the specified number of parallel jobs.
  62. """
  63. parallel_args = []
  64. if int(parallel) <= 0:
  65. # 0 or -1 means -j without argument (unlimited jobs)
  66. parallel_args.append('--jobs')
  67. else:
  68. parallel_args.append('--jobs=' + str(parallel))
  69. if int(parallel) != 1:
  70. # If more than 1 job is used, synchronize parallel output by target
  71. parallel_args.append('--output-sync=target')
  72. return parallel_args
  73. def compile_configurator_json(user_keymap, bootloader=None, parallel=1, clean=False, **env_vars):
  74. """Convert a configurator export JSON file into a C file and then compile it.
  75. Args:
  76. user_keymap
  77. A deserialized keymap export
  78. bootloader
  79. A bootloader to flash
  80. parallel
  81. The number of make jobs to run in parallel
  82. Returns:
  83. A command to run to compile and flash the C file.
  84. """
  85. # In case the user passes a keymap.json from a keymap directory directly to the CLI.
  86. # e.g.: qmk compile - < keyboards/clueboard/california/keymaps/default/keymap.json
  87. user_keymap["keymap"] = user_keymap.get("keymap", "default_json")
  88. keyboard_filesafe = user_keymap['keyboard'].replace('/', '_')
  89. target = f'{keyboard_filesafe}_{user_keymap["keymap"]}'
  90. keyboard_output = Path(f'{KEYBOARD_OUTPUT_PREFIX}{keyboard_filesafe}')
  91. keymap_output = Path(f'{keyboard_output}_{user_keymap["keymap"]}')
  92. keymap_dir = keymap_output / 'src'
  93. keymap_json = keymap_dir / 'keymap.json'
  94. if clean:
  95. if keyboard_output.exists():
  96. shutil.rmtree(keyboard_output)
  97. if keymap_output.exists():
  98. shutil.rmtree(keymap_output)
  99. # begin with making the deepest folder in the tree
  100. keymap_dir.mkdir(exist_ok=True, parents=True)
  101. # Compare minified to ensure consistent comparison
  102. new_content = json.dumps(user_keymap, separators=(',', ':'))
  103. if keymap_json.exists():
  104. old_content = json.dumps(json.loads(keymap_json.read_text(encoding='utf-8')), separators=(',', ':'))
  105. if old_content == new_content:
  106. new_content = None
  107. # Write the keymap.json file if different
  108. if new_content:
  109. keymap_json.write_text(new_content, encoding='utf-8')
  110. # Return a command that can be run to make the keymap and flash if given
  111. verbose = 'true' if cli.config.general.verbose else 'false'
  112. color = 'true' if cli.config.general.color else 'false'
  113. make_command = [_find_make()]
  114. if not cli.config.general.verbose:
  115. make_command.append('-s')
  116. make_command.extend([
  117. *get_make_parallel_args(parallel),
  118. '-r',
  119. '-R',
  120. '-f',
  121. 'builddefs/build_keyboard.mk',
  122. ])
  123. if bootloader:
  124. make_command.append(bootloader)
  125. for key, value in env_vars.items():
  126. make_command.append(f'{key}={value}')
  127. make_command.extend([
  128. f'KEYBOARD={user_keymap["keyboard"]}',
  129. f'KEYMAP={user_keymap["keymap"]}',
  130. f'KEYBOARD_FILESAFE={keyboard_filesafe}',
  131. f'TARGET={target}',
  132. f'KEYBOARD_OUTPUT={keyboard_output}',
  133. f'KEYMAP_OUTPUT={keymap_output}',
  134. f'MAIN_KEYMAP_PATH_1={keymap_output}',
  135. f'MAIN_KEYMAP_PATH_2={keymap_output}',
  136. f'MAIN_KEYMAP_PATH_3={keymap_output}',
  137. f'MAIN_KEYMAP_PATH_4={keymap_output}',
  138. f'MAIN_KEYMAP_PATH_5={keymap_output}',
  139. f'KEYMAP_JSON={keymap_json}',
  140. f'KEYMAP_PATH={keymap_dir}',
  141. f'VERBOSE={verbose}',
  142. f'COLOR={color}',
  143. 'SILENT=false',
  144. 'QMK_BIN="qmk"',
  145. ])
  146. return make_command
  147. def parse_configurator_json(configurator_file):
  148. """Open and parse a configurator json export
  149. """
  150. user_keymap = json_load(configurator_file)
  151. # Validate against the jsonschema
  152. try:
  153. validate(user_keymap, 'qmk.keymap.v1')
  154. except jsonschema.ValidationError as e:
  155. cli.log.error(f'Invalid JSON keymap: {configurator_file} : {e.message}')
  156. exit(1)
  157. orig_keyboard = user_keymap['keyboard']
  158. aliases = json_load(Path('data/mappings/keyboard_aliases.json'))
  159. if orig_keyboard in aliases:
  160. if 'target' in aliases[orig_keyboard]:
  161. user_keymap['keyboard'] = aliases[orig_keyboard]['target']
  162. if 'layouts' in aliases[orig_keyboard] and user_keymap['layout'] in aliases[orig_keyboard]['layouts']:
  163. user_keymap['layout'] = aliases[orig_keyboard]['layouts'][user_keymap['layout']]
  164. return user_keymap
  165. def build_environment(args):
  166. """Common processing for cli.args.env
  167. """
  168. envs = {}
  169. for env in args:
  170. if '=' in env:
  171. key, value = env.split('=', 1)
  172. envs[key] = value
  173. else:
  174. cli.log.warning('Invalid environment variable: %s', env)
  175. return envs
  176. def in_virtualenv():
  177. """Check if running inside a virtualenv.
  178. Based on https://stackoverflow.com/a/1883251
  179. """
  180. active_prefix = getattr(sys, "base_prefix", None) or getattr(sys, "real_prefix", None) or sys.prefix
  181. return active_prefix != sys.prefix
  182. def dump_lines(output_file, lines, quiet=True):
  183. """Handle dumping to stdout or file
  184. Creates parent folders if required
  185. """
  186. generated = '\n'.join(lines) + '\n'
  187. if output_file and output_file.name != '-':
  188. output_file.parent.mkdir(parents=True, exist_ok=True)
  189. if output_file.exists():
  190. output_file.replace(output_file.parent / (output_file.name + '.bak'))
  191. output_file.write_text(generated, encoding='utf-8')
  192. if not quiet:
  193. cli.log.info(f'Wrote {output_file.name} to {output_file}.')
  194. else:
  195. print(generated)