1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
|
#!/usr/bin/env python3
#
# Copyright (c) 2025 Raspberry Pi (Trading) Ltd.
#
# SPDX-License-Identifier: BSD-3-Clause
#
#
# Script to scan the Raspberry Pi Pico SDK tree searching for CMake functions
# Outputs a tab separated file of the functions:
# name group signature brief description params
#
# Usage:
#
# tools/extract_cmake_functions.py <root of repo> [output file]
#
# If not specified, output file will be `pico_cmake_functions.tsv`
import os
import sys
import re
import csv
import logging
if sys.version_info < (3, 11):
# Python <3.11 doesn't have ExceptionGroup, so define a simple one
class ExceptionGroup(Exception):
def __init__(self, message, errors):
message += "\n" + "\n".join(str(e) for e in errors)
super().__init__(message)
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
scandir = sys.argv[1]
outfile = sys.argv[2] if len(sys.argv) > 2 else 'pico_cmake_functions.tsv'
CMAKE_FUNCTION_RE = re.compile(r'^\s*#(.*)((\n\s*#.*)*)\n\s*function\(([^\s]*)', re.MULTILINE)
CMAKE_PICO_FUNCTIONS_RE = re.compile(r'^\s*function\((pico_[^\s\)]*)', re.MULTILINE)
# Files containing internal functions that don't need to be documented publicly
skip_files = set([
"pico_sdk_init.cmake",
"pico_utils.cmake",
"no_hardware.cmake",
"find_compiler.cmake",
])
skip_groups = set([
"src", # skip the root src/CMakeLists.txt
])
# Other internal functions that don't need to be documented publicly
allowed_missing_functions = set([
"pico_init_pioasm",
"pico_init_picotool",
"pico_add_platform_library",
"pico_get_runtime_output_directory",
"pico_get_output_name",
"pico_set_printf_implementation",
"pico_expand_pico_platform",
])
# Group descriptions
group_names_descriptions = {
'boot_stage2': ('Boot Stage 2', 'CMake functions to create stage 2 bootloaders'),
'pico_binary_info': ('Pico Binary Info', 'CMake functions to add binary info to the output binary'),
'pico_btstack': ('Pico BTstack', 'CMake functions to configure the bluetooth stack'),
'pico_lwip': ('Pico LwIP', 'CMake functions to configure LwIP'),
'pico_cyw43_driver': ('Pico CYW43 Driver', 'CMake functions to configure the CYW43 driver'),
'pico_runtime': ('Pico Runtime', 'CMake functions to configure the runtime environment'),
'pico_standard_link': ('Pico Standard Link', 'CMake functions to configure the linker'),
'pico_stdio': ('Pico Standard I/O', 'CMake functions to configure the standard I/O library'),
'pico_pio': ('Pico PIO', 'CMake functions to generate PIO headers'),
'other': ('Other', 'Other CMake functions'),
}
all_functions = {}
for group, (brief, description) in group_names_descriptions.items():
all_functions['_desc_{group}'.format(group=group)] = {
'name': '_desc_{group}'.format(group=group),
'group': group,
'signature': '',
'brief': brief,
'description': description,
'params': '',
}
# Supported commands:
# \brief\ <brief description, which should be included in the main description>
# \brief_nodesc\ <brief description, which should be excluded from the main description>
# \param\ <parameter_name> <parameter description>
# \ingroup\ <group_name>
#
# Commands in the middle of a line are not supported
#
# The ; character at the end of a line denotes a hard line break in the description
# The \ character (outside of a command) and the # character are not supported in descriptions
def process_commands(description, name, group, signature):
brief = ''
params = []
desc = ''
errors = []
for line in description.split('\n'):
line = line.strip()
if line.startswith('\\'):
_, command, remainder = line.split('\\', 2)
remainder = remainder.strip()
if command == 'param':
# Parameter name and description
params.append(remainder)
elif command == 'brief':
# Brief description
brief = remainder
desc += brief + '\\n'
elif command == 'brief_nodesc':
# Brief description which should not be included in the main description
brief = remainder
elif command == 'ingroup':
# Group name override
group = remainder
else:
errors.append(Exception("{}:{} has unknown command: {}".format(group, name, command)))
elif '\\' in line:
errors.append(Exception("{}:{} has a line containing '\\': {}".format(group, name, line)))
else:
desc += line + '\\n'
# Check that there are no semicolons in the parameter descriptions, as that's the delimiter for the parameter list
if any([';' in x for x in params]):
errors.append(Exception("{}:{} has a parameter description containing ';'".format(group, name)))
# Check that all parameters are in the signature
signature_words = set(re.split(r'\W+', signature))
for param in params:
param_name = param.split(' ', maxsplit=1)[0]
if param_name not in signature_words:
errors.append(Exception("{}:{} has a parameter {} which is not in the signature {}".format(group, name, param_name, signature)))
# Check that the brief description is not empty
if not brief:
logger.warning("{}:{} has no brief description".format(group, name))
# Check that the group has a description
if group not in group_names_descriptions:
errors.append(Exception("{} has no group description (referenced from {})".format(group, name)))
desc = re.sub(r'^(\\n)*(.*?)(\\n)*$', r'\2', desc)
return desc.strip(), brief, ';'.join(params), group, errors
def sort_functions(item):
group = item['group']
name = item['name']
precedence = 5
if name.startswith('_desc_'):
precedence = 0
elif group == 'other':
if re.match(r'^pico_add_.*_output$', name):
precedence = 1
elif name == 'pico_add_extra_outputs':
precedence = 2
elif re.match(r'^pico_.*_binary$', name):
precedence = 3
return group + str(precedence) + name
all_errors = []
# Scan all CMakeLists.txt and .cmake files in the specific path, recursively.
for dirpath, dirnames, filenames in os.walk(scandir):
for filename in filenames:
if filename in skip_files:
continue
# Default group is the directory name - can be overridden by the \ingroup\ command
group = os.path.basename(dirpath)
if group in skip_groups:
continue
if group in ['tools', 'cmake']:
group = 'other'
file_ext = os.path.splitext(filename)[1]
if filename == 'CMakeLists.txt' or file_ext == '.cmake':
file_path = os.path.join(dirpath, filename)
with open(file_path, encoding="ISO-8859-1") as fh:
text = fh.read()
for match in CMAKE_FUNCTION_RE.finditer(text):
name = match.group(4)
signature = match.group(1).strip()
if signature.startswith(name):
description, brief, params, processed_group, errors = process_commands(match.group(2).replace('#', ''), name, group, signature)
all_errors.extend(errors)
new_dict = {
'name': name,
'group': processed_group,
'signature': signature,
'brief': brief,
'description': description,
'params': params
}
if all_functions.get(name):
if new_dict != all_functions[name]:
logger.warning("{}:{} has multiple different definitions - using the new one from {}".format(processed_group, name, file_path))
all_functions[name] = new_dict
for match in CMAKE_PICO_FUNCTIONS_RE.finditer(text):
name = match.group(1)
if name not in all_functions and name not in allowed_missing_functions:
logger.warning("{} function has no description in {}".format(name, file_path))
if all_errors:
raise ExceptionGroup("Errors in {}".format(outfile), all_errors)
with open(outfile, 'w', newline='') as csvfile:
fieldnames = ('name', 'group', 'signature', 'brief', 'description', 'params')
writer = csv.DictWriter(csvfile, fieldnames=fieldnames, extrasaction='ignore', dialect='excel-tab')
writer.writeheader()
for row in sorted(all_functions.values(), key=sort_functions):
writer.writerow(row)
|