c_parse.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. """Functions for working with config.h files.
  2. """
  3. from pathlib import Path
  4. import re
  5. from milc import cli
  6. from qmk.comment_remover import comment_remover
  7. default_key_entry = {'x': -1, 'y': 0, 'w': 1}
  8. single_comment_regex = re.compile(r'\s+/[/*].*$')
  9. multi_comment_regex = re.compile(r'/\*(.|\n)*?\*/', re.MULTILINE)
  10. layout_macro_define_regex = re.compile(r'^#\s*define')
  11. def strip_line_comment(string):
  12. """Removes comments from a single line string.
  13. """
  14. return single_comment_regex.sub('', string)
  15. def strip_multiline_comment(string):
  16. """Removes comments from a single line string.
  17. """
  18. return multi_comment_regex.sub('', string)
  19. def c_source_files(dir_names):
  20. """Returns a list of all *.c, *.h, and *.cpp files for a given list of directories
  21. Args:
  22. dir_names
  23. List of directories relative to `qmk_firmware`.
  24. """
  25. files = []
  26. for dir in dir_names:
  27. files.extend(file for file in Path(dir).glob('**/*') if file.suffix in ['.c', '.h', '.cpp'])
  28. return files
  29. def find_layouts(file):
  30. """Returns list of parsed LAYOUT preprocessor macros found in the supplied include file.
  31. """
  32. file = Path(file)
  33. aliases = {} # Populated with all `#define`s that aren't functions
  34. parsed_layouts = {}
  35. # Search the file for LAYOUT macros and aliases
  36. file_contents = file.read_text(encoding='utf-8')
  37. file_contents = comment_remover(file_contents)
  38. file_contents = file_contents.replace('\\\n', '')
  39. for line in file_contents.split('\n'):
  40. if layout_macro_define_regex.match(line.lstrip()) and '(' in line and 'LAYOUT' in line:
  41. # We've found a LAYOUT macro
  42. macro_name, layout, matrix = _parse_layout_macro(line.strip())
  43. # Reject bad macro names
  44. if macro_name.startswith('LAYOUT_kc') or not macro_name.startswith('LAYOUT'):
  45. continue
  46. # Parse the matrix data
  47. matrix_locations = _parse_matrix_locations(matrix, file, macro_name)
  48. # Parse the layout entries into a basic structure
  49. default_key_entry['x'] = -1 # Set to -1 so _default_key(key) will increment it to 0
  50. layout = layout.strip()
  51. parsed_layout = [_default_key(key) for key in layout.split(',')]
  52. for i, key in enumerate(parsed_layout):
  53. if 'label' not in key:
  54. cli.log.error('Invalid LAYOUT macro in %s: Empty parameter name in macro %s at pos %s.', file, macro_name, i)
  55. elif key['label'] in matrix_locations:
  56. key['matrix'] = matrix_locations[key['label']]
  57. parsed_layouts[macro_name] = {
  58. 'layout': parsed_layout,
  59. 'filename': str(file),
  60. }
  61. elif '#define' in line:
  62. # Attempt to extract a new layout alias
  63. try:
  64. _, pp_macro_name, pp_macro_text = line.strip().split(' ', 2)
  65. aliases[pp_macro_name] = pp_macro_text
  66. except ValueError:
  67. continue
  68. return parsed_layouts, aliases
  69. def parse_config_h_file(config_h_file, config_h=None):
  70. """Extract defines from a config.h file.
  71. """
  72. if not config_h:
  73. config_h = {}
  74. config_h_file = Path(config_h_file)
  75. if config_h_file.exists():
  76. config_h_text = config_h_file.read_text(encoding='utf-8')
  77. config_h_text = config_h_text.replace('\\\n', '')
  78. config_h_text = strip_multiline_comment(config_h_text)
  79. for linenum, line in enumerate(config_h_text.split('\n')):
  80. line = strip_line_comment(line).strip()
  81. if not line:
  82. continue
  83. line = line.split()
  84. if line[0] == '#define':
  85. if len(line) == 1:
  86. cli.log.error('%s: Incomplete #define! On or around line %s' % (config_h_file, linenum))
  87. elif len(line) == 2:
  88. config_h[line[1]] = True
  89. else:
  90. config_h[line[1]] = ' '.join(line[2:])
  91. elif line[0] == '#undef':
  92. if len(line) == 2:
  93. if line[1] in config_h:
  94. if config_h[line[1]] is True:
  95. del config_h[line[1]]
  96. else:
  97. config_h[line[1]] = False
  98. else:
  99. cli.log.error('%s: Incomplete #undef! On or around line %s' % (config_h_file, linenum))
  100. return config_h
  101. def _default_key(label=None):
  102. """Increment x and return a copy of the default_key_entry.
  103. """
  104. default_key_entry['x'] += 1
  105. new_key = default_key_entry.copy()
  106. if label:
  107. new_key['label'] = label
  108. return new_key
  109. def _parse_layout_macro(layout_macro):
  110. """Split the LAYOUT macro into its constituent parts
  111. """
  112. layout_macro = layout_macro.replace('\\', '').replace(' ', '').replace('\t', '').replace('#define', '')
  113. macro_name, layout = layout_macro.split('(', 1)
  114. layout, matrix = layout.split(')', 1)
  115. return macro_name, layout, matrix
  116. def _parse_matrix_locations(matrix, file, macro_name):
  117. """Parse raw matrix data into a dictionary keyed by the LAYOUT identifier.
  118. """
  119. matrix_locations = {}
  120. for row_num, row in enumerate(matrix.split('},{')):
  121. if row.startswith('LAYOUT'):
  122. cli.log.error('%s: %s: Nested layout macro detected. Matrix data not available!', file, macro_name)
  123. break
  124. row = row.replace('{', '').replace('}', '')
  125. for col_num, identifier in enumerate(row.split(',')):
  126. if identifier != 'KC_NO':
  127. matrix_locations[identifier] = [row_num, col_num]
  128. return matrix_locations