mirror of
https://github.com/raspberrypi/pico-sdk.git
synced 2025-07-29 22:01:12 +03:00
* Add action to check board headers when modified * Fix invalid escape warning * Check for more board header errors before exiting script * Also run when action file changes * Add back newline at end of check_all_board_headers.sh * Remove python install step * `e.__str__()` -> `str(e)`
222 lines
8.6 KiB
Python
Executable File
222 lines
8.6 KiB
Python
Executable File
#!/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)
|