2021-01-20 16:44:27 +00:00
#!/usr/bin/env python3
#
# Copyright (c) 2021 Raspberry Pi (Trading) Ltd.
#
# SPDX-License-Identifier: BSD-3-Clause
#
#
2021-01-26 13:56:30 +00:00
# Script to scan the Raspberry Pi Pico SDK tree searching for configuration items
2021-01-20 16:44:27 +00:00
# Outputs a tab separated file of the configuration item:
# name location description type advanced default depends enumvalues group max min
#
# Usage:
#
# ./extract_configs.py <root of source tree> [output file]
#
# If not specified, output file will be `pico_configs.tsv`
import os
import sys
import re
import csv
import logging
logger = logging . getLogger ( __name__ )
logging . basicConfig ( level = logging . INFO )
scandir = sys . argv [ 1 ]
outfile = sys . argv [ 2 ] if len ( sys . argv ) > 2 else ' pico_configs.tsv '
CONFIG_RE = re . compile ( r ' // \ s+PICO_CONFIG: \ s+( \ w+), \ s+([^,]+)(?:, \ s+(.*))?$ ' )
DEFINE_RE = re . compile ( r ' #define \ s+( \ w+) \ s+( \ w+)$ ' )
all_configs = { }
all_attrs = set ( )
all_descriptions = { }
all_defines = { }
def ValidateAttrs ( config_attrs ) :
_type = config_attrs . get ( ' type ' , ' int ' )
# Validate attrs
if _type == ' int ' :
assert ' enumvalues ' not in config_attrs
_min = _max = _default = None
if config_attrs . get ( ' min ' , None ) is not None :
value = config_attrs [ ' min ' ]
m = re . match ( r ' ^( \ d+)e( \ d+)$ ' , value . lower ( ) )
if m :
_min = int ( m . group ( 1 ) ) * 10 * * int ( m . group ( 2 ) )
else :
_min = int ( value , 0 )
if config_attrs . get ( ' max ' , None ) is not None :
value = config_attrs [ ' max ' ]
m = re . match ( r ' ^( \ d+)e( \ d+)$ ' , value . lower ( ) )
if m :
_max = int ( m . group ( 1 ) ) * 10 * * int ( m . group ( 2 ) )
else :
_max = int ( value , 0 )
if config_attrs . get ( ' default ' , None ) is not None :
if ' / ' not in config_attrs [ ' default ' ] :
try :
value = config_attrs [ ' default ' ]
m = re . match ( r ' ^( \ d+)e( \ d+)$ ' , value . lower ( ) )
if m :
_default = int ( m . group ( 1 ) ) * 10 * * int ( m . group ( 2 ) )
else :
_default = int ( value , 0 )
except ValueError :
pass
if _min is not None and _max is not None :
if _min > _max :
raise Exception ( ' {} at {} : {} has min {} > max {} ' . format ( config_name , file_path , linenum , config_attrs [ ' min ' ] , config_attrs [ ' max ' ] ) )
if _min is not None and _default is not None :
if _min > _default :
raise Exception ( ' {} at {} : {} has min {} > default {} ' . format ( config_name , file_path , linenum , config_attrs [ ' min ' ] , config_attrs [ ' default ' ] ) )
if _default is not None and _max is not None :
if _default > _max :
raise Exception ( ' {} at {} : {} has default {} > max {} ' . format ( config_name , file_path , linenum , config_attrs [ ' default ' ] , config_attrs [ ' max ' ] ) )
elif _type == ' bool ' :
assert ' min ' not in config_attrs
assert ' max ' not in config_attrs
assert ' enumvalues ' not in config_attrs
_default = config_attrs . get ( ' default ' , None )
if _default is not None :
if ' / ' not in _default :
if ( _default . lower ( ) != ' 0 ' ) and ( config_attrs [ ' default ' ] . lower ( ) != ' 1 ' ) and ( _default not in all_configs ) :
logger . info ( ' {} at {} : {} has non-integer default value " {} " ' . format ( config_name , file_path , linenum , config_attrs [ ' default ' ] ) )
elif _type == ' enum ' :
assert ' min ' not in config_attrs
assert ' max ' not in config_attrs
assert ' enumvalues ' in config_attrs
_enumvalues = tuple ( config_attrs [ ' enumvalues ' ] . split ( ' | ' ) )
_default = None
if config_attrs . get ( ' default ' , None ) is not None :
_default = config_attrs [ ' default ' ]
if _default is not None :
if _default not in _enumvalues :
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 ' ] ) )
else :
raise Exception ( " Found unknown PICO_CONFIG type {} at {} : {} " . format ( _type , file_path , linenum ) )
# Scan all .c and .h files in the specific path, recursively.
for dirpath , dirnames , filenames in os . walk ( scandir ) :
for filename in filenames :
file_ext = os . path . splitext ( filename ) [ 1 ]
if file_ext in ( ' .c ' , ' .h ' ) :
file_path = os . path . join ( dirpath , filename )
with open ( file_path , encoding = " ISO-8859-1 " ) as fh :
linenum = 0
for line in fh . readlines ( ) :
linenum + = 1
line = line . strip ( )
m = CONFIG_RE . match ( line )
if m :
config_name = m . group ( 1 )
config_description = m . group ( 2 )
_attrs = m . group ( 3 )
# allow commas to appear inside brackets by converting them to and from NULL chars
_attrs = re . sub ( r ' ( \ (.+ \ )) ' , lambda m : m . group ( 1 ) . replace ( ' , ' , ' \0 ' ) , _attrs )
if ' = ' in config_description :
raise Exception ( " For {} at {} : {} the description was set to ' {} ' - has the description field been omitted? " . format ( config_name , file_path , linenum , config_description ) )
if config_description in all_descriptions :
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 ' ] ) )
else :
all_descriptions [ config_description ] = { ' config_name ' : config_name , ' filename ' : os . path . relpath ( file_path , scandir ) , ' line_number ' : linenum }
config_attrs = { }
prev = None
# Handle case where attr value contains a comma
for item in _attrs . split ( ' , ' ) :
if " = " not in item :
assert ( prev )
item = prev + " , " + item
try :
k , v = ( i . strip ( ) for i in item . split ( ' = ' ) )
except ValueError :
raise Exception ( ' {} at {} : {} has malformed value {} ' . format ( config_name , file_path , linenum , item ) )
config_attrs [ k ] = v . replace ( ' \0 ' , ' , ' ) if v != ' undefined ' else None
all_attrs . add ( k )
prev = item
#print(file_path, config_name, config_attrs)
if ' group ' not in config_attrs :
raise Exception ( ' {} at {} : {} has no group attribute ' . format ( config_name , file_path , linenum ) )
#print(file_path, config_name, config_attrs)
if config_name in all_configs :
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 ' ] ) )
else :
all_configs [ config_name ] = { ' attrs ' : config_attrs , ' filename ' : os . path . relpath ( file_path , scandir ) , ' line_number ' : linenum , ' description ' : config_description }
else :
m = DEFINE_RE . match ( line )
if m :
name = m . group ( 1 )
value = m . group ( 2 )
# discard any 'u' qualifier
m = re . match ( r ' ^((0x)? \ d+)u$ ' , value . lower ( ) )
if m :
value = m . group ( 1 )
if name not in all_defines :
all_defines [ name ] = dict ( )
if value not in all_defines [ name ] :
all_defines [ name ] [ value ] = set ( )
all_defines [ name ] [ value ] = ( file_path , linenum )
# Check for defines with missing PICO_CONFIG entries
for d in all_defines :
if d not in all_configs and d . startswith ( " PICO_ " ) :
logger . warning ( " Potential unmarked PICO define {} " . format ( d ) )
for config_name in all_configs :
ValidateAttrs ( all_configs [ config_name ] [ ' attrs ' ] )
if ' default ' in all_configs [ config_name ] [ ' attrs ' ] and config_name in all_defines :
if all_configs [ config_name ] [ ' attrs ' ] [ ' default ' ] not in all_defines [ config_name ] and ( all_configs [ config_name ] [ ' attrs ' ] . get ( ' type ' , ' int ' ) != ' bool ' ) :
if config_name in [ ' USB_DPRAM_MAX ' ] or ' / ' in all_configs [ config_name ] [ ' attrs ' ] [ ' default ' ] :
continue
raise Exception ( ' Found {} at {} : {} with a default of {} , but #define says {} ' . format ( config_name , all_configs [ config_name ] [ ' filename ' ] , all_configs [ config_name ] [ ' line_number ' ] , all_configs [ config_name ] [ ' attrs ' ] [ ' default ' ] , all_defines [ config_name ] ) )
with open ( outfile , ' w ' , newline = ' ' ) as csvfile :
fieldnames = ( ' name ' , ' location ' , ' description ' , ' type ' ) + tuple ( sorted ( all_attrs - set ( [ ' type ' ] ) ) )
writer = csv . DictWriter ( csvfile , fieldnames = fieldnames , extrasaction = ' ignore ' , dialect = ' excel-tab ' )
writer . writeheader ( )
for config_name in sorted ( all_configs ) :
writer . writerow ( { ' name ' : config_name , ' location ' : ' {} : {} ' . format ( all_configs [ config_name ] [ ' filename ' ] , all_configs [ config_name ] [ ' line_number ' ] ) , ' description ' : all_configs [ config_name ] [ ' description ' ] , * * all_configs [ config_name ] [ ' attrs ' ] } )