commands.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  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 time import strftime
  9. from itertools import islice
  10. from milc import cli
  11. import jsonschema
  12. import qmk.keymap
  13. from qmk.constants import QMK_FIRMWARE, KEYBOARD_OUTPUT_PREFIX
  14. from qmk.json_schema import json_load, validate
  15. time_fmt = '%Y-%m-%d-%H:%M:%S'
  16. def _find_make():
  17. """Returns the correct make command for this environment.
  18. """
  19. make_cmd = os.environ.get('MAKE')
  20. if not make_cmd:
  21. make_cmd = 'gmake' if shutil.which('gmake') else 'make'
  22. return make_cmd
  23. def create_make_target(target, dry_run=False, parallel=1, **env_vars):
  24. """Create a make command
  25. Args:
  26. target
  27. Usually a make rule, such as 'clean' or 'all'.
  28. dry_run
  29. make -n -- don't actually build
  30. parallel
  31. The number of make jobs to run in parallel
  32. **env_vars
  33. Environment variables to be passed to make.
  34. Returns:
  35. A command that can be run to make the specified keyboard and keymap
  36. """
  37. env = []
  38. make_cmd = _find_make()
  39. for key, value in env_vars.items():
  40. env.append(f'{key}={value}')
  41. return [make_cmd, *(['-n'] if dry_run else []), *get_make_parallel_args(parallel), *env, target]
  42. def create_make_command(keyboard, keymap, target=None, dry_run=False, parallel=1, **env_vars):
  43. """Create a make compile command
  44. Args:
  45. keyboard
  46. The path of the keyboard, for example 'plank'
  47. keymap
  48. The name of the keymap, for example 'algernon'
  49. target
  50. Usually a bootloader.
  51. dry_run
  52. make -n -- don't actually build
  53. parallel
  54. The number of make jobs to run in parallel
  55. **env_vars
  56. Environment variables to be passed to make.
  57. Returns:
  58. A command that can be run to make the specified keyboard and keymap
  59. """
  60. make_args = [keyboard, keymap]
  61. if target:
  62. make_args.append(target)
  63. return create_make_target(':'.join(make_args), dry_run=dry_run, parallel=parallel, **env_vars)
  64. def get_git_version(current_time=None, repo_dir='.', check_dir='.'):
  65. """Returns the current git version for a repo, or the current time.
  66. """
  67. git_describe_cmd = ['git', 'describe', '--abbrev=6', '--dirty', '--always', '--tags']
  68. if current_time is None:
  69. current_time = strftime(time_fmt)
  70. if repo_dir != '.':
  71. repo_dir = Path('lib') / repo_dir
  72. if check_dir != '.':
  73. check_dir = repo_dir / check_dir
  74. if Path(check_dir).exists():
  75. git_describe = cli.run(git_describe_cmd, stdin=DEVNULL, cwd=repo_dir)
  76. if git_describe.returncode == 0:
  77. return git_describe.stdout.strip()
  78. else:
  79. cli.log.warn(f'"{" ".join(git_describe_cmd)}" returned error code {git_describe.returncode}')
  80. print(git_describe.stderr)
  81. return current_time
  82. return current_time
  83. def get_make_parallel_args(parallel=1):
  84. """Returns the arguments for running the specified number of parallel jobs.
  85. """
  86. parallel_args = []
  87. if int(parallel) <= 0:
  88. # 0 or -1 means -j without argument (unlimited jobs)
  89. parallel_args.append('--jobs')
  90. else:
  91. parallel_args.append('--jobs=' + str(parallel))
  92. if int(parallel) != 1:
  93. # If more than 1 job is used, synchronize parallel output by target
  94. parallel_args.append('--output-sync=target')
  95. return parallel_args
  96. def create_version_h(skip_git=False, skip_all=False):
  97. """Generate version.h contents
  98. """
  99. if skip_all:
  100. current_time = "1970-01-01-00:00:00"
  101. else:
  102. current_time = None
  103. if skip_git:
  104. git_version = "NA"
  105. chibios_version = "NA"
  106. chibios_contrib_version = "NA"
  107. else:
  108. git_version = get_git_version(current_time)
  109. chibios_version = get_git_version(current_time, "chibios", "os")
  110. chibios_contrib_version = get_git_version(current_time, "chibios-contrib", "os")
  111. version_h_lines = f"""/* This file was automatically generated. Do not edit or copy.
  112. */
  113. #pragma once
  114. #define QMK_VERSION "{git_version}"
  115. #define QMK_BUILDDATE "{current_time}"
  116. #define CHIBIOS_VERSION "{chibios_version}"
  117. #define CHIBIOS_CONTRIB_VERSION "{chibios_contrib_version}"
  118. """
  119. return version_h_lines
  120. def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_vars):
  121. """Convert a configurator export JSON file into a C file and then compile it.
  122. Args:
  123. user_keymap
  124. A deserialized keymap export
  125. bootloader
  126. A bootloader to flash
  127. parallel
  128. The number of make jobs to run in parallel
  129. Returns:
  130. A command to run to compile and flash the C file.
  131. """
  132. # In case the user passes a keymap.json from a keymap directory directly to the CLI.
  133. # e.g.: qmk compile - < keyboards/clueboard/california/keymaps/default/keymap.json
  134. user_keymap["keymap"] = user_keymap.get("keymap", "default_json")
  135. # Write the keymap.c file
  136. keyboard_filesafe = user_keymap['keyboard'].replace('/', '_')
  137. target = f'{keyboard_filesafe}_{user_keymap["keymap"]}'
  138. keyboard_output = Path(f'{KEYBOARD_OUTPUT_PREFIX}{keyboard_filesafe}')
  139. keymap_output = Path(f'{keyboard_output}_{user_keymap["keymap"]}')
  140. c_text = qmk.keymap.generate_c(user_keymap)
  141. keymap_dir = keymap_output / 'src'
  142. keymap_c = keymap_dir / 'keymap.c'
  143. keymap_dir.mkdir(exist_ok=True, parents=True)
  144. keymap_c.write_text(c_text)
  145. version_h = Path('quantum/version.h')
  146. version_h.write_text(create_version_h())
  147. # Return a command that can be run to make the keymap and flash if given
  148. verbose = 'true' if cli.config.general.verbose else 'false'
  149. color = 'true' if cli.config.general.color else 'false'
  150. make_command = [_find_make()]
  151. if not cli.config.general.verbose:
  152. make_command.append('-s')
  153. make_command.extend([
  154. *get_make_parallel_args(parallel),
  155. '-r',
  156. '-R',
  157. '-f',
  158. 'builddefs/build_keyboard.mk',
  159. ])
  160. if bootloader:
  161. make_command.append(bootloader)
  162. for key, value in env_vars.items():
  163. make_command.append(f'{key}={value}')
  164. make_command.extend([
  165. f'KEYBOARD={user_keymap["keyboard"]}',
  166. f'KEYMAP={user_keymap["keymap"]}',
  167. f'KEYBOARD_FILESAFE={keyboard_filesafe}',
  168. f'TARGET={target}',
  169. f'KEYBOARD_OUTPUT={keyboard_output}',
  170. f'KEYMAP_OUTPUT={keymap_output}',
  171. f'MAIN_KEYMAP_PATH_1={keymap_output}',
  172. f'MAIN_KEYMAP_PATH_2={keymap_output}',
  173. f'MAIN_KEYMAP_PATH_3={keymap_output}',
  174. f'MAIN_KEYMAP_PATH_4={keymap_output}',
  175. f'MAIN_KEYMAP_PATH_5={keymap_output}',
  176. f'KEYMAP_C={keymap_c}',
  177. f'KEYMAP_PATH={keymap_dir}',
  178. f'VERBOSE={verbose}',
  179. f'COLOR={color}',
  180. 'SILENT=false',
  181. 'QMK_BIN="qmk"',
  182. ])
  183. return make_command
  184. def parse_configurator_json(configurator_file):
  185. """Open and parse a configurator json export
  186. """
  187. user_keymap = json_load(configurator_file)
  188. # Validate against the jsonschema
  189. try:
  190. validate(user_keymap, 'qmk.keymap.v1')
  191. except jsonschema.ValidationError as e:
  192. cli.log.error(f'Invalid JSON keymap: {configurator_file} : {e.message}')
  193. exit(1)
  194. orig_keyboard = user_keymap['keyboard']
  195. aliases = json_load(Path('data/mappings/keyboard_aliases.json'))
  196. if orig_keyboard in aliases:
  197. if 'target' in aliases[orig_keyboard]:
  198. user_keymap['keyboard'] = aliases[orig_keyboard]['target']
  199. if 'layouts' in aliases[orig_keyboard] and user_keymap['layout'] in aliases[orig_keyboard]['layouts']:
  200. user_keymap['layout'] = aliases[orig_keyboard]['layouts'][user_keymap['layout']]
  201. return user_keymap
  202. def git_get_username():
  203. """Retrieves user's username from Git config, if set.
  204. """
  205. git_username = cli.run(['git', 'config', '--get', 'user.name'])
  206. if git_username.returncode == 0 and git_username.stdout:
  207. return git_username.stdout.strip()
  208. def git_check_repo():
  209. """Checks that the .git directory exists inside QMK_HOME.
  210. This is a decent enough indicator that the qmk_firmware directory is a
  211. proper Git repository, rather than a .zip download from GitHub.
  212. """
  213. dot_git_dir = QMK_FIRMWARE / '.git'
  214. return dot_git_dir.is_dir()
  215. def git_get_branch():
  216. """Returns the current branch for a repo, or None.
  217. """
  218. git_branch = cli.run(['git', 'branch', '--show-current'])
  219. if not git_branch.returncode != 0 or not git_branch.stdout:
  220. # Workaround for Git pre-2.22
  221. git_branch = cli.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'])
  222. if git_branch.returncode == 0:
  223. return git_branch.stdout.strip()
  224. def git_get_tag():
  225. """Returns the current tag for a repo, or None.
  226. """
  227. git_tag = cli.run(['git', 'describe', '--abbrev=0', '--tags'])
  228. if git_tag.returncode == 0:
  229. return git_tag.stdout.strip()
  230. def git_is_dirty():
  231. """Returns 1 if repo is dirty, or 0 if clean
  232. """
  233. git_diff_staged_cmd = ['git', 'diff', '--quiet']
  234. git_diff_unstaged_cmd = [*git_diff_staged_cmd, '--cached']
  235. unstaged = cli.run(git_diff_staged_cmd)
  236. staged = cli.run(git_diff_unstaged_cmd)
  237. return unstaged.returncode != 0 or staged.returncode != 0
  238. def git_get_remotes():
  239. """Returns the current remotes for a repo.
  240. """
  241. remotes = {}
  242. git_remote_show_cmd = ['git', 'remote', 'show']
  243. git_remote_get_cmd = ['git', 'remote', 'get-url']
  244. git_remote_show = cli.run(git_remote_show_cmd)
  245. if git_remote_show.returncode == 0:
  246. for name in git_remote_show.stdout.splitlines():
  247. git_remote_name = cli.run([*git_remote_get_cmd, name])
  248. remotes[name.strip()] = {"url": git_remote_name.stdout.strip()}
  249. return remotes
  250. def git_check_deviation(active_branch):
  251. """Return True if branch has custom commits
  252. """
  253. cli.run(['git', 'fetch', 'upstream', active_branch])
  254. deviations = cli.run(['git', '--no-pager', 'log', f'upstream/{active_branch}...{active_branch}'])
  255. return bool(deviations.returncode)
  256. def in_virtualenv():
  257. """Check if running inside a virtualenv.
  258. Based on https://stackoverflow.com/a/1883251
  259. """
  260. active_prefix = getattr(sys, "base_prefix", None) or getattr(sys, "real_prefix", None) or sys.prefix
  261. return active_prefix != sys.prefix
  262. def get_chunks(it, size):
  263. """Break down a collection into smaller parts
  264. """
  265. it = iter(it)
  266. return iter(lambda: tuple(islice(it, size)), ())
  267. def dump_lines(output_file, lines):
  268. """Handle dumping to stdout or file
  269. Creates parent folders if required
  270. """
  271. generated = '\n'.join(lines)
  272. if output_file:
  273. if output_file.name == '-':
  274. print(generated)
  275. else:
  276. output_file.parent.mkdir(parents=True, exist_ok=True)
  277. if output_file.exists():
  278. output_file.replace(output_file.parent / (output_file.name + '.bak'))
  279. output_file.write_text(generated)