logo

qmk_firmware

custom branch of QMK firmware git clone https://anongit.hacktivis.me/git/qmk_firmware.git

c_parse.py (11554B)


  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}
  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. elif len(matrix_locations.get(key['label'])) > 1:
  71. cli.log.error('Invalid LAYOUT macro in %s: Key %s in macro %s has multiple matrix positions (%s)', file, key['label'], macro_name, ', '.join(str(x) for x in matrix_locations[key['label']]))
  72. else:
  73. key['matrix'] = matrix_locations[key['label']][0]
  74. parsed_layouts[macro_name] = {
  75. 'layout': parsed_layout,
  76. 'filename': str(file),
  77. }
  78. elif '#define' in line:
  79. # Attempt to extract a new layout alias
  80. try:
  81. _, pp_macro_name, pp_macro_text = line.strip().split(' ', 2)
  82. aliases[pp_macro_name] = pp_macro_text
  83. except ValueError:
  84. continue
  85. return parsed_layouts, aliases
  86. def parse_config_h_file(config_h_file, config_h=None):
  87. """Extract defines from a config.h file.
  88. """
  89. if not config_h:
  90. config_h = {}
  91. config_h_file = Path(config_h_file)
  92. if config_h_file.exists():
  93. config_h_text = config_h_file.read_text(encoding='utf-8')
  94. config_h_text = config_h_text.replace('\\\n', '')
  95. config_h_text = strip_multiline_comment(config_h_text)
  96. for linenum, line in enumerate(config_h_text.split('\n')):
  97. line = strip_line_comment(line).strip()
  98. if not line:
  99. continue
  100. line = line.split()
  101. if line[0] == '#define':
  102. if len(line) == 1:
  103. cli.log.error('%s: Incomplete #define! On or around line %s' % (config_h_file, linenum))
  104. elif len(line) == 2:
  105. config_h[line[1]] = True
  106. else:
  107. config_h[line[1]] = ' '.join(line[2:])
  108. elif line[0] == '#undef':
  109. if len(line) == 2:
  110. if line[1] in config_h:
  111. if config_h[line[1]] is True:
  112. del config_h[line[1]]
  113. else:
  114. config_h[line[1]] = False
  115. else:
  116. cli.log.error('%s: Incomplete #undef! On or around line %s' % (config_h_file, linenum))
  117. return config_h
  118. def _default_key(label=None):
  119. """Increment x and return a copy of the default_key_entry.
  120. """
  121. default_key_entry['x'] += 1
  122. new_key = default_key_entry.copy()
  123. if label:
  124. new_key['label'] = label
  125. return new_key
  126. def _parse_layout_macro(layout_macro):
  127. """Split the LAYOUT macro into its constituent parts
  128. """
  129. layout_macro = layout_macro.replace('\\', '').replace(' ', '').replace('\t', '').replace('#define', '')
  130. macro_name, layout = layout_macro.split('(', 1)
  131. layout, matrix = layout.split(')', 1)
  132. return macro_name, layout, matrix
  133. def _parse_matrix_locations(matrix, file, macro_name):
  134. """Parse raw matrix data into a dictionary keyed by the LAYOUT identifier.
  135. """
  136. matrix_locations = {}
  137. for row_num, row in enumerate(matrix.split('},{')):
  138. if row.startswith('LAYOUT'):
  139. cli.log.error('%s: %s: Nested layout macro detected. Matrix data not available!', file, macro_name)
  140. break
  141. row = row.replace('{', '').replace('}', '')
  142. for col_num, identifier in enumerate(row.split(',')):
  143. if identifier != 'KC_NO':
  144. if identifier not in matrix_locations:
  145. matrix_locations[identifier] = []
  146. matrix_locations[identifier].append([row_num, col_num])
  147. return matrix_locations
  148. def _coerce_led_token(_type, value):
  149. """ Convert token to valid info.json content
  150. """
  151. value_map = {
  152. 'NO_LED': None,
  153. 'LED_FLAG_ALL': 0xFF,
  154. 'LED_FLAG_NONE': 0x00,
  155. 'LED_FLAG_MODIFIER': 0x01,
  156. 'LED_FLAG_UNDERGLOW': 0x02,
  157. 'LED_FLAG_KEYLIGHT': 0x04,
  158. 'LED_FLAG_INDICATOR': 0x08,
  159. }
  160. if _type is Token.Literal.Number.Integer:
  161. return int(value)
  162. if _type is Token.Literal.Number.Float:
  163. return float(value)
  164. if _type is Token.Literal.Number.Hex:
  165. return int(value, 0)
  166. if _type is Token.Name and value in value_map.keys():
  167. return value_map[value]
  168. def _validate_led_config(matrix, matrix_rows, matrix_cols, matrix_indexes, position, position_raw, flags):
  169. # TODO: Improve crude parsing/validation
  170. if len(matrix) != matrix_rows and len(matrix) != (matrix_rows / 2):
  171. raise ValueError("Unable to parse g_led_config matrix data")
  172. for index, row in enumerate(matrix):
  173. if len(row) != matrix_cols:
  174. raise ValueError(f"Number of columns in row {index} ({len(row)}) does not match matrix ({matrix_cols})")
  175. if len(position) != len(flags):
  176. raise ValueError(f"Number of g_led_config physical positions ({len(position)}) does not match number of flags ({len(flags)})")
  177. if len(matrix_indexes) and (max(matrix_indexes) >= len(flags)):
  178. raise ValueError(f"LED index {max(matrix_indexes)} is OOB in g_led_config - should be < {len(flags)}")
  179. if not all(isinstance(n, int) for n in matrix_indexes):
  180. raise ValueError("matrix indexes are not all ints")
  181. if (len(position_raw) % 2) != 0:
  182. raise ValueError("Malformed g_led_config position data")
  183. def _parse_led_config(file, matrix_cols, matrix_rows):
  184. """Return any 'raw' led/rgb matrix config
  185. """
  186. matrix = []
  187. position_raw = []
  188. flags = []
  189. found_led_config_t = False
  190. found_g_led_config = False
  191. bracket_count = 0
  192. section = 0
  193. current_row_index = 0
  194. current_row = []
  195. for _type, value in lex(preprocess_c_file(file), CLexer()):
  196. if not found_g_led_config:
  197. # Check for type
  198. if value == 'led_config_t':
  199. found_led_config_t = True
  200. # Type found, now check for name
  201. elif found_led_config_t and value == 'g_led_config':
  202. found_g_led_config = True
  203. elif value == ';':
  204. found_g_led_config = False
  205. else:
  206. # Assume bracket count hints to section of config we are within
  207. if value == '{':
  208. bracket_count += 1
  209. if bracket_count == 2:
  210. section += 1
  211. elif value == '}':
  212. if section == 1 and bracket_count == 3:
  213. matrix.append(current_row)
  214. current_row = []
  215. current_row_index += 1
  216. bracket_count -= 1
  217. else:
  218. # Assume any non whitespace value here is important enough to stash
  219. if _type in [Token.Literal.Number.Integer, Token.Literal.Number.Float, Token.Literal.Number.Hex, Token.Name]:
  220. if section == 1 and bracket_count == 3:
  221. current_row.append(_coerce_led_token(_type, value))
  222. if section == 2 and bracket_count == 3:
  223. position_raw.append(_coerce_led_token(_type, value))
  224. if section == 3 and bracket_count == 2:
  225. flags.append(_coerce_led_token(_type, value))
  226. elif _type in [Token.Comment.Preproc]:
  227. # TODO: Promote to error
  228. return None
  229. # Slightly better intrim format
  230. position = list(_get_chunks(position_raw, 2))
  231. matrix_indexes = list(filter(lambda x: x is not None, sum(matrix, [])))
  232. # If we have not found anything - bail with no error
  233. if not section:
  234. return None
  235. # Throw any validation errors
  236. _validate_led_config(matrix, matrix_rows, matrix_cols, matrix_indexes, position, position_raw, flags)
  237. return (matrix, position, flags)
  238. def find_led_config(file, matrix_cols, matrix_rows):
  239. """Search file for led/rgb matrix config
  240. """
  241. found = _parse_led_config(file, matrix_cols, matrix_rows)
  242. if not found:
  243. return None
  244. # Expand collected content
  245. (matrix, position, flags) = found
  246. # Align to output format
  247. led_config = []
  248. for index, item in enumerate(position, start=0):
  249. led_config.append({
  250. 'x': item[0],
  251. 'y': item[1],
  252. 'flags': flags[index],
  253. })
  254. for r in range(len(matrix)):
  255. for c in range(len(matrix[r])):
  256. index = matrix[r][c]
  257. if index is not None:
  258. led_config[index]['matrix'] = [r, c]
  259. return led_config