Fork fix: the SDIO wedge is FIXED (esp-hosted-mcu #167)
Root cause (verified against our exact IDF tree, not the community guess): the "258" in "sdio_write_task: Failed to send data: 258" is NOT a timeout (that is 263). 258 = 0x102 = ESP_ERR_INVALID_ARG. On the ESP32-P4, block- mode CMD53 writes require the SOURCE buffer to be 64-byte (cache-line) aligned; the IDF sdmmc driver rejects a misaligned source with INVALID_ARG BEFORE any bus activity. esp_hosts write loop then declares "Unrecoverable host sdio state" and reboots the whole P4. The audio TX payload is not 64-aligned, so streaming mic audio wedged on the very FIRST frame (which is exactly what we saw: listening -> instant Failed to send -> reboot). This also explains why buffer/queue/clock/retry tuning all did nothing: the write never reached the bus. And why our symptom was instant, not after ~100 writes (the community block-mode-desync theory) — it is the first misaligned buffer, every time. Fix: vendored esp_hosted 2.12.11 as an editable local component (overrides the registry copy) and bounce a misaligned TX payload through one aligned DMA scratch buffer in hosted_sdio_write_block (port_esp_hosted_host_sdio.c). TX is serialized by the bus lock so a single static bounce buffer is safe; freed in hosted_sdio_deinit. Host-only change — no C6 reflash. VERIFIED ON HARDWARE (autonomous self-test): 40s of continuous mic-audio upstream streaming — the traffic that previously wedged on the first frame — ran clean, zero timeouts, zero reboots. A guarded SDIO_TX_SELFTEST harness is kept (compiled out) for future SDIO stress testing. Credit: root cause + patch designed via multi-agent investigation; the precise 258=INVALID_ARG decode (correcting the upstream community timeout assumption) came from checking our actual esp_err.h. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# check that the CHANGELOG.md file contains the changelog for the version
|
||||
# in idf_component.yml
|
||||
# exit with 0 if ok
|
||||
# exit with 1 if fail
|
||||
# to be run whenever idf_component.yml is updated
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
# paths to files to check
|
||||
yml_file = "idf_component.yml"
|
||||
changelog_file = "CHANGELOG.md"
|
||||
def get_idf_yml_version_as_string() -> str:
|
||||
# read the yml file
|
||||
file_info = open(yml_file, "r")
|
||||
info = file_info.read()
|
||||
file_info.close()
|
||||
# extract the version info
|
||||
ver = re.search("^version: \"([0-9.]+)\"", info)
|
||||
# print("yml:", ver.group(1))
|
||||
return ver.group(1)
|
||||
def changelog_has_version(ver_string: str, debug: bool = False) -> int:
|
||||
# iterate over the changelog file
|
||||
escaped_ver = re.escape(ver_string)
|
||||
# Match the colored version heading format:
|
||||
# # $${\color{COLOR} \text{VERSION}}$$ (with optional trailing text e.g. " - Some Title")
|
||||
pattern = r'^# \$\$\{\\color\{[a-z]+\} \\text\{' + escaped_ver + r'\}\}\$\$'
|
||||
if debug:
|
||||
print(f"[debug] Looking for version : {ver_string}")
|
||||
print(f"[debug] Using pattern : {pattern}")
|
||||
print(f"[debug] Scanning : {changelog_file}")
|
||||
print()
|
||||
with open(changelog_file, "r") as changelog:
|
||||
for lineno, line in enumerate(changelog, start=1):
|
||||
stripped = line.rstrip('\n')
|
||||
if re.match(pattern, stripped):
|
||||
if debug:
|
||||
print(f"[debug] MATCH at line {lineno}: {stripped!r}")
|
||||
return 0
|
||||
elif debug and stripped.startswith('# '):
|
||||
print(f"[debug] heading line {lineno:4d}: {stripped!r}")
|
||||
return 1
|
||||
def check(debug: bool = False) -> int:
|
||||
yml_string = get_idf_yml_version_as_string()
|
||||
if debug:
|
||||
print(f"[debug] Version from {yml_file}: {yml_string}")
|
||||
print()
|
||||
result = changelog_has_version(yml_string, debug=debug)
|
||||
if result:
|
||||
print(f"Changelog for version {yml_string} not found in {changelog_file}")
|
||||
if not debug:
|
||||
print(f"Tip: re-run with --debug for more details")
|
||||
return 1
|
||||
return 0
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Check that CHANGELOG.md contains an entry for the version in idf_component.yml"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--debug",
|
||||
action="store_true",
|
||||
help="Print the pattern used and all # headings found in the changelog to help diagnose failures"
|
||||
)
|
||||
args, _ = parser.parse_known_args() # parse_known_args ignores filenames passed by pre-commit
|
||||
sys.exit(check(debug=args.debug))
|
||||
@@ -0,0 +1,37 @@
|
||||
# don't modify this section!
|
||||
DEFAULT:
|
||||
perform_check: yes # should the check be performed?
|
||||
# Sections setting this to 'no' don't need to include any other options as they are ignored
|
||||
# When a file is using a section with the option set to 'no', no checks are performed.
|
||||
|
||||
# what licenses (or license expressions) are allowed for files in this section
|
||||
# when setting this option in a section, you need to list all the allowed licenses
|
||||
allowed_licenses:
|
||||
- Apache-2.0
|
||||
- Unlicense
|
||||
- CC0-1.0
|
||||
license_for_new_files: Apache-2.0 # license to be used when inserting a new copyright notice
|
||||
new_notice_c: | # notice for new C, CPP, H, HPP and LD files
|
||||
/*
|
||||
* SPDX-FileCopyrightText: {years} Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: {license}
|
||||
*/
|
||||
new_notice_python: | # notice for new python files
|
||||
# SPDX-FileCopyrightText: {years} Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: {license}
|
||||
|
||||
# comment lines matching:
|
||||
# SPDX-FileCopyrightText: year[-year] Espressif Systems
|
||||
# or
|
||||
# SPDX-FileContributor: year[-year] Espressif Systems
|
||||
# are replaced with this template prefixed with the correct comment notation (# or // or *) and SPDX- notation
|
||||
espressif_copyright: '{years} Espressif Systems (Shanghai) CO LTD'
|
||||
|
||||
ignore: # You can also select ignoring files here
|
||||
perform_check: no # Don't check files from that block
|
||||
include:
|
||||
- common/proto/
|
||||
- common/protobuf-c/
|
||||
- build/
|
||||
- docs/
|
||||
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# check the host and co-processor version numbers against the value in
|
||||
# idf_component.yml
|
||||
# exit with 0 if ok
|
||||
# exit with 1 if fail
|
||||
|
||||
import argparse
|
||||
from datetime import date
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
# paths to files to check
|
||||
yml_file = "idf_component.yml"
|
||||
coprocessor_version_file = "slave/main/esp_hosted_coprocessor_fw_ver.h"
|
||||
host_version_file = "host/esp_hosted_host_fw_ver.h"
|
||||
|
||||
# year starts from 2025
|
||||
start_year = 2025
|
||||
|
||||
def get_idf_yml_version() -> (int, int, int):
|
||||
# read the yml file
|
||||
file_info = open(yml_file, "r")
|
||||
info = file_info.read()
|
||||
file_info.close()
|
||||
|
||||
# extract the version info
|
||||
ver = re.search("^version: \"([0-9]+).([0-9]+).([0-9]+)\"", info)
|
||||
#print("yml:", ver.group(1), ver.group(2), ver.group(3))
|
||||
|
||||
# return version info as a tuple (major, minor, patch)
|
||||
return (ver.group(1), ver.group(2), ver.group(3))
|
||||
|
||||
def get_coprocessor_version() -> (int, int, int):
|
||||
# read the coprocessor file
|
||||
try:
|
||||
file_info = open(coprocessor_version_file, "r")
|
||||
except:
|
||||
print("Coprocessor file open error: default to 0.0.0")
|
||||
return (0, 0, 0)
|
||||
info = file_info.read()
|
||||
file_info.close()
|
||||
|
||||
# extract the version info
|
||||
major_re = re.search("_VERSION_MAJOR_1 ([0-9]+)", info)
|
||||
minor_re = re.search("_VERSION_MINOR_1 ([0-9]+)", info)
|
||||
patch_re = re.search("_VERSION_PATCH_1 ([0-9]+)", info)
|
||||
|
||||
if ((not major_re) or (not minor_re) or (not patch_re)):
|
||||
print("No coprocessor version info found: default to 0.0.0")
|
||||
return (0, 0, 0)
|
||||
|
||||
#print("coprocessor:", major_re.group(1), minor_re.group(1), patch_re.group(1))
|
||||
|
||||
return (major_re.group(1), minor_re.group(1), patch_re.group(1))
|
||||
|
||||
def get_host_version() -> (int, int, int):
|
||||
# read the host file
|
||||
try:
|
||||
file_info = open(host_version_file, "r")
|
||||
except:
|
||||
print("Host file open error: default to 0.0.0")
|
||||
return (0, 0, 0)
|
||||
info = file_info.read()
|
||||
file_info.close()
|
||||
|
||||
# extract the version info
|
||||
major_re = re.search("_VERSION_MAJOR_1 ([0-9]+)", info)
|
||||
minor_re = re.search("_VERSION_MINOR_1 ([0-9]+)", info)
|
||||
patch_re = re.search("_VERSION_PATCH_1 ([0-9]+)", info)
|
||||
|
||||
if ((not major_re) or (not minor_re) or (not patch_re)):
|
||||
print("No host version info found: default to 0.0.0")
|
||||
return (0, 0, 0)
|
||||
|
||||
#print("host:", major_re.group(1), minor_re.group(1), patch_re.group(1))
|
||||
|
||||
return (major_re.group(1), minor_re.group(1), patch_re.group(1))
|
||||
|
||||
# write common header using the provided file handle
|
||||
def write_common_header(file_info):
|
||||
year = date.today().year
|
||||
|
||||
file_info.write("/*\n")
|
||||
if (year == start_year):
|
||||
file_info.write(f" * SPDX-FileCopyrightText: {year} Espressif Systems (Shanghai) CO LTD\n")
|
||||
else:
|
||||
file_info.write(f" * SPDX-FileCopyrightText: {start_year}-{year} Espressif Systems (Shanghai) CO LTD\n")
|
||||
file_info.write(" *\n")
|
||||
file_info.write(" * SPDX-License-Identifier: Apache-2.0\n")
|
||||
file_info.write(" *\n")
|
||||
file_info.write(" * DO NOT MODIFY THIS FILE.\n")
|
||||
file_info.write(" *\n")
|
||||
file_info.write(" * tools/check_fw_versions.py generated this file.\n")
|
||||
file_info.write(" *\n")
|
||||
file_info.write(" * This file is autogenerated by a pre-commit hook.\n")
|
||||
file_info.write(" * Version info here is populated from idf_component.yml\n");
|
||||
file_info.write(" */\n")
|
||||
|
||||
# write footer for coprocessor file using the provided file handle
|
||||
def write_coprocessor_footer(file_info):
|
||||
file_info.write("\n")
|
||||
file_info.write("/**\n")
|
||||
file_info.write(" * Macro to convert version number into an integer\n")
|
||||
file_info.write(" */\n")
|
||||
file_info.write("#define ESP_HOSTED_VERSION_VAL(major, minor, patch) ((major << 16) | (minor << 8) | (patch))\n")
|
||||
file_info.write("\n")
|
||||
|
||||
# write footer for host file using the provided file handle
|
||||
def write_host_footer(file_info):
|
||||
file_info.write("\n")
|
||||
file_info.write("/**\n")
|
||||
file_info.write(" * Macro to convert version number into an integer\n")
|
||||
file_info.write(" */\n")
|
||||
file_info.write("#define ESP_HOSTED_VERSION_VAL(major, minor, patch) ((major << 16) | (minor << 8) | (patch))\n")
|
||||
file_info.write("\n")
|
||||
file_info.write("/* Extract version components from version value */\n")
|
||||
file_info.write("#define ESP_HOSTED_VERSION_MAJOR(ver) (((ver) >> 16) & 0xFF)\n")
|
||||
file_info.write("#define ESP_HOSTED_VERSION_MINOR(ver) (((ver) >> 8) & 0xFF)\n")
|
||||
file_info.write("#define ESP_HOSTED_VERSION_PATCH(ver) ((ver) & 0xFF)\n")
|
||||
file_info.write("\n")
|
||||
file_info.write("/* Format version tuple for printing */\n")
|
||||
file_info.write("#define ESP_HOSTED_VERSION_PRINTF_ARGS(ver) \\\n")
|
||||
file_info.write("\t(unsigned int)ESP_HOSTED_VERSION_MAJOR(ver), \\\n")
|
||||
file_info.write("\t(unsigned int)ESP_HOSTED_VERSION_MINOR(ver), \\\n")
|
||||
file_info.write("\t(unsigned int)ESP_HOSTED_VERSION_PATCH(ver)\n")
|
||||
file_info.write("\n")
|
||||
file_info.write("#define ESP_HOSTED_VERSION_PRINTF_FMT \"%u.%u.%u\"\n")
|
||||
file_info.write("\n")
|
||||
|
||||
def set_coprocessor_version(version: tuple) -> int:
|
||||
# write the coprocessor file
|
||||
file_info = open(coprocessor_version_file, "w")
|
||||
|
||||
write_common_header(file_info)
|
||||
|
||||
file_info.write("#ifndef __ESP_HOSTED_COPROCESSOR_FW_VER_H__\n")
|
||||
file_info.write("#define __ESP_HOSTED_COPROCESSOR_FW_VER_H__\n")
|
||||
file_info.write("\n")
|
||||
ver = version[0]
|
||||
file_info.write(f"#define PROJECT_VERSION_MAJOR_1 {ver}\n")
|
||||
ver = version[1]
|
||||
file_info.write(f"#define PROJECT_VERSION_MINOR_1 {ver}\n")
|
||||
ver = version[2]
|
||||
file_info.write(f"#define PROJECT_VERSION_PATCH_1 {ver}\n")
|
||||
write_coprocessor_footer(file_info)
|
||||
file_info.write("#endif\n")
|
||||
file_info.close()
|
||||
return 0
|
||||
|
||||
def set_host_version(version: tuple) -> int:
|
||||
# write the host file
|
||||
file_info = open(host_version_file, "w")
|
||||
|
||||
write_common_header(file_info)
|
||||
|
||||
file_info.write("#ifndef __ESP_HOSTED_HOST_FW_VERSION_H__\n");
|
||||
file_info.write("#define __ESP_HOSTED_HOST_FW_VERSION_H__\n");
|
||||
file_info.write("\n")
|
||||
ver = version[0]
|
||||
file_info.write(f"#define ESP_HOSTED_VERSION_MAJOR_1 {ver}\n")
|
||||
ver = version[1]
|
||||
file_info.write(f"#define ESP_HOSTED_VERSION_MINOR_1 {ver}\n")
|
||||
ver = version[2]
|
||||
file_info.write(f"#define ESP_HOSTED_VERSION_PATCH_1 {ver}\n")
|
||||
write_host_footer(file_info)
|
||||
file_info.write("#endif\n")
|
||||
file_info.close()
|
||||
return 0
|
||||
|
||||
def check_slave_version(yml_version: tuple) -> int:
|
||||
version = get_coprocessor_version()
|
||||
if (version == yml_version):
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
def check_host_version(yml_version: tuple) -> int:
|
||||
version = get_host_version()
|
||||
if (version == yml_version):
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
def check(args) -> int:
|
||||
ret = 0
|
||||
yml_version = get_idf_yml_version()
|
||||
update = args.update
|
||||
force = args.force
|
||||
|
||||
if (check_slave_version(yml_version)):
|
||||
print(f"Co-processor version check failed. Info in {coprocessor_version_file} different from {yml_file}.")
|
||||
if (update or force):
|
||||
print(f"{coprocessor_version_file} updated with correct version info.")
|
||||
set_coprocessor_version(yml_version)
|
||||
ret = 1
|
||||
elif force:
|
||||
print(f"Force updating version info in {coprocessor_version_file}.")
|
||||
set_coprocessor_version(yml_version)
|
||||
|
||||
if (check_host_version(yml_version)):
|
||||
print(f"Host version check failed. Info in {host_version_file} different from {yml_file}.")
|
||||
if (update or force):
|
||||
set_host_version(yml_version)
|
||||
print(f"{host_version_file} updated with correct version info.")
|
||||
ret = 1
|
||||
elif force:
|
||||
print(f"Force updating version info in {host_version_file}.")
|
||||
set_host_version(yml_version)
|
||||
|
||||
return ret
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description = "Check, update version headers against component version")
|
||||
parser.add_argument('-u', '--update', action = 'store_true',
|
||||
help = "Updates version headers if different from component version")
|
||||
parser.add_argument('-f', '--force', action = 'store_true',
|
||||
help = "Force update version headers")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
sys.exit(check(args))
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# check that RPC calls in protobuf and documentation files match
|
||||
# exit with 0 if ok
|
||||
# exit with 1 if fail
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
# method:
|
||||
# 1. get the set of RPC calls from both the protobuf and documentation files
|
||||
# 2. compare the sets
|
||||
# 3. if there are differences, print the differences
|
||||
|
||||
protobuf_file = "common/proto/esp_hosted_rpc.proto"
|
||||
doc_file = "docs/implemented_rpcs.md"
|
||||
|
||||
def get_set_from_document() -> set:
|
||||
set_rpcs = set()
|
||||
|
||||
# read the protobuf file
|
||||
try:
|
||||
file_info = open(doc_file, "r")
|
||||
except:
|
||||
print("Document file open error")
|
||||
return set_of_rpcs
|
||||
|
||||
# from this pattern in the doc_file:
|
||||
# | 1 | 257 | GetMacAddress | 0.0.6 |
|
||||
# it will extract GetMacAddress
|
||||
# this pattern works for the list of RCP Requests and Events
|
||||
pattern = re.compile(r"\|\s*\d+\s*\|\s*\d+\s*\|\s*(\S+)")
|
||||
|
||||
for line in file_info:
|
||||
# get the list of rpc calls
|
||||
rpc_call = pattern.search(line)
|
||||
if rpc_call:
|
||||
# print(rpc_call.group(1))
|
||||
set_rpcs.add(rpc_call.group(1))
|
||||
|
||||
file_info.close()
|
||||
|
||||
# print(set_rpcs)
|
||||
|
||||
return set_rpcs
|
||||
|
||||
def get_set_from_protobuf() -> set:
|
||||
set_rpcs = set()
|
||||
|
||||
# read the protobuf file
|
||||
try:
|
||||
file_info = open(protobuf_file, "r")
|
||||
except:
|
||||
print("Protobuf file open error")
|
||||
return set_of_rpcs
|
||||
|
||||
# extract requests
|
||||
# from this pattern in the protobuf_file
|
||||
# Rpc_Req_GetMacAddress req_get_mac_address = 257;
|
||||
# it will extract GetMacAddress
|
||||
pattern_req = re.compile(r"\s+(Rpc_Req_)(\S+)\s+req_")
|
||||
|
||||
# extract events
|
||||
# from this pattern in the protobuf_file
|
||||
# Rpc_Event_ESPInit event_esp_init = 769;
|
||||
# it will extract ESPInit
|
||||
pattern_event = re.compile(r"\s+(Rpc_Event_)(\S+)\s+event_")
|
||||
|
||||
for line in file_info:
|
||||
# get the list of rpc calls
|
||||
rpc_call = pattern_req.search(line)
|
||||
if rpc_call:
|
||||
set_rpcs.add(rpc_call.group(2))
|
||||
rpc_call = pattern_event.search(line)
|
||||
if rpc_call:
|
||||
set_rpcs.add(rpc_call.group(2))
|
||||
|
||||
file_info.close()
|
||||
|
||||
# print(set_rpcs)
|
||||
|
||||
return set_rpcs
|
||||
|
||||
def check() -> int:
|
||||
ret = 1
|
||||
|
||||
# get the set of RPC calls in the protobuf file
|
||||
protobuf_set = get_set_from_protobuf()
|
||||
|
||||
# get the set of RPC calls in the documentation file
|
||||
document_set = get_set_from_document()
|
||||
|
||||
# compare the two
|
||||
|
||||
# if not equal
|
||||
if (protobuf_set == document_set):
|
||||
ret = 0
|
||||
else:
|
||||
# get the differences
|
||||
only_in_docs = document_set.difference(protobuf_set)
|
||||
only_in_protobuf = protobuf_set.difference(document_set)
|
||||
|
||||
if only_in_docs:
|
||||
print("Error: RPCs only found in document:", only_in_docs)
|
||||
if only_in_protobuf:
|
||||
print("Error: RPCs only found in protobuf:", only_in_protobuf)
|
||||
|
||||
return ret
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(check())
|
||||
@@ -0,0 +1,223 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
Check that functions are properly marked as weak symbols.
|
||||
|
||||
Usage:
|
||||
# Single file, all esp_ functions:
|
||||
./check_weak_functions.py --file path/to/file.c
|
||||
|
||||
# Single file, specific functions:
|
||||
./check_weak_functions.py --file path/to/file.c --functions func1,func2,func3
|
||||
|
||||
# Multiple files (--functions applies to the preceding --file):
|
||||
./check_weak_functions.py --file file1.c --functions a,b --file file2.c --file file3.c --functions c
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
# All possible weak symbol markers
|
||||
WEAK_MARKERS = [
|
||||
'H_WEAK_REF',
|
||||
'WEAK_REF',
|
||||
'WEAK',
|
||||
'__attribute__((weak))',
|
||||
]
|
||||
|
||||
def check_weak_functions(filepath, target_functions=None, debug=False, verbose=False):
|
||||
"""
|
||||
Check that functions have weak symbol attributes.
|
||||
|
||||
Args:
|
||||
filepath: Path to the file to check
|
||||
target_functions: List of specific function names to check, or None for all
|
||||
debug: Enable debug output
|
||||
verbose: Print success messages
|
||||
|
||||
Returns:
|
||||
bool: True if all checks pass, False otherwise
|
||||
"""
|
||||
|
||||
if not Path(filepath).exists():
|
||||
print(f"❌ ERROR: {filepath} not found.", file=sys.stderr)
|
||||
return False
|
||||
|
||||
with open(filepath, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
violations = []
|
||||
found_functions = {}
|
||||
|
||||
# Pattern to match function definitions (not calls)
|
||||
# A function definition has the opening brace { on the same or next line
|
||||
func_def_pattern = r'([^\n;]*?)\b(\w+)\s*\([^)]*\)\s*(?:\n\s*)?\{'
|
||||
|
||||
for match in re.finditer(func_def_pattern, content, re.MULTILINE):
|
||||
function_name = match.group(2)
|
||||
|
||||
# Only check functions starting with esp_
|
||||
if not function_name.startswith('esp_'):
|
||||
continue
|
||||
|
||||
# Find the line number
|
||||
start_pos = match.start()
|
||||
line_num = content[:start_pos].count('\n') + 1
|
||||
|
||||
# Get the full line(s) for this function definition
|
||||
declaration = match.group(0)
|
||||
|
||||
# Check if any weak marker is present in the declaration
|
||||
has_weak_marker = any(marker in declaration for marker in WEAK_MARKERS)
|
||||
found_marker = None
|
||||
if has_weak_marker:
|
||||
for marker in WEAK_MARKERS:
|
||||
if marker in declaration:
|
||||
found_marker = marker
|
||||
break
|
||||
|
||||
if debug:
|
||||
marker_str = found_marker if has_weak_marker else "NONE"
|
||||
print(f"DEBUG: Line {line_num}: {function_name}()")
|
||||
print(f" Marker: {marker_str}")
|
||||
print(f" Declaration: {declaration[:100]}...")
|
||||
print()
|
||||
|
||||
found_functions[function_name] = {
|
||||
'line': line_num,
|
||||
'has_weak': has_weak_marker,
|
||||
'marker': found_marker
|
||||
}
|
||||
|
||||
# If checking specific functions, skip others
|
||||
if target_functions and function_name not in target_functions:
|
||||
continue
|
||||
|
||||
if not has_weak_marker:
|
||||
violations.append(f" Line {line_num}: {function_name}() - Missing weak marker")
|
||||
|
||||
# Check if all target functions were found
|
||||
if target_functions:
|
||||
missing = set(target_functions) - set(found_functions.keys())
|
||||
if missing:
|
||||
print(f"❌ ERROR: The following functions were not found in {filepath}:", file=sys.stderr)
|
||||
for func in sorted(missing):
|
||||
print(f" - {func}()", file=sys.stderr)
|
||||
print(file=sys.stderr)
|
||||
return False
|
||||
|
||||
# Report results
|
||||
if violations:
|
||||
print(f"❌ ERROR: Found function definitions without weak markers in {filepath}:", file=sys.stderr)
|
||||
print('\n'.join(violations), file=sys.stderr)
|
||||
print(f"\nAccepted weak markers: {', '.join(WEAK_MARKERS)}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
# Success message (only if verbose or debug)
|
||||
if verbose or debug:
|
||||
if target_functions:
|
||||
checked_count = len([f for f in target_functions if f in found_functions])
|
||||
if checked_count > 0:
|
||||
print(f"✅ Checked {checked_count} function(s) in {filepath}")
|
||||
for func in target_functions:
|
||||
if func in found_functions:
|
||||
info = found_functions[func]
|
||||
marker_display = info['marker'] if info['marker'] else "NO MARKER"
|
||||
print(f" Line {info['line']}: {func}() - {marker_display}")
|
||||
else:
|
||||
total = len(found_functions)
|
||||
print(f"✅ All {total} function(s) in {filepath} are properly marked as weak")
|
||||
|
||||
return True
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Check that functions are properly marked as weak symbols',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--file', '-f',
|
||||
dest='files',
|
||||
metavar='FILE',
|
||||
help='File to check. Can be repeated. Each --file may be followed by --functions.'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--functions', '-F',
|
||||
help='Comma-separated list of specific function names to check'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--verbose', '-v',
|
||||
action='store_true',
|
||||
help='Print success messages'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--debug', '-d',
|
||||
action='store_true',
|
||||
help='Debug output'
|
||||
)
|
||||
|
||||
# Don't use parse_args() — walk sys.argv manually so we can pair
|
||||
# each --file with the --functions that immediately follows it (if any).
|
||||
checks = []
|
||||
current_file = None
|
||||
verbose = False
|
||||
debug = False
|
||||
|
||||
i = 1
|
||||
while i < len(sys.argv):
|
||||
arg = sys.argv[i]
|
||||
if arg in ('--file', '-f'):
|
||||
if current_file is not None:
|
||||
# previous --file had no --functions, check all
|
||||
checks.append((current_file, None))
|
||||
current_file = sys.argv[i + 1]
|
||||
i += 2
|
||||
elif arg in ('--functions', '-F'):
|
||||
if current_file is None:
|
||||
print("❌ ERROR: --functions must follow --file", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
funcs = [f.strip() for f in sys.argv[i + 1].split(',') if f.strip()]
|
||||
checks.append((current_file, funcs))
|
||||
current_file = None
|
||||
i += 2
|
||||
elif arg in ('--verbose', '-v'):
|
||||
verbose = True
|
||||
i += 1
|
||||
elif arg in ('--debug', '-d'):
|
||||
debug = True
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
|
||||
# flush last --file if it had no --functions
|
||||
if current_file is not None:
|
||||
checks.append((current_file, None))
|
||||
|
||||
if not checks:
|
||||
print("❌ ERROR: At least one --file is required.", file=sys.stderr)
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
if debug:
|
||||
for filepath, funcs in checks:
|
||||
func_str = ', '.join(funcs) if funcs else 'all esp_ functions'
|
||||
print(f"Checking: {filepath} ({func_str})")
|
||||
print()
|
||||
|
||||
all_passed = True
|
||||
for filepath, target_functions in checks:
|
||||
if not check_weak_functions(filepath, target_functions, debug=debug, verbose=verbose):
|
||||
all_passed = False
|
||||
|
||||
if not all_passed:
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user