commands.py 7.8 KB

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