1+ import os
12import re
3+ import subprocess
24from pathlib import Path # For OS-agnostic path manipulation
3- from typing import Iterable
45from click import secho
56from SCons .Script import Action , Exit
67Import ("env" )
@@ -12,24 +13,64 @@ def read_lines(p: Path):
1213 return f .readlines ()
1314
1415
15- def check_map_file_objects (map_file : list [str ], dirs : Iterable [str ]) -> set [str ]:
16- """ Identify which dirs contributed to the final build
16+ def _get_nm_path (env ) -> str :
17+ """ Derive the nm tool path from the build environment """
18+ if "NM" in env :
19+ return env .subst ("$NM" )
20+ # Derive from the C compiler: xtensa-esp32-elf-gcc → xtensa-esp32-elf-nm
21+ cc = env .subst ("$CC" )
22+ nm = re .sub (r'(gcc|g\+\+)$' , 'nm' , os .path .basename (cc ))
23+ return os .path .join (os .path .dirname (cc ), nm )
1724
18- Returns the (sub)set of dirs that are found in the output ELF
25+
26+ def check_elf_modules (elf_path : Path , env , module_lib_builders ) -> set [str ]:
27+ """ Check which modules have at least one defined symbol placed in the ELF.
28+
29+ The map file is not a reliable source for this: with LTO, original object
30+ file paths are replaced by temporary ltrans.o partitions in all output
31+ sections, making per-module attribution impossible from the map alone.
32+ Instead we invoke nm --defined-only -l on the ELF, which uses DWARF debug
33+ info to attribute each placed symbol to its original source file.
34+
35+ Requires usermod libraries to be compiled with -g so that DWARF sections
36+ are present in the ELF. load_usermods.py injects -g for all WLED modules
37+ via dep.env.AppendUnique(CCFLAGS=["-g"]).
38+
39+ Returns the set of build_dir basenames for confirmed modules.
1940 """
20- # Pattern to match symbols in object directories
21- # Join directories into alternation
22- usermod_dir_regex = "|" .join ([re .escape (dir ) for dir in dirs ])
23- # Matches nonzero address, any size, and any path in a matching directory
24- object_path_regex = re .compile (r"0x0*[1-9a-f][0-9a-f]*\s+0x[0-9a-f]+\s+\S+[/\\](" + usermod_dir_regex + r")[/\\]\S+\.o" )
41+ nm_path = _get_nm_path (env )
42+ try :
43+ result = subprocess .run (
44+ [nm_path , "--defined-only" , "-l" , str (elf_path )],
45+ capture_output = True , text = True , errors = "ignore" , timeout = 120 ,
46+ )
47+ nm_output = result .stdout
48+ except (subprocess .TimeoutExpired , FileNotFoundError , OSError ) as e :
49+ secho (f"WARNING: nm failed ({ e } ); skipping per-module validation" , fg = "yellow" , err = True )
50+ return {Path (b .build_dir ).name for b in module_lib_builders } # conservative pass
51+
52+ # Build a filtered set of lines that have a nonzero address.
53+ # nm --defined-only still includes debugging symbols (type 'N') such as the
54+ # per-CU markers GCC emits in .debug_info (e.g. "usermod_example_cpp_6734d48d").
55+ # These live at address 0x00000000 in their debug section — not in any load
56+ # segment — so filtering them out leaves only genuinely placed symbols.
57+ placed_lines = [
58+ line for line in nm_output .splitlines ()
59+ if (parts := line .split (None , 1 )) and parts [0 ].lstrip ('0' )
60+ ]
61+ placed_output = "\n " .join (placed_lines )
2562
2663 found = set ()
27- for line in map_file :
28- matches = object_path_regex .findall (line )
29- for m in matches :
30- found .add (m )
64+ for builder in module_lib_builders :
65+ # builder.src_dir is the library source directory (used by is_wled_module() too)
66+ src_dir = str (builder .src_dir ).rstrip ("/\\ " )
67+ # Guard against prefix collisions (e.g. /path/to/mod vs /path/to/mod-extra)
68+ # by requiring a path separator immediately after the directory name.
69+ if re .search (re .escape (src_dir ) + r'[/\\]' , placed_output ):
70+ found .add (Path (builder .build_dir ).name )
3171 return found
3272
73+
3374DYNARRAY_SECTION = ".dtors" if env .get ("PIOPLATFORM" ) == "espressif8266" else ".dynarray"
3475USERMODS_SECTION = f"{ DYNARRAY_SECTION } .usermods.1"
3576
@@ -60,11 +101,13 @@ def validate_map_file(source, target, env):
60101 usermod_object_count = count_usermod_objects (map_file_contents )
61102 secho (f"INFO: { usermod_object_count } usermod object entries" )
62103
63- confirmed_modules = check_map_file_objects (map_file_contents , modules .keys ())
104+ elf_path = build_dir / env .subst ("${PROGNAME}.elf" )
105+ confirmed_modules = check_elf_modules (elf_path , env , module_lib_builders )
106+
64107 missing_modules = [modname for mdir , modname in modules .items () if mdir not in confirmed_modules ]
65108 if missing_modules :
66109 secho (
67- f"ERROR: No object files from { missing_modules } found in linked output!" ,
110+ f"ERROR: No symbols from { missing_modules } found in linked output!" ,
68111 fg = "red" ,
69112 err = True )
70113 Exit (1 )
0 commit comments