mass_compile.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. """Compile all keyboards.
  2. This will compile everything in parallel, for testing purposes.
  3. """
  4. import fnmatch
  5. import logging
  6. import multiprocessing
  7. import os
  8. import re
  9. from pathlib import Path
  10. from subprocess import DEVNULL
  11. from dotty_dict import dotty
  12. from milc import cli
  13. from qmk.constants import QMK_FIRMWARE
  14. from qmk.commands import _find_make, get_make_parallel_args
  15. from qmk.info import keymap_json
  16. import qmk.keyboard
  17. import qmk.keymap
  18. def _set_log_level(level):
  19. cli.acquire_lock()
  20. old = cli.log_level
  21. cli.log_level = level
  22. cli.log.setLevel(level)
  23. logging.root.setLevel(level)
  24. cli.release_lock()
  25. return old
  26. def _all_keymaps(keyboard):
  27. old = _set_log_level(logging.CRITICAL)
  28. keymaps = qmk.keymap.list_keymaps(keyboard)
  29. _set_log_level(old)
  30. return (keyboard, keymaps)
  31. def _keymap_exists(keyboard, keymap):
  32. old = _set_log_level(logging.CRITICAL)
  33. ret = keyboard if qmk.keymap.locate_keymap(keyboard, keymap) is not None else None
  34. _set_log_level(old)
  35. return ret
  36. def _load_keymap_info(keyboard, keymap):
  37. old = _set_log_level(logging.CRITICAL)
  38. ret = (keyboard, keymap, keymap_json(keyboard, keymap))
  39. _set_log_level(old)
  40. return ret
  41. @cli.argument('-t', '--no-temp', arg_only=True, action='store_true', help="Remove temporary files during build.")
  42. @cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.")
  43. @cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.")
  44. @cli.argument(
  45. '-f',
  46. '--filter',
  47. arg_only=True,
  48. action='append',
  49. default=[],
  50. help= # noqa: `format-python` and `pytest` don't agree here.
  51. "Filter the list of keyboards based on the supplied value in rules.mk. Matches info.json structure, and accepts the formats 'features.rgblight=true' or 'exists(matrix_pins.direct)'. May be passed multiple times, all filters need to match. Value may include wildcards such as '*' and '?'." # noqa: `format-python` and `pytest` don't agree here.
  52. )
  53. @cli.argument('-km', '--keymap', type=str, default='default', help="The keymap name to build. Default is 'default'.")
  54. @cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.")
  55. @cli.subcommand('Compile QMK Firmware for all keyboards.', hidden=False if cli.config.user.developer else True)
  56. def mass_compile(cli):
  57. """Compile QMK Firmware against all keyboards.
  58. """
  59. make_cmd = _find_make()
  60. if cli.args.clean:
  61. cli.run([make_cmd, 'clean'], capture_output=False, stdin=DEVNULL)
  62. builddir = Path(QMK_FIRMWARE) / '.build'
  63. makefile = builddir / 'parallel_kb_builds.mk'
  64. targets = []
  65. with multiprocessing.Pool() as pool:
  66. cli.log.info(f'Retrieving list of keyboards with keymap "{cli.args.keymap}"...')
  67. target_list = []
  68. if cli.args.keymap == 'all':
  69. kb_to_kms = pool.map(_all_keymaps, qmk.keyboard.list_keyboards())
  70. for targets in kb_to_kms:
  71. keyboard = targets[0]
  72. keymaps = targets[1]
  73. target_list.extend([(keyboard, keymap) for keymap in keymaps])
  74. else:
  75. target_list = [(kb, cli.args.keymap) for kb in filter(lambda kb: kb is not None, pool.starmap(_keymap_exists, [(kb, cli.args.keymap) for kb in qmk.keyboard.list_keyboards()]))]
  76. if len(cli.args.filter) == 0:
  77. targets = target_list
  78. else:
  79. cli.log.info('Parsing data for all matching keyboard/keymap combinations...')
  80. valid_keymaps = [(e[0], e[1], dotty(e[2])) for e in pool.starmap(_load_keymap_info, target_list)]
  81. equals_re = re.compile(r'^(?P<key>[a-zA-Z0-9_\.]+)\s*=\s*(?P<value>[^#]+)$')
  82. exists_re = re.compile(r'^exists\((?P<key>[a-zA-Z0-9_\.]+)\)$')
  83. for filter_txt in cli.args.filter:
  84. f = equals_re.match(filter_txt)
  85. if f is not None:
  86. key = f.group('key')
  87. value = f.group('value')
  88. cli.log.info(f'Filtering on condition ("{key}" == "{value}")...')
  89. def _make_filter(k, v):
  90. expr = fnmatch.translate(v)
  91. rule = re.compile(expr, re.IGNORECASE)
  92. def f(e):
  93. lhs = e[2].get(k)
  94. lhs = str(False if lhs is None else lhs)
  95. return rule.search(lhs) is not None
  96. return f
  97. valid_keymaps = filter(_make_filter(key, value), valid_keymaps)
  98. f = exists_re.match(filter_txt)
  99. if f is not None:
  100. key = f.group('key')
  101. cli.log.info(f'Filtering on condition (exists: "{key}")...')
  102. valid_keymaps = filter(lambda e: e[2].get(key) is not None, valid_keymaps)
  103. targets = [(e[0], e[1]) for e in valid_keymaps]
  104. if len(targets) == 0:
  105. return
  106. builddir.mkdir(parents=True, exist_ok=True)
  107. with open(makefile, "w") as f:
  108. for target in sorted(targets):
  109. keyboard_name = target[0]
  110. keymap_name = target[1]
  111. keyboard_safe = keyboard_name.replace('/', '_')
  112. # yapf: disable
  113. f.write(
  114. f"""\
  115. all: {keyboard_safe}_{keymap_name}_binary
  116. {keyboard_safe}_{keymap_name}_binary:
  117. @rm -f "{QMK_FIRMWARE}/.build/failed.log.{keyboard_safe}.{keymap_name}" || true
  118. @echo "Compiling QMK Firmware for target: '{keyboard_name}:{keymap_name}'..." >>"{QMK_FIRMWARE}/.build/build.log.{os.getpid()}.{keyboard_safe}"
  119. +@$(MAKE) -C "{QMK_FIRMWARE}" -f "{QMK_FIRMWARE}/builddefs/build_keyboard.mk" KEYBOARD="{keyboard_name}" KEYMAP="{keymap_name}" COLOR=true SILENT=false {' '.join(cli.args.env)} \\
  120. >>"{QMK_FIRMWARE}/.build/build.log.{os.getpid()}.{keyboard_safe}.{keymap_name}" 2>&1 \\
  121. || cp "{QMK_FIRMWARE}/.build/build.log.{os.getpid()}.{keyboard_safe}.{keymap_name}" "{QMK_FIRMWARE}/.build/failed.log.{os.getpid()}.{keyboard_safe}.{keymap_name}"
  122. @{{ grep '\[ERRORS\]' "{QMK_FIRMWARE}/.build/build.log.{os.getpid()}.{keyboard_safe}.{keymap_name}" >/dev/null 2>&1 && printf "Build %-64s \e[1;31m[ERRORS]\e[0m\\n" "{keyboard_name}:{keymap_name}" ; }} \\
  123. || {{ grep '\[WARNINGS\]' "{QMK_FIRMWARE}/.build/build.log.{os.getpid()}.{keyboard_safe}.{keymap_name}" >/dev/null 2>&1 && printf "Build %-64s \e[1;33m[WARNINGS]\e[0m\\n" "{keyboard_name}:{keymap_name}" ; }} \\
  124. || printf "Build %-64s \e[1;32m[OK]\e[0m\\n" "{keyboard_name}:{keymap_name}"
  125. @rm -f "{QMK_FIRMWARE}/.build/build.log.{os.getpid()}.{keyboard_safe}.{keymap_name}" || true
  126. """# noqa
  127. )
  128. # yapf: enable
  129. if cli.args.no_temp:
  130. # yapf: disable
  131. f.write(
  132. f"""\
  133. @rm -rf "{QMK_FIRMWARE}/.build/{keyboard_safe}_{keymap_name}.elf" 2>/dev/null || true
  134. @rm -rf "{QMK_FIRMWARE}/.build/{keyboard_safe}_{keymap_name}.map" 2>/dev/null || true
  135. @rm -rf "{QMK_FIRMWARE}/.build/{keyboard_safe}_{keymap_name}.hex" 2>/dev/null || true
  136. @rm -rf "{QMK_FIRMWARE}/.build/{keyboard_safe}_{keymap_name}.bin" 2>/dev/null || true
  137. @rm -rf "{QMK_FIRMWARE}/.build/{keyboard_safe}_{keymap_name}.uf2" 2>/dev/null || true
  138. @rm -rf "{QMK_FIRMWARE}/.build/obj_{keyboard_safe}" || true
  139. @rm -rf "{QMK_FIRMWARE}/.build/obj_{keyboard_safe}_{keymap_name}" || true
  140. """# noqa
  141. )
  142. # yapf: enable
  143. f.write('\n')
  144. cli.run([make_cmd, *get_make_parallel_args(cli.args.parallel), '-f', makefile.as_posix(), 'all'], capture_output=False, stdin=DEVNULL)
  145. # Check for failures
  146. failures = [f for f in builddir.glob(f'failed.log.{os.getpid()}.*')]
  147. if len(failures) > 0:
  148. return False