Skip to content

Commit 309c8d6

Browse files
committed
Generalize module link validation
Perform validation for external modules, too.
1 parent 75c95d8 commit 309c8d6

File tree

4 files changed

+96
-99
lines changed

4 files changed

+96
-99
lines changed

pio-scripts/load_usermods.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,6 @@ def is_wled_module(dep: LibBuilderBase) -> bool:
3939
# Handle "all usermods" case
4040
if usermods == '*':
4141
usermods = [f.name for f in usermod_dir.iterdir() if f.is_dir() and f.joinpath('library.json').exists()]
42-
# Update the environment, as many modules use scripts to detect their dependencies
43-
env.GetProjectConfig().set("env:" + env['PIOENV'], 'custom_usermods', " ".join(usermods))
44-
# Leave a note for the validation script
45-
env.GetProjectConfig().set("env:" + env['PIOENV'], 'custom_all_usermods_enabled', "1")
4642
else:
4743
usermods = usermods.split()
4844

pio-scripts/validate_modules.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import re
2+
import sys
3+
from pathlib import Path # For OS-agnostic path manipulation
4+
from typing import Iterable
5+
from click import secho
6+
from SCons.Script import Action, Exit
7+
from platformio import util
8+
from platformio.builder.tools.piolib import LibBuilderBase
9+
10+
11+
def is_wled_module(env, dep: LibBuilderBase) -> bool:
12+
"""Returns true if the specified library is a wled module
13+
"""
14+
usermod_dir = Path(env["PROJECT_DIR"]).resolve() / "usermods"
15+
return usermod_dir in Path(dep.src_dir).parents or str(dep.name).startswith("wled-")
16+
17+
18+
def read_lines(p: Path):
19+
""" Read in the contents of a file for analysis """
20+
with p.open("r", encoding="utf-8", errors="ignore") as f:
21+
return f.readlines()
22+
23+
24+
def check_map_file_objects(map_file: list[str], dirs: Iterable[str]) -> set[str]:
25+
""" Identify which dirs contributed to the final build
26+
27+
Returns the (sub)set of dirs that are found in the output ELF
28+
"""
29+
# Pattern to match symbols in object directories
30+
# Join directories into alternation
31+
usermod_dir_regex = "|".join([re.escape(dir) for dir in dirs])
32+
# Matches nonzero address, any size, and any path in a matching directory
33+
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")
34+
35+
found = set()
36+
for line in map_file:
37+
matches = object_path_regex.findall(line)
38+
for m in matches:
39+
found.add(m)
40+
return found
41+
42+
43+
def count_usermod_objects(map_file: list[str]) -> int:
44+
""" Returns the number of usermod objects in the usermod list """
45+
# Count the number of entries in the usermods table section
46+
return len([x for x in map_file if ".dtors.tbl.usermods.1" in x])
47+
48+
49+
def validate_map_file(source, target, env):
50+
""" Validate that all modules appear in the output build """
51+
build_dir = Path(env.subst("$BUILD_DIR"))
52+
map_file_path = build_dir / env.subst("${PROGNAME}.map")
53+
54+
if not map_file_path.exists():
55+
secho(f"ERROR: Map file not found: {map_file_path}", fg="red", err=True)
56+
Exit(1)
57+
58+
# Identify the WLED module source directories
59+
module_lib_builders = [builder for builder in env.GetLibBuilders() if is_wled_module(env, builder)]
60+
61+
if env.GetProjectOption("custom_usermods","") == "*":
62+
# All usermods build; filter non-platform-OK modules
63+
module_lib_builders = [builder for builder in module_lib_builders if env.IsCompatibleLibBuilder(builder)]
64+
else:
65+
incompatible_builders = [builder for builder in module_lib_builders if not env.IsCompatibleLibBuilder(builder)]
66+
if incompatible_builders:
67+
secho(
68+
f"ERROR: Modules {[b.name for b in incompatible_builders]} are not compatible with this platform!",
69+
fg="red",
70+
err=True)
71+
Exit(1)
72+
pass
73+
74+
# Extract the values we care about
75+
modules = {Path(builder.build_dir).name: builder.name for builder in module_lib_builders}
76+
secho(f"INFO: {len(modules)} libraries linked as WLED optional/user modules")
77+
78+
# Now parse the map file
79+
map_file_contents = read_lines(map_file_path)
80+
usermod_object_count = count_usermod_objects(map_file_contents)
81+
secho(f"INFO: {usermod_object_count} usermod object entries")
82+
83+
confirmed_modules = check_map_file_objects(map_file_contents, modules.keys())
84+
missing_modules = [modname for mdir, modname in modules.items() if mdir not in confirmed_modules]
85+
if missing_modules:
86+
secho(
87+
f"ERROR: No object files from {missing_modules} found in linked output!",
88+
fg="red",
89+
err=True)
90+
Exit(1)
91+
return None
92+
93+
Import("env")
94+
env.Append(LINKFLAGS=[env.subst("-Wl,--Map=${BUILD_DIR}/${PROGNAME}.map")])
95+
env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", Action(validate_map_file, cmdstr='Checking linked optional modules (usermods) in map file'))

pio-scripts/validate_usermods.py

Lines changed: 0 additions & 94 deletions
This file was deleted.

platformio.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ extra_scripts =
116116
pre:pio-scripts/user_config_copy.py
117117
pre:pio-scripts/load_usermods.py
118118
pre:pio-scripts/build_ui.py
119-
post:pio-scripts/validate_usermods.py ;; double-check the build output usermods
119+
post:pio-scripts/validate_modules.py ;; double-check the build output usermods
120120
; post:pio-scripts/obj-dump.py ;; convenience script to create a disassembly dump of the firmware (hardcore debugging)
121121

122122
# ------------------------------------------------------------------------------

0 commit comments

Comments
 (0)