c_parse.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. """Functions for working with config.h files.
  2. """
  3. from pygments.lexers.c_cpp import CLexer
  4. from pygments.token import Token
  5. from pygments import lex
  6. from itertools import islice
  7. from pathlib import Path
  8. import re
  9. from milc import cli
  10. from qmk.comment_remover import comment_remover
  11. default_key_entry = {'x': -1, 'y': 0, 'w': 1}
  12. single_comment_regex = re.compile(r'\s+/[/*].*$')
  13. multi_comment_regex = re.compile(r'/\*(.|\n)*?\*/', re.MULTILINE)
  14. layout_macro_define_regex = re.compile(r'^#\s*define')
  15. def _get_chunks(it, size):
  16. """Break down a collection into smaller parts
  17. """
  18. it = iter(it)
  19. return iter(lambda: tuple(islice(it, size)), ())
  20. def _preprocess_c_file(file):
  21. """Load file and strip comments
  22. """
  23. file_contents = file.read_text(encoding='utf-8')
  24. file_contents = comment_remover(file_contents)
  25. return file_contents.replace('\\\n', '')
  26. def strip_line_comment(string):
  27. """Removes comments from a single line string.
  28. """
  29. return single_comment_regex.sub('', string)
  30. def strip_multiline_comment(string):
  31. """Removes comments from a single line string.
  32. """
  33. return multi_comment_regex.sub('', string)
  34. def c_source_files(dir_names):
  35. """Returns a list of all *.c, *.h, and *.cpp files for a given list of directories
  36. Args:
  37. dir_names
  38. List of directories relative to `qmk_firmware`.
  39. """
  40. files = []
  41. for dir in dir_names:
  42. files.extend(file for file in Path(dir).glob('**/*') if file.suffix in ['.c', '.h', '.cpp'])
  43. return files
  44. def find_layouts(file):
  45. """Returns list of parsed LAYOUT preprocessor macros found in the supplied include file.
  46. """
  47. file = Path(file)
  48. aliases = {} # Populated with all `#define`s that aren't functions
  49. parsed_layouts = {}
  50. # Search the file for LAYOUT macros and aliases
  51. file_contents = _preprocess_c_file(file)
  52. for line in file_contents.split('\n'):
  53. if layout_macro_define_regex.match(line.lstrip()) and '(' in line and 'LAYOUT' in line:
  54. # We've found a LAYOUT macro
  55. macro_name, layout, matrix = _parse_layout_macro(line.strip())
  56. # Reject bad macro names
  57. if macro_name.startswith('LAYOUT_kc') or not macro_name.startswith('LAYOUT'):
  58. continue
  59. # Parse the matrix data
  60. matrix_locations = _parse_matrix_locations(matrix, file, macro_name)
  61. # Parse the layout entries into a basic structure
  62. default_key_entry['x'] = -1 # Set to -1 so _default_key(key) will increment it to 0
  63. layout = layout.strip()
  64. parsed_layout = [_default_key(key) for key in layout.split(',')]
  65. for i, key in enumerate(parsed_layout):
  66. if 'label' not in key:
  67. cli.log.error('Invalid LAYOUT macro in %s: Empty parameter name in macro %s at pos %s.', file, macro_name, i)
  68. elif key['label'] not in matrix_locations:
  69. cli.log.error('Invalid LAYOUT macro in %s: Key %s in macro %s has no matrix position!', file, key['label'], macro_name)
  70. else:
  71. key['matrix'] = matrix_locations[key['label']]
  72. parsed_layouts[macro_name] = {
  73. 'layout': parsed_layout,
  74. 'filename': str(file),
  75. }
  76. elif '#define' in line:
  77. # Attempt to extract a new layout alias
  78. try:
  79. _, pp_macro_name, pp_macro_text = line.strip().split(' ', 2)
  80. aliases[pp_macro_name] = pp_macro_text
  81. except ValueError:
  82. continue
  83. return parsed_layouts, aliases
  84. def parse_config_h_file(config_h_file, config_h=None):
  85. """Extract defines from a config.h file.
  86. """
  87. if not config_h:
  88. config_h = {}
  89. config_h_file = Path(config_h_file)
  90. if config_h_file.exists():
  91. config_h_text = config_h_file.read_text(encoding='utf-8')
  92. config_h_text = config_h_text.replace('\\\n', '')
  93. config_h_text = strip_multiline_comment(config_h_text)
  94. for linenum, line in enumerate(config_h_text.split('\n')):
  95. line = strip_line_comment(line).strip()
  96. if not line:
  97. continue
  98. line = line.split()
  99. if line[0] == '#define':
  100. if len(line) == 1:
  101. cli.log.error('%s: Incomplete #define! On or around line %s' % (config_h_file, linenum))
  102. elif len(line) == 2:
  103. config_h[line[1]] = True
  104. else:
  105. config_h[line[1]] = ' '.join(line[2:])
  106. elif line[0] == '#undef':
  107. if len(line) == 2:
  108. if line[1] in config_h:
  109. if config_h[line[1]] is True:
  110. del config_h[line[1]]
  111. else:
  112. config_h[line[1]] = False
  113. else:
  114. cli.log.error('%s: Incomplete #undef! On or around line %s' % (config_h_file, linenum))
  115. return config_h
  116. def _default_key(label=None):
  117. """Increment x and return a copy of the default_key_entry.
  118. """
  119. default_key_entry['x'] += 1
  120. new_key = default_key_entry.copy()
  121. if label:
  122. new_key['label'] = label
  123. return new_key
  124. def _parse_layout_macro(layout_macro):
  125. """Split the LAYOUT macro into its constituent parts
  126. """
  127. layout_macro = layout_macro.replace('\\', '').replace(' ', '').replace('\t', '').replace('#define', '')
  128. macro_name, layout = layout_macro.split('(', 1)
  129. layout, matrix = layout.split(')', 1)
  130. return macro_name, layout, matrix
  131. def _parse_matrix_locations(matrix, file, macro_name):
  132. """Parse raw matrix data into a dictionary keyed by the LAYOUT identifier.
  133. """
  134. matrix_locations = {}
  135. for row_num, row in enumerate(matrix.split('},{')):
  136. if row.startswith('LAYOUT'):
  137. cli.log.error('%s: %s: Nested layout macro detected. Matrix data not available!', file, macro_name)
  138. break
  139. row = row.replace('{', '').replace('}', '')
  140. for col_num, identifier in enumerate(row.split(',')):
  141. if identifier != 'KC_NO':
  142. matrix_locations[identifier] = [row_num, col_num]
  143. return matrix_locations
  144. def _coerce_led_token(_type, value):
  145. """ Convert token to valid info.json content
  146. """
  147. value_map = {
  148. 'NO_LED': None,
  149. 'LED_FLAG_ALL': 0xFF,
  150. 'LED_FLAG_NONE': 0x00,
  151. 'LED_FLAG_MODIFIER': 0x01,
  152. 'LED_FLAG_UNDERGLOW': 0x02,
  153. 'LED_FLAG_KEYLIGHT': 0x04,
  154. 'LED_FLAG_INDICATOR': 0x08,
  155. }
  156. if _type is Token.Literal.Number.Integer:
  157. return int(value)
  158. if _type is Token.Literal.Number.Float:
  159. return float(value)
  160. if _type is Token.Literal.Number.Hex:
  161. return int(value, 0)
  162. if _type is Token.Name and value in value_map.keys():
  163. return value_map[value]
  164. def _validate_led_config(matrix, matrix_rows, matrix_cols, matrix_indexes, position, position_raw, flags):
  165. # TODO: Improve crude parsing/validation
  166. if len(matrix) != matrix_rows and len(matrix) != (matrix_rows / 2):
  167. raise ValueError("Unable to parse g_led_config matrix data")
  168. for index, row in enumerate(matrix):
  169. if len(row) != matrix_cols:
  170. raise ValueError(f"Number of columns in row {index} ({len(row)}) does not match matrix ({matrix_cols})")
  171. if len(position) != len(flags):
  172. raise ValueError(f"Number of g_led_config physical positions ({len(position)}) does not match number of flags ({len(flags)})")
  173. if len(matrix_indexes) and (max(matrix_indexes) >= len(flags)):
  174. raise ValueError(f"LED index {max(matrix_indexes)} is OOB in g_led_config - should be < {len(flags)}")
  175. if not all(isinstance(n, int) for n in matrix_indexes):
  176. raise ValueError("matrix indexes are not all ints")
  177. if (len(position_raw) % 2) != 0:
  178. raise ValueError("Malformed g_led_config position data")
  179. def _parse_led_config(file, matrix_cols, matrix_rows):
  180. """Return any 'raw' led/rgb matrix config
  181. """
  182. matrix = []
  183. position_raw = []
  184. flags = []
  185. found_led_config = False
  186. bracket_count = 0
  187. section = 0
  188. current_row_index = 0
  189. current_row = []
  190. for _type, value in lex(_preprocess_c_file(file), CLexer()):
  191. # Assume g_led_config..stuff..;
  192. if value == 'g_led_config':
  193. found_led_config = True
  194. elif value == ';':
  195. found_led_config = False
  196. elif found_led_config:
  197. # Assume bracket count hints to section of config we are within
  198. if value == '{':
  199. bracket_count += 1
  200. if bracket_count == 2:
  201. section += 1
  202. elif value == '}':
  203. if section == 1 and bracket_count == 3:
  204. matrix.append(current_row)
  205. current_row = []
  206. current_row_index += 1
  207. bracket_count -= 1
  208. else:
  209. # Assume any non whitespace value here is important enough to stash
  210. if _type in [Token.Literal.Number.Integer, Token.Literal.Number.Float, Token.Literal.Number.Hex, Token.Name]:
  211. if section == 1 and bracket_count == 3:
  212. current_row.append(_coerce_led_token(_type, value))
  213. if section == 2 and bracket_count == 3:
  214. position_raw.append(_coerce_led_token(_type, value))
  215. if section == 3 and bracket_count == 2:
  216. flags.append(_coerce_led_token(_type, value))
  217. elif _type in [Token.Comment.Preproc]:
  218. # TODO: Promote to error
  219. return None
  220. # Slightly better intrim format
  221. position = list(_get_chunks(position_raw, 2))
  222. matrix_indexes = list(filter(lambda x: x is not None, sum(matrix, [])))
  223. # If we have not found anything - bail with no error
  224. if not section:
  225. return None
  226. # Throw any validation errors
  227. _validate_led_config(matrix, matrix_rows, matrix_cols, matrix_indexes, position, position_raw, flags)
  228. return (matrix, position, flags)
  229. def find_led_config(file, matrix_cols, matrix_rows):
  230. """Search file for led/rgb matrix config
  231. """
  232. found = _parse_led_config(file, matrix_cols, matrix_rows)
  233. if not found:
  234. return None
  235. # Expand collected content
  236. (matrix, position, flags) = found
  237. # Align to output format
  238. led_config = []
  239. for index, item in enumerate(position, start=0):
  240. led_config.append({
  241. 'x': item[0],
  242. 'y': item[1],
  243. 'flags': flags[index],
  244. })
  245. for r in range(len(matrix)):
  246. for c in range(len(matrix[r])):
  247. index = matrix[r][c]
  248. if index is not None:
  249. led_config[index]['matrix'] = [r, c]
  250. return led_config