c_parse.py 9.5 KB

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