mirror of
https://github.com/wled/WLED.git
synced 2026-04-20 22:22:51 +00:00
Avoid checking usermods we've already matched, and exit early if we've got everything. Co-authored-by: Claude <noreply@anthropic.com>
127 lines
5.3 KiB
Python
127 lines
5.3 KiB
Python
import os
|
|
import re
|
|
import subprocess
|
|
from pathlib import Path # For OS-agnostic path manipulation
|
|
from click import secho
|
|
from SCons.Script import Action, Exit
|
|
Import("env")
|
|
|
|
|
|
def read_lines(p: Path):
|
|
""" Read in the contents of a file for analysis """
|
|
with p.open("r", encoding="utf-8", errors="ignore") as f:
|
|
return f.readlines()
|
|
|
|
|
|
def _get_nm_path(env) -> str:
|
|
""" Derive the nm tool path from the build environment """
|
|
if "NM" in env:
|
|
return env.subst("$NM")
|
|
# Derive from the C compiler: xtensa-esp32-elf-gcc → xtensa-esp32-elf-nm
|
|
cc = env.subst("$CC")
|
|
nm = re.sub(r'(gcc|g\+\+)$', 'nm', os.path.basename(cc))
|
|
return os.path.join(os.path.dirname(cc), nm)
|
|
|
|
|
|
def check_elf_modules(elf_path: Path, env, module_lib_builders) -> set[str]:
|
|
""" Check which modules have at least one defined symbol placed in the ELF.
|
|
|
|
The map file is not a reliable source for this: with LTO, original object
|
|
file paths are replaced by temporary ltrans.o partitions in all output
|
|
sections, making per-module attribution impossible from the map alone.
|
|
Instead we invoke nm --defined-only -l on the ELF, which uses DWARF debug
|
|
info to attribute each placed symbol to its original source file.
|
|
|
|
Requires usermod libraries to be compiled with -g so that DWARF sections
|
|
are present in the ELF. load_usermods.py injects -g for all WLED modules
|
|
via dep.env.AppendUnique(CCFLAGS=["-g"]).
|
|
|
|
Returns the set of build_dir basenames for confirmed modules.
|
|
"""
|
|
nm_path = _get_nm_path(env)
|
|
try:
|
|
result = subprocess.run(
|
|
[nm_path, "--defined-only", "-l", str(elf_path)],
|
|
capture_output=True, text=True, errors="ignore", timeout=120,
|
|
)
|
|
nm_output = result.stdout
|
|
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
|
secho(f"WARNING: nm failed ({e}); skipping per-module validation", fg="yellow", err=True)
|
|
return {Path(b.build_dir).name for b in module_lib_builders} # conservative pass
|
|
|
|
# Match placed symbols against builders as we parse nm output, exiting early
|
|
# once all builders are accounted for.
|
|
# nm --defined-only still includes debugging symbols (type 'N') such as the
|
|
# per-CU markers GCC emits in .debug_info (e.g. "usermod_example_cpp_6734d48d").
|
|
# These live at address 0x00000000 in their debug section — not in any load
|
|
# segment — so filtering them out leaves only genuinely placed symbols.
|
|
# nm -l appends a tab-separated "file:lineno" location to each symbol line.
|
|
remaining = {Path(str(b.src_dir)): Path(b.build_dir).name for b in module_lib_builders}
|
|
found = set()
|
|
|
|
for line in nm_output.splitlines():
|
|
if not remaining:
|
|
break # all builders matched
|
|
addr, _, _ = line.partition(' ')
|
|
if not addr.lstrip('0'):
|
|
continue # zero address — skip debug-section marker
|
|
if '\t' not in line:
|
|
continue
|
|
loc = line.rsplit('\t', 1)[1]
|
|
# Strip trailing :lineno (e.g. "/path/to/foo.cpp:42" → "/path/to/foo.cpp")
|
|
src_path = Path(loc.rsplit(':', 1)[0])
|
|
# Path.is_relative_to() handles OS-specific separators correctly without
|
|
# any regex, avoiding Windows path escaping issues.
|
|
for src_dir in list(remaining):
|
|
if src_path.is_relative_to(src_dir):
|
|
found.add(remaining.pop(src_dir))
|
|
break
|
|
|
|
return found
|
|
|
|
|
|
DYNARRAY_SECTION = ".dtors" if env.get("PIOPLATFORM") == "espressif8266" else ".dynarray"
|
|
USERMODS_SECTION = f"{DYNARRAY_SECTION}.usermods.1"
|
|
|
|
def count_usermod_objects(map_file: list[str]) -> int:
|
|
""" Returns the number of usermod objects in the usermod list """
|
|
# Count the number of entries in the usermods table section
|
|
return len([x for x in map_file if USERMODS_SECTION in x])
|
|
|
|
|
|
def validate_map_file(source, target, env):
|
|
""" Validate that all modules appear in the output build """
|
|
build_dir = Path(env.subst("$BUILD_DIR"))
|
|
map_file_path = build_dir / env.subst("${PROGNAME}.map")
|
|
|
|
if not map_file_path.exists():
|
|
secho(f"ERROR: Map file not found: {map_file_path}", fg="red", err=True)
|
|
Exit(1)
|
|
|
|
# Identify the WLED module builders, set by load_usermods.py
|
|
module_lib_builders = env['WLED_MODULES']
|
|
|
|
# Extract the values we care about
|
|
modules = {Path(builder.build_dir).name: builder.name for builder in module_lib_builders}
|
|
secho(f"INFO: {len(modules)} libraries linked as WLED optional/user modules")
|
|
|
|
# Now parse the map file
|
|
map_file_contents = read_lines(map_file_path)
|
|
usermod_object_count = count_usermod_objects(map_file_contents)
|
|
secho(f"INFO: {usermod_object_count} usermod object entries")
|
|
|
|
elf_path = build_dir / env.subst("${PROGNAME}.elf")
|
|
confirmed_modules = check_elf_modules(elf_path, env, module_lib_builders)
|
|
|
|
missing_modules = [modname for mdir, modname in modules.items() if mdir not in confirmed_modules]
|
|
if missing_modules:
|
|
secho(
|
|
f"ERROR: No symbols from {missing_modules} found in linked output!",
|
|
fg="red",
|
|
err=True)
|
|
Exit(1)
|
|
return None
|
|
|
|
env.Append(LINKFLAGS=[env.subst("-Wl,--Map=${BUILD_DIR}/${PROGNAME}.map")])
|
|
env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", Action(validate_map_file, cmdstr='Checking linked optional modules (usermods) in map file'))
|