metadata.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. """Functions that help us generate and use info.json files.
  2. """
  3. from functools import lru_cache
  4. from glob import glob
  5. import os
  6. from pathlib import Path
  7. import jsonschema
  8. from dotty_dict import dotty
  9. from milc import cli
  10. from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS
  11. from qmk.c_parse import find_layouts
  12. from qmk.json_schema import deep_update, json_load, validate
  13. from qmk.keyboard import config_h, rules_mk
  14. from qmk.makefile import parse_rules_mk_file
  15. from qmk.math import compute
  16. true_values = ['1', 'on', 'yes', 'true']
  17. false_values = ['0', 'off', 'no', 'false']
  18. @lru_cache(maxsize=None)
  19. def basic_info_json(keyboard):
  20. """Generate a subset of info.json for a specific keyboard.
  21. This does minimal validation, and should only be used as needed to avoid loops or when performance is critical.
  22. """
  23. cur_dir = Path('keyboards')
  24. rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk')
  25. if 'DEFAULT_FOLDER' in rules:
  26. keyboard = rules['DEFAULT_FOLDER']
  27. rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk', rules)
  28. info_data = {
  29. 'keyboard_name': str(keyboard),
  30. 'keyboard_folder': str(keyboard),
  31. 'keymaps': {},
  32. 'layouts': {},
  33. 'parse_errors': [],
  34. 'parse_warnings': [],
  35. 'maintainer': 'qmk',
  36. }
  37. # Populate layout data
  38. layouts, aliases = _find_all_layouts(info_data, keyboard)
  39. if aliases:
  40. info_data['layout_aliases'] = aliases
  41. for layout_name, layout_json in layouts.items():
  42. if not layout_name.startswith('LAYOUT_kc'):
  43. layout_json['c_macro'] = True
  44. info_data['layouts'][layout_name] = layout_json
  45. # Merge in the data from info.json, config.h, and rules.mk
  46. info_data = merge_info_jsons(keyboard, info_data)
  47. info_data = _extract_config_h(info_data)
  48. info_data = _extract_rules_mk(info_data)
  49. return info_data
  50. def _extract_features(info_data, rules):
  51. """Find all the features enabled in rules.mk.
  52. """
  53. # Special handling for bootmagic which also supports a "lite" mode.
  54. if rules.get('BOOTMAGIC_ENABLE') == 'lite':
  55. rules['BOOTMAGIC_LITE_ENABLE'] = 'on'
  56. del rules['BOOTMAGIC_ENABLE']
  57. if rules.get('BOOTMAGIC_ENABLE') == 'full':
  58. rules['BOOTMAGIC_ENABLE'] = 'on'
  59. # Skip non-boolean features we haven't implemented special handling for
  60. for feature in 'HAPTIC_ENABLE', 'QWIIC_ENABLE':
  61. if rules.get(feature):
  62. del rules[feature]
  63. # Process the rest of the rules as booleans
  64. for key, value in rules.items():
  65. if key.endswith('_ENABLE'):
  66. key = '_'.join(key.split('_')[:-1]).lower()
  67. value = True if value.lower() in true_values else False if value.lower() in false_values else value
  68. if 'config_h_features' not in info_data:
  69. info_data['config_h_features'] = {}
  70. if 'features' not in info_data:
  71. info_data['features'] = {}
  72. if key in info_data['features']:
  73. info_log_warning(info_data, 'Feature %s is specified in both info.json and rules.mk, the rules.mk value wins.' % (key,))
  74. info_data['features'][key] = value
  75. info_data['config_h_features'][key] = value
  76. return info_data
  77. @lru_cache(maxsize=None)
  78. def _pin_name(pin):
  79. """Returns the proper representation for a pin.
  80. """
  81. pin = pin.strip()
  82. if not pin:
  83. return None
  84. elif pin.isdigit():
  85. return int(pin)
  86. elif pin == 'NO_PIN':
  87. return None
  88. return pin
  89. @lru_cache(maxsize=None)
  90. def _extract_pins(pins):
  91. """Returns a list of pins from a comma separated string of pins.
  92. """
  93. return [_pin_name(pin) for pin in pins.split(',')]
  94. def _extract_direct_matrix(info_data, direct_pins):
  95. """
  96. """
  97. info_data['matrix_pins'] = {}
  98. direct_pin_array = []
  99. while direct_pins[-1] != '}':
  100. direct_pins = direct_pins[:-1]
  101. for row in direct_pins.split('},{'):
  102. if row.startswith('{'):
  103. row = row[1:]
  104. if row.endswith('}'):
  105. row = row[:-1]
  106. direct_pin_array.append([])
  107. for pin in row.split(','):
  108. if pin == 'NO_PIN':
  109. pin = None
  110. direct_pin_array[-1].append(pin)
  111. return direct_pin_array
  112. def _extract_matrix_info(info_data, config_c):
  113. """Populate the matrix information.
  114. """
  115. row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip()
  116. col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip()
  117. direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1]
  118. if 'MATRIX_ROWS' in config_c and 'MATRIX_COLS' in config_c:
  119. if 'matrix_size' in info_data:
  120. info_log_warning(info_data, 'Matrix size is specified in both info.json and config.h, the config.h values win.')
  121. info_data['matrix_size'] = {
  122. 'cols': compute(config_c.get('MATRIX_COLS', '0')),
  123. 'rows': compute(config_c.get('MATRIX_ROWS', '0')),
  124. }
  125. if row_pins and col_pins:
  126. if 'matrix_pins' in info_data:
  127. info_log_warning(info_data, 'Matrix pins are specified in both info.json and config.h, the config.h values win.')
  128. info_data['matrix_pins'] = {
  129. 'cols': _extract_pins(col_pins),
  130. 'rows': _extract_pins(row_pins),
  131. }
  132. if direct_pins:
  133. if 'matrix_pins' in info_data:
  134. info_log_warning(info_data, 'Direct pins are specified in both info.json and config.h, the config.h values win.')
  135. info_data['matrix_pins']['direct'] = _extract_direct_matrix(info_data, direct_pins)
  136. return info_data
  137. def _extract_config_h(info_data):
  138. """Pull some keyboard information from existing config.h files
  139. """
  140. config_c = config_h(info_data['keyboard_folder'])
  141. # Pull in data from the json map
  142. dotty_info = dotty(info_data)
  143. info_config_map = json_load(Path('data/mappings/info_config.json'))
  144. for config_key, info_dict in info_config_map.items():
  145. info_key = info_dict['info_key']
  146. key_type = info_dict.get('value_type', 'str')
  147. try:
  148. if config_key in config_c and info_dict.get('to_json', True):
  149. if dotty_info.get(info_key) and info_dict.get('warn_duplicate', True):
  150. info_log_warning(info_data, '%s in config.h is overwriting %s in info.json' % (config_key, info_key))
  151. if key_type.startswith('array'):
  152. if '.' in key_type:
  153. key_type, array_type = key_type.split('.', 1)
  154. else:
  155. array_type = None
  156. config_value = config_c[config_key].replace('{', '').replace('}', '').strip()
  157. if array_type == 'int':
  158. dotty_info[info_key] = list(map(int, config_value.split(',')))
  159. else:
  160. dotty_info[info_key] = config_value.split(',')
  161. elif key_type == 'bool':
  162. dotty_info[info_key] = config_c[config_key] in true_values
  163. elif key_type == 'hex':
  164. dotty_info[info_key] = '0x' + config_c[config_key][2:].upper()
  165. elif key_type == 'list':
  166. dotty_info[info_key] = config_c[config_key].split()
  167. elif key_type == 'int':
  168. dotty_info[info_key] = int(config_c[config_key])
  169. else:
  170. dotty_info[info_key] = config_c[config_key]
  171. except Exception as e:
  172. info_log_warning(info_data, f'{config_key}->{info_key}: {e}')
  173. info_data.update(dotty_info)
  174. # Pull data that easily can't be mapped in json
  175. _extract_matrix_info(info_data, config_c)
  176. return info_data
  177. def _extract_rules_mk(info_data):
  178. """Pull some keyboard information from existing rules.mk files
  179. """
  180. rules = rules_mk(info_data['keyboard_folder'])
  181. info_data['processor'] = rules.get('MCU', info_data.get('processor', 'atmega32u4'))
  182. if info_data['processor'] in CHIBIOS_PROCESSORS:
  183. arm_processor_rules(info_data, rules)
  184. elif info_data['processor'] in LUFA_PROCESSORS + VUSB_PROCESSORS:
  185. avr_processor_rules(info_data, rules)
  186. else:
  187. cli.log.warning("%s: Unknown MCU: %s" % (info_data['keyboard_folder'], info_data['processor']))
  188. unknown_processor_rules(info_data, rules)
  189. # Pull in data from the json map
  190. dotty_info = dotty(info_data)
  191. info_rules_map = json_load(Path('data/mappings/info_rules.json'))
  192. for rules_key, info_dict in info_rules_map.items():
  193. info_key = info_dict['info_key']
  194. key_type = info_dict.get('value_type', 'str')
  195. try:
  196. if rules_key in rules and info_dict.get('to_json', True):
  197. if dotty_info.get(info_key) and info_dict.get('warn_duplicate', True):
  198. info_log_warning(info_data, '%s in rules.mk is overwriting %s in info.json' % (rules_key, info_key))
  199. if key_type.startswith('array'):
  200. if '.' in key_type:
  201. key_type, array_type = key_type.split('.', 1)
  202. else:
  203. array_type = None
  204. rules_value = rules[rules_key].replace('{', '').replace('}', '').strip()
  205. if array_type == 'int':
  206. dotty_info[info_key] = list(map(int, rules_value.split(',')))
  207. else:
  208. dotty_info[info_key] = rules_value.split(',')
  209. elif key_type == 'list':
  210. dotty_info[info_key] = rules[rules_key].split()
  211. elif key_type == 'bool':
  212. dotty_info[info_key] = rules[rules_key] in true_values
  213. elif key_type == 'hex':
  214. dotty_info[info_key] = '0x' + rules[rules_key][2:].upper()
  215. elif key_type == 'int':
  216. dotty_info[info_key] = int(rules[rules_key])
  217. else:
  218. dotty_info[info_key] = rules[rules_key]
  219. except Exception as e:
  220. info_log_warning(info_data, f'{rules_key}->{info_key}: {e}')
  221. info_data.update(dotty_info)
  222. # Merge in config values that can't be easily mapped
  223. _extract_features(info_data, rules)
  224. return info_data
  225. @lru_cache(maxsize=None)
  226. def _search_keyboard_h(path):
  227. current_path = Path('keyboards/')
  228. aliases = {}
  229. layouts = {}
  230. for directory in path.parts:
  231. current_path = current_path / directory
  232. keyboard_h = '%s.h' % (directory,)
  233. keyboard_h_path = current_path / keyboard_h
  234. if keyboard_h_path.exists():
  235. new_layouts, new_aliases = find_layouts(keyboard_h_path)
  236. layouts.update(new_layouts)
  237. for alias, alias_text in new_aliases.items():
  238. if alias_text in layouts:
  239. aliases[alias] = alias_text
  240. return layouts, aliases
  241. def _find_all_layouts(info_data, keyboard):
  242. """Looks for layout macros associated with this keyboard.
  243. """
  244. layouts, aliases = _search_keyboard_h(Path(keyboard))
  245. if not layouts:
  246. # If we don't find any layouts from info.json or keyboard.h we widen our search. This is error prone which is why we want to encourage people to follow the standard above.
  247. info_data['parse_warnings'].append('%s: Falling back to searching for KEYMAP/LAYOUT macros.' % (keyboard))
  248. layouts, new_aliases = _deep_search_layouts(keyboard)
  249. aliases.update(new_aliases)
  250. return layouts, aliases
  251. @lru_cache(maxsize=None)
  252. def _deep_search_layouts(keyboard):
  253. """Do a wider (error-prone) search for layout macros.
  254. """
  255. layouts = {}
  256. aliases = {}
  257. for file in glob('keyboards/%s/*.h' % keyboard):
  258. if file.endswith('.h'):
  259. these_layouts, these_aliases = find_layouts(file)
  260. if these_layouts:
  261. layouts.update(these_layouts)
  262. for alias, alias_text in these_aliases.items():
  263. if alias_text in layouts:
  264. aliases[alias] = alias_text
  265. return layouts, aliases
  266. def info_log_error(info_data, message):
  267. """Send an error message to both JSON and the log.
  268. """
  269. info_data['parse_errors'].append(message)
  270. cli.log.error('%s: %s', info_data.get('keyboard_folder', 'Unknown Keyboard!'), message)
  271. def info_log_warning(info_data, message):
  272. """Send a warning message to both JSON and the log.
  273. """
  274. info_data['parse_warnings'].append(message)
  275. cli.log.warning('%s: %s', info_data.get('keyboard_folder', 'Unknown Keyboard!'), message)
  276. def arm_processor_rules(info_data, rules):
  277. """Setup the default info for an ARM board.
  278. """
  279. info_data['processor_type'] = 'arm'
  280. info_data['protocol'] = 'ChibiOS'
  281. if 'bootloader' not in info_data:
  282. if 'STM32' in info_data['processor']:
  283. info_data['bootloader'] = 'stm32-dfu'
  284. else:
  285. info_data['bootloader'] = 'unknown'
  286. if 'STM32' in info_data['processor']:
  287. info_data['platform'] = 'STM32'
  288. elif 'MCU_SERIES' in rules:
  289. info_data['platform'] = rules['MCU_SERIES']
  290. elif 'ARM_ATSAM' in rules:
  291. info_data['platform'] = 'ARM_ATSAM'
  292. return info_data
  293. def avr_processor_rules(info_data, rules):
  294. """Setup the default info for an AVR board.
  295. """
  296. info_data['processor_type'] = 'avr'
  297. info_data['platform'] = rules['ARCH'] if 'ARCH' in rules else 'unknown'
  298. info_data['protocol'] = 'V-USB' if rules.get('MCU') in VUSB_PROCESSORS else 'LUFA'
  299. if 'bootloader' not in info_data:
  300. info_data['bootloader'] = 'atmel-dfu'
  301. # FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk:
  302. # info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA'
  303. return info_data
  304. def unknown_processor_rules(info_data, rules):
  305. """Setup the default keyboard info for unknown boards.
  306. """
  307. info_data['bootloader'] = 'unknown'
  308. info_data['platform'] = 'unknown'
  309. info_data['processor'] = 'unknown'
  310. info_data['processor_type'] = 'unknown'
  311. info_data['protocol'] = 'unknown'
  312. return info_data
  313. def store_mtime(file):
  314. """Stores the mtime for a json file.
  315. """
  316. cli.log.debug('store_mtime(%s)', file)
  317. mtime = str(os.stat(file).st_mtime)
  318. cache_file = f'.build/json_times/{file}'
  319. cache_dir = os.path.dirname(cache_file)
  320. os.makedirs(cache_dir, exist_ok=True)
  321. with open(cache_file, 'w') as fd:
  322. fd.write(mtime)
  323. def has_been_validated(file):
  324. """Returns True if file is in the json cache.
  325. """
  326. cli.log.debug('has_been_validated(%s)', file)
  327. mtime_file = f'.build/json_times/{file}'
  328. if os.path.exists(mtime_file):
  329. with open(mtime_file) as fd:
  330. cache_mtime = fd.read()
  331. cache_mtime = cache_mtime.strip()
  332. file_mtime = str(os.stat(file).st_mtime)
  333. if cache_mtime == file_mtime:
  334. return True
  335. else:
  336. os.remove(mtime_file)
  337. return False
  338. def merge_info_jsons(keyboard, info_data):
  339. """Return a merged copy of all the info.json files for a keyboard.
  340. """
  341. for info_file in find_info_json(keyboard):
  342. # Load and validate the JSON data
  343. new_info_data = json_load(info_file)
  344. if not isinstance(new_info_data, dict):
  345. info_log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),))
  346. continue
  347. if not has_been_validated(info_file):
  348. try:
  349. validate(new_info_data, 'qmk.keyboard.v1')
  350. except jsonschema.ValidationError as e:
  351. json_path = '.'.join([str(p) for p in e.absolute_path])
  352. cli.log.error('Not including data from file: %s', info_file)
  353. cli.log.error('\t%s: %s', json_path, e.message)
  354. continue
  355. store_mtime(info_file)
  356. # Merge layout data in
  357. if 'layout_aliases' in new_info_data:
  358. info_data['layout_aliases'] = {**info_data.get('layout_aliases', {}), **new_info_data['layout_aliases']}
  359. del new_info_data['layout_aliases']
  360. for layout_name, layout in new_info_data.get('layouts', {}).items():
  361. if layout_name in info_data.get('layout_aliases', {}):
  362. info_log_warning(info_data, f"info.json uses alias name {layout_name} instead of {info_data['layout_aliases'][layout_name]}")
  363. layout_name = info_data['layout_aliases'][layout_name]
  364. if layout_name in info_data['layouts']:
  365. for new_key, existing_key in zip(layout['layout'], info_data['layouts'][layout_name]['layout']):
  366. existing_key.update(new_key)
  367. else:
  368. layout['c_macro'] = False
  369. info_data['layouts'][layout_name] = layout
  370. # Update info_data with the new data
  371. if 'layouts' in new_info_data:
  372. del new_info_data['layouts']
  373. deep_update(info_data, new_info_data)
  374. return info_data
  375. @lru_cache(maxsize=None)
  376. def find_info_json(keyboard):
  377. """Finds all the info.json files associated with a keyboard.
  378. """
  379. # Find the most specific first
  380. base_path = Path('keyboards')
  381. keyboard_path = base_path / keyboard
  382. keyboard_parent = keyboard_path.parent
  383. info_jsons = [keyboard_path / 'info.json']
  384. # Add DEFAULT_FOLDER before parents, if present
  385. rules = rules_mk(keyboard)
  386. if 'DEFAULT_FOLDER' in rules:
  387. info_jsons.append(Path(rules['DEFAULT_FOLDER']) / 'info.json')
  388. # Add in parent folders for least specific
  389. for _ in range(5):
  390. info_jsons.append(keyboard_parent / 'info.json')
  391. if keyboard_parent.parent == base_path:
  392. break
  393. keyboard_parent = keyboard_parent.parent
  394. # Return a list of the info.json files that actually exist
  395. return [info_json for info_json in info_jsons if info_json.exists()]