commands.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. """Helper functions for commands.
  2. """
  3. import os
  4. import sys
  5. import shutil
  6. from pathlib import Path
  7. from subprocess import DEVNULL
  8. from milc import cli
  9. import jsonschema
  10. import qmk.keymap
  11. from qmk.constants import QMK_FIRMWARE, 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. return [make_cmd, *(['-n'] if dry_run else []), *get_make_parallel_args(parallel), *env, target]
  39. def create_make_command(keyboard, keymap, target=None, dry_run=False, parallel=1, **env_vars):
  40. """Create a make compile command
  41. Args:
  42. keyboard
  43. The path of the keyboard, for example 'plank'
  44. keymap
  45. The name of the keymap, for example 'algernon'
  46. target
  47. Usually a bootloader.
  48. dry_run
  49. make -n -- don't actually build
  50. parallel
  51. The number of make jobs to run in parallel
  52. **env_vars
  53. Environment variables to be passed to make.
  54. Returns:
  55. A command that can be run to make the specified keyboard and keymap
  56. """
  57. make_args = [keyboard, keymap]
  58. if target:
  59. make_args.append(target)
  60. return create_make_target(':'.join(make_args), dry_run=dry_run, parallel=parallel, **env_vars)
  61. def get_git_version(current_time, repo_dir='.', check_dir='.'):
  62. """Returns the current git version for a repo, or the current time.
  63. """
  64. git_describe_cmd = ['git', 'describe', '--abbrev=6', '--dirty', '--always', '--tags']
  65. if repo_dir != '.':
  66. repo_dir = Path('lib') / repo_dir
  67. if check_dir != '.':
  68. check_dir = repo_dir / check_dir
  69. if Path(check_dir).exists():
  70. git_describe = cli.run(git_describe_cmd, stdin=DEVNULL, cwd=repo_dir)
  71. if git_describe.returncode == 0:
  72. return git_describe.stdout.strip()
  73. else:
  74. cli.log.warn(f'"{" ".join(git_describe_cmd)}" returned error code {git_describe.returncode}')
  75. print(git_describe.stderr)
  76. return current_time
  77. return current_time
  78. def get_make_parallel_args(parallel=1):
  79. """Returns the arguments for running the specified number of parallel jobs.
  80. """
  81. parallel_args = []
  82. if int(parallel) <= 0:
  83. # 0 or -1 means -j without argument (unlimited jobs)
  84. parallel_args.append('--jobs')
  85. else:
  86. parallel_args.append('--jobs=' + str(parallel))
  87. if int(parallel) != 1:
  88. # If more than 1 job is used, synchronize parallel output by target
  89. parallel_args.append('--output-sync=target')
  90. return parallel_args
  91. def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_vars):
  92. """Convert a configurator export JSON file into a C file and then compile it.
  93. Args:
  94. user_keymap
  95. A deserialized keymap export
  96. bootloader
  97. A bootloader to flash
  98. parallel
  99. The number of make jobs to run in parallel
  100. Returns:
  101. A command to run to compile and flash the C file.
  102. """
  103. # In case the user passes a keymap.json from a keymap directory directly to the CLI.
  104. # e.g.: qmk compile - < keyboards/clueboard/california/keymaps/default/keymap.json
  105. user_keymap["keymap"] = user_keymap.get("keymap", "default_json")
  106. # Write the keymap.c file
  107. keyboard_filesafe = user_keymap['keyboard'].replace('/', '_')
  108. target = f'{keyboard_filesafe}_{user_keymap["keymap"]}'
  109. keyboard_output = Path(f'{KEYBOARD_OUTPUT_PREFIX}{keyboard_filesafe}')
  110. keymap_output = Path(f'{keyboard_output}_{user_keymap["keymap"]}')
  111. c_text = qmk.keymap.generate_c(user_keymap)
  112. keymap_dir = keymap_output / 'src'
  113. keymap_c = keymap_dir / 'keymap.c'
  114. keymap_dir.mkdir(exist_ok=True, parents=True)
  115. keymap_c.write_text(c_text)
  116. # Return a command that can be run to make the keymap and flash if given
  117. verbose = 'true' if cli.config.general.verbose else 'false'
  118. color = 'true' if cli.config.general.color else 'false'
  119. make_command = [_find_make()]
  120. if not cli.config.general.verbose:
  121. make_command.append('-s')
  122. make_command.extend([
  123. *get_make_parallel_args(parallel),
  124. '-r',
  125. '-R',
  126. '-f',
  127. 'builddefs/build_keyboard.mk',
  128. ])
  129. if bootloader:
  130. make_command.append(bootloader)
  131. for key, value in env_vars.items():
  132. make_command.append(f'{key}={value}')
  133. make_command.extend([
  134. f'KEYBOARD={user_keymap["keyboard"]}',
  135. f'KEYMAP={user_keymap["keymap"]}',
  136. f'KEYBOARD_FILESAFE={keyboard_filesafe}',
  137. f'TARGET={target}',
  138. f'KEYBOARD_OUTPUT={keyboard_output}',
  139. f'KEYMAP_OUTPUT={keymap_output}',
  140. f'MAIN_KEYMAP_PATH_1={keymap_output}',
  141. f'MAIN_KEYMAP_PATH_2={keymap_output}',
  142. f'MAIN_KEYMAP_PATH_3={keymap_output}',
  143. f'MAIN_KEYMAP_PATH_4={keymap_output}',
  144. f'MAIN_KEYMAP_PATH_5={keymap_output}',
  145. f'KEYMAP_C={keymap_c}',
  146. f'KEYMAP_PATH={keymap_dir}',
  147. f'VERBOSE={verbose}',
  148. f'COLOR={color}',
  149. 'SILENT=false',
  150. 'QMK_BIN="qmk"',
  151. ])
  152. return make_command
  153. def parse_configurator_json(configurator_file):
  154. """Open and parse a configurator json export
  155. """
  156. user_keymap = json_load(configurator_file)
  157. # Validate against the jsonschema
  158. try:
  159. validate(user_keymap, 'qmk.keymap.v1')
  160. except jsonschema.ValidationError as e:
  161. cli.log.error(f'Invalid JSON keymap: {configurator_file} : {e.message}')
  162. exit(1)
  163. orig_keyboard = user_keymap['keyboard']
  164. aliases = json_load(Path('data/mappings/keyboard_aliases.json'))
  165. if orig_keyboard in aliases:
  166. if 'target' in aliases[orig_keyboard]:
  167. user_keymap['keyboard'] = aliases[orig_keyboard]['target']
  168. if 'layouts' in aliases[orig_keyboard] and user_keymap['layout'] in aliases[orig_keyboard]['layouts']:
  169. user_keymap['layout'] = aliases[orig_keyboard]['layouts'][user_keymap['layout']]
  170. return user_keymap
  171. def git_get_username():
  172. """Retrieves user's username from Git config, if set.
  173. """
  174. git_username = cli.run(['git', 'config', '--get', 'user.name'])
  175. if git_username.returncode == 0 and git_username.stdout:
  176. return git_username.stdout.strip()
  177. def git_check_repo():
  178. """Checks that the .git directory exists inside QMK_HOME.
  179. This is a decent enough indicator that the qmk_firmware directory is a
  180. proper Git repository, rather than a .zip download from GitHub.
  181. """
  182. dot_git_dir = QMK_FIRMWARE / '.git'
  183. return dot_git_dir.is_dir()
  184. def git_get_branch():
  185. """Returns the current branch for a repo, or None.
  186. """
  187. git_branch = cli.run(['git', 'branch', '--show-current'])
  188. if not git_branch.returncode != 0 or not git_branch.stdout:
  189. # Workaround for Git pre-2.22
  190. git_branch = cli.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'])
  191. if git_branch.returncode == 0:
  192. return git_branch.stdout.strip()
  193. def git_get_tag():
  194. """Returns the current tag for a repo, or None.
  195. """
  196. git_tag = cli.run(['git', 'describe', '--abbrev=0', '--tags'])
  197. if git_tag.returncode == 0:
  198. return git_tag.stdout.strip()
  199. def git_is_dirty():
  200. """Returns 1 if repo is dirty, or 0 if clean
  201. """
  202. git_diff_staged_cmd = ['git', 'diff', '--quiet']
  203. git_diff_unstaged_cmd = [*git_diff_staged_cmd, '--cached']
  204. unstaged = cli.run(git_diff_staged_cmd)
  205. staged = cli.run(git_diff_unstaged_cmd)
  206. return unstaged.returncode != 0 or staged.returncode != 0
  207. def git_get_remotes():
  208. """Returns the current remotes for a repo.
  209. """
  210. remotes = {}
  211. git_remote_show_cmd = ['git', 'remote', 'show']
  212. git_remote_get_cmd = ['git', 'remote', 'get-url']
  213. git_remote_show = cli.run(git_remote_show_cmd)
  214. if git_remote_show.returncode == 0:
  215. for name in git_remote_show.stdout.splitlines():
  216. git_remote_name = cli.run([*git_remote_get_cmd, name])
  217. remotes[name.strip()] = {"url": git_remote_name.stdout.strip()}
  218. return remotes
  219. def git_check_deviation(active_branch):
  220. """Return True if branch has custom commits
  221. """
  222. cli.run(['git', 'fetch', 'upstream', active_branch])
  223. deviations = cli.run(['git', '--no-pager', 'log', f'upstream/{active_branch}...{active_branch}'])
  224. return bool(deviations.returncode)
  225. def in_virtualenv():
  226. """Check if running inside a virtualenv.
  227. Based on https://stackoverflow.com/a/1883251
  228. """
  229. active_prefix = getattr(sys, "base_prefix", None) or getattr(sys, "real_prefix", None) or sys.prefix
  230. return active_prefix != sys.prefix
  231. def dump_lines(output_file, lines, quiet=True):
  232. """Handle dumping to stdout or file
  233. Creates parent folders if required
  234. """
  235. generated = '\n'.join(lines) + '\n'
  236. if output_file and output_file.name != '-':
  237. output_file.parent.mkdir(parents=True, exist_ok=True)
  238. if output_file.exists():
  239. output_file.replace(output_file.parent / (output_file.name + '.bak'))
  240. output_file.write_text(generated, encoding='utf-8')
  241. if not quiet:
  242. cli.log.info(f'Wrote {output_file.name} to {output_file}.')
  243. else:
  244. print(generated)