Austin Schuh | 208337d | 2022-01-01 14:29:11 -0800 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | # |
| 3 | # Copyright (c) 2021 Raspberry Pi (Trading) Ltd. |
| 4 | # |
| 5 | # SPDX-License-Identifier: BSD-3-Clause |
| 6 | # |
| 7 | # |
| 8 | # Script to scan the Raspberry Pi Pico SDK tree searching for configuration items |
| 9 | # Outputs a tab separated file of the configuration item: |
| 10 | # name location description type advanced default depends enumvalues group max min |
| 11 | # |
| 12 | # Usage: |
| 13 | # |
| 14 | # ./extract_configs.py <root of source tree> [output file] |
| 15 | # |
| 16 | # If not specified, output file will be `pico_configs.tsv` |
| 17 | |
| 18 | |
| 19 | import os |
| 20 | import sys |
| 21 | import re |
| 22 | import csv |
| 23 | import logging |
| 24 | |
| 25 | logger = logging.getLogger(__name__) |
| 26 | logging.basicConfig(level=logging.INFO) |
| 27 | |
| 28 | scandir = sys.argv[1] |
| 29 | outfile = sys.argv[2] if len(sys.argv) > 2 else 'pico_configs.tsv' |
| 30 | |
| 31 | CONFIG_RE = re.compile(r'//\s+PICO_CONFIG:\s+(\w+),\s+([^,]+)(?:,\s+(.*))?$') |
| 32 | DEFINE_RE = re.compile(r'#define\s+(\w+)\s+(.+?)(\s*///.*)?$') |
| 33 | |
| 34 | all_configs = {} |
| 35 | all_attrs = set() |
| 36 | all_descriptions = {} |
| 37 | all_defines = {} |
| 38 | |
| 39 | |
| 40 | |
Ravago Jones | d208ae7 | 2023-02-13 02:24:07 -0800 | [diff] [blame] | 41 | def ValidateAttrs(config_attrs, file_path, linenum): |
Austin Schuh | 208337d | 2022-01-01 14:29:11 -0800 | [diff] [blame] | 42 | _type = config_attrs.get('type', 'int') |
| 43 | |
| 44 | # Validate attrs |
| 45 | if _type == 'int': |
| 46 | assert 'enumvalues' not in config_attrs |
| 47 | _min = _max = _default = None |
| 48 | if config_attrs.get('min', None) is not None: |
| 49 | value = config_attrs['min'] |
| 50 | m = re.match(r'^(\d+)e(\d+)$', value.lower()) |
| 51 | if m: |
| 52 | _min = int(m.group(1)) * 10**int(m.group(2)) |
| 53 | else: |
| 54 | _min = int(value, 0) |
| 55 | if config_attrs.get('max', None) is not None: |
| 56 | value = config_attrs['max'] |
| 57 | m = re.match(r'^(\d+)e(\d+)$', value.lower()) |
| 58 | if m: |
| 59 | _max = int(m.group(1)) * 10**int(m.group(2)) |
| 60 | else: |
| 61 | _max = int(value, 0) |
| 62 | if config_attrs.get('default', None) is not None: |
| 63 | if '/' not in config_attrs['default']: |
| 64 | try: |
| 65 | value = config_attrs['default'] |
| 66 | m = re.match(r'^(\d+)e(\d+)$', value.lower()) |
| 67 | if m: |
| 68 | _default = int(m.group(1)) * 10**int(m.group(2)) |
| 69 | else: |
| 70 | _default = int(value, 0) |
| 71 | except ValueError: |
| 72 | pass |
| 73 | if _min is not None and _max is not None: |
| 74 | if _min > _max: |
| 75 | raise Exception('{} at {}:{} has min {} > max {}'.format(config_name, file_path, linenum, config_attrs['min'], config_attrs['max'])) |
| 76 | if _min is not None and _default is not None: |
| 77 | if _min > _default: |
| 78 | raise Exception('{} at {}:{} has min {} > default {}'.format(config_name, file_path, linenum, config_attrs['min'], config_attrs['default'])) |
| 79 | if _default is not None and _max is not None: |
| 80 | if _default > _max: |
| 81 | raise Exception('{} at {}:{} has default {} > max {}'.format(config_name, file_path, linenum, config_attrs['default'], config_attrs['max'])) |
| 82 | elif _type == 'bool': |
| 83 | |
| 84 | assert 'min' not in config_attrs |
| 85 | assert 'max' not in config_attrs |
| 86 | assert 'enumvalues' not in config_attrs |
| 87 | |
| 88 | _default = config_attrs.get('default', None) |
| 89 | if _default is not None: |
| 90 | if '/' not in _default: |
| 91 | if (_default.lower() != '0') and (config_attrs['default'].lower() != '1') and ( _default not in all_configs): |
| 92 | logger.info('{} at {}:{} has non-integer default value "{}"'.format(config_name, file_path, linenum, config_attrs['default'])) |
| 93 | |
| 94 | elif _type == 'enum': |
| 95 | |
| 96 | assert 'min' not in config_attrs |
| 97 | assert 'max' not in config_attrs |
| 98 | assert 'enumvalues' in config_attrs |
| 99 | |
| 100 | _enumvalues = tuple(config_attrs['enumvalues'].split('|')) |
| 101 | _default = None |
| 102 | if config_attrs.get('default', None) is not None: |
| 103 | _default = config_attrs['default'] |
| 104 | if _default is not None: |
| 105 | if _default not in _enumvalues: |
| 106 | raise Exception('{} at {}:{} has default value {} which isn\'t in list of enumvalues {}'.format(config_name, file_path, linenum, config_attrs['default'], config_attrs['enumvalues'])) |
| 107 | else: |
| 108 | raise Exception("Found unknown PICO_CONFIG type {} at {}:{}".format(_type, file_path, linenum)) |
| 109 | |
| 110 | |
| 111 | |
| 112 | |
| 113 | # Scan all .c and .h files in the specific path, recursively. |
| 114 | |
| 115 | for dirpath, dirnames, filenames in os.walk(scandir): |
| 116 | for filename in filenames: |
| 117 | file_ext = os.path.splitext(filename)[1] |
| 118 | if file_ext in ('.c', '.h'): |
| 119 | file_path = os.path.join(dirpath, filename) |
| 120 | |
| 121 | with open(file_path, encoding="ISO-8859-1") as fh: |
| 122 | linenum = 0 |
| 123 | for line in fh.readlines(): |
| 124 | linenum += 1 |
| 125 | line = line.strip() |
| 126 | m = CONFIG_RE.match(line) |
| 127 | if m: |
| 128 | config_name = m.group(1) |
| 129 | config_description = m.group(2) |
| 130 | _attrs = m.group(3) |
| 131 | # allow commas to appear inside brackets by converting them to and from NULL chars |
| 132 | _attrs = re.sub(r'(\(.+\))', lambda m: m.group(1).replace(',', '\0'), _attrs) |
| 133 | |
| 134 | if '=' in config_description: |
| 135 | raise Exception("For {} at {}:{} the description was set to '{}' - has the description field been omitted?".format(config_name, file_path, linenum, config_description)) |
| 136 | if config_description in all_descriptions: |
| 137 | raise Exception("Found description {} at {}:{} but it was already used at {}:{}".format(config_description, file_path, linenum, os.path.join(scandir, all_descriptions[config_description]['filename']), all_descriptions[config_description]['line_number'])) |
| 138 | else: |
| 139 | all_descriptions[config_description] = {'config_name': config_name, 'filename': os.path.relpath(file_path, scandir), 'line_number': linenum} |
| 140 | |
| 141 | config_attrs = {} |
| 142 | prev = None |
| 143 | # Handle case where attr value contains a comma |
| 144 | for item in _attrs.split(','): |
| 145 | if "=" not in item: |
| 146 | assert(prev) |
| 147 | item = prev + "," + item |
| 148 | try: |
| 149 | k, v = (i.strip() for i in item.split('=')) |
| 150 | except ValueError: |
| 151 | raise Exception('{} at {}:{} has malformed value {}'.format(config_name, file_path, linenum, item)) |
| 152 | config_attrs[k] = v.replace('\0', ',') |
| 153 | all_attrs.add(k) |
| 154 | prev = item |
| 155 | #print(file_path, config_name, config_attrs) |
| 156 | |
| 157 | if 'group' not in config_attrs: |
| 158 | raise Exception('{} at {}:{} has no group attribute'.format(config_name, file_path, linenum)) |
| 159 | |
| 160 | #print(file_path, config_name, config_attrs) |
| 161 | if config_name in all_configs: |
| 162 | raise Exception("Found {} at {}:{} but it was already declared at {}:{}".format(config_name, file_path, linenum, os.path.join(scandir, all_configs[config_name]['filename']), all_configs[config_name]['line_number'])) |
| 163 | else: |
| 164 | all_configs[config_name] = {'attrs': config_attrs, 'filename': os.path.relpath(file_path, scandir), 'line_number': linenum, 'description': config_description} |
| 165 | else: |
| 166 | m = DEFINE_RE.match(line) |
| 167 | if m: |
| 168 | name = m.group(1) |
| 169 | value = m.group(2) |
| 170 | # discard any 'u' qualifier |
| 171 | m = re.match(r'^((0x)?\d+)u$', value.lower()) |
| 172 | if m: |
| 173 | value = m.group(1) |
| 174 | else: |
| 175 | # discard any '_u(X)' macro |
| 176 | m = re.match(r'^_u\(((0x)?\d+)\)$', value.lower()) |
| 177 | if m: |
| 178 | value = m.group(1) |
| 179 | if name not in all_defines: |
| 180 | all_defines[name] = dict() |
| 181 | if value not in all_defines[name]: |
| 182 | all_defines[name][value] = set() |
| 183 | all_defines[name][value] = (file_path, linenum) |
| 184 | |
| 185 | # Check for defines with missing PICO_CONFIG entries |
| 186 | resolved_defines = dict() |
| 187 | for d in all_defines: |
| 188 | if d not in all_configs and d.startswith("PICO_"): |
| 189 | logger.warning("Potential unmarked PICO define {}".format(d)) |
| 190 | # resolve "nested defines" - this allows e.g. USB_DPRAM_MAX to resolve to USB_DPRAM_SIZE which is set to 4096 (which then matches the relevant PICO_CONFIG entry) |
| 191 | for val in all_defines[d]: |
| 192 | if val in all_defines: |
| 193 | resolved_defines[d] = all_defines[val] |
| 194 | |
Ravago Jones | d208ae7 | 2023-02-13 02:24:07 -0800 | [diff] [blame] | 195 | for config_name, config_obj in all_configs.items(): |
| 196 | file_path = os.path.join(scandir, config_obj['filename']) |
| 197 | linenum = config_obj['line_number'] |
Austin Schuh | 208337d | 2022-01-01 14:29:11 -0800 | [diff] [blame] | 198 | |
Ravago Jones | d208ae7 | 2023-02-13 02:24:07 -0800 | [diff] [blame] | 199 | ValidateAttrs(config_obj['attrs'], file_path, linenum) |
Austin Schuh | 208337d | 2022-01-01 14:29:11 -0800 | [diff] [blame] | 200 | |
| 201 | # Check that default values match up |
Ravago Jones | d208ae7 | 2023-02-13 02:24:07 -0800 | [diff] [blame] | 202 | if 'default' in config_obj['attrs']: |
| 203 | config_default = config_obj['attrs']['default'] |
Austin Schuh | 208337d | 2022-01-01 14:29:11 -0800 | [diff] [blame] | 204 | if config_name in all_defines: |
Ravago Jones | d208ae7 | 2023-02-13 02:24:07 -0800 | [diff] [blame] | 205 | defines_obj = all_defines[config_name] |
| 206 | if config_default not in defines_obj and (config_name not in resolved_defines or config_default not in resolved_defines[config_name]): |
| 207 | if '/' in config_default or ' ' in config_default: |
Austin Schuh | 208337d | 2022-01-01 14:29:11 -0800 | [diff] [blame] | 208 | continue |
| 209 | # There _may_ be multiple matching defines, but arbitrarily display just one in the error message |
Ravago Jones | d208ae7 | 2023-02-13 02:24:07 -0800 | [diff] [blame] | 210 | first_define_value = list(defines_obj.keys())[0] |
| 211 | first_define_file_path, first_define_linenum = defines_obj[first_define_value] |
| 212 | raise Exception('Found {} at {}:{} with a default of {}, but #define says {} (at {}:{})'.format(config_name, file_path, linenum, config_default, first_define_value, first_define_file_path, first_define_linenum)) |
Austin Schuh | 208337d | 2022-01-01 14:29:11 -0800 | [diff] [blame] | 213 | else: |
Ravago Jones | d208ae7 | 2023-02-13 02:24:07 -0800 | [diff] [blame] | 214 | raise Exception('Found {} at {}:{} with a default of {}, but no matching #define found'.format(config_name, file_path, linenum, config_default)) |
Austin Schuh | 208337d | 2022-01-01 14:29:11 -0800 | [diff] [blame] | 215 | |
| 216 | with open(outfile, 'w', newline='') as csvfile: |
| 217 | fieldnames = ('name', 'location', 'description', 'type') + tuple(sorted(all_attrs - set(['type']))) |
| 218 | writer = csv.DictWriter(csvfile, fieldnames=fieldnames, extrasaction='ignore', dialect='excel-tab') |
| 219 | |
| 220 | writer.writeheader() |
Ravago Jones | d208ae7 | 2023-02-13 02:24:07 -0800 | [diff] [blame] | 221 | for config_name, config_obj in sorted(all_configs.items()): |
| 222 | writer.writerow({'name': config_name, 'location': '{}:{}'.format(config_obj['filename'], config_obj['line_number']), 'description': config_obj['description'], **config_obj['attrs']}) |