qmk_firmware

QMK firmware for my keyboards (Corne, Sweep Ferris) and trackball (Ploopy Adept)
Log | Files | Refs | Submodules | LICENSE

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