Skip to content

Commit 4a3af81

Browse files
authored
Merge pull request #4669 from willmmiles/4597-usermods-not-building
Correct issues with usermods not being linked. - Explicitly set libArchive: false in usermod library.json files - Fix up symlink path generation on Windows - Add validation script to report usermod linkage in resulting binary
2 parents db22936 + f362315 commit 4a3af81

File tree

55 files changed

+236
-97
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+236
-97
lines changed

pio-scripts/load_usermods.py

Lines changed: 43 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
Import('env')
2-
import os.path
32
from collections import deque
43
from pathlib import Path # For OS-agnostic path manipulation
5-
from platformio.package.manager.library import LibraryPackageManager
4+
from click import secho
5+
from SCons.Script import Exit
6+
from platformio.builder.tools.piolib import LibBuilderBase
67

7-
usermod_dir = Path(env["PROJECT_DIR"]) / "usermods"
8-
all_usermods = [f for f in usermod_dir.iterdir() if f.is_dir() and f.joinpath('library.json').exists()]
8+
usermod_dir = Path(env["PROJECT_DIR"]).resolve() / "usermods"
99

10-
if env['PIOENV'] == "usermods":
11-
# Add all usermods
12-
env.GetProjectConfig().set(f"env:usermods", 'custom_usermods', " ".join([f.name for f in all_usermods]))
13-
14-
def find_usermod(mod: str):
10+
# Utility functions
11+
def find_usermod(mod: str) -> Path:
1512
"""Locate this library in the usermods folder.
1613
We do this to avoid needing to rename a bunch of folders;
1714
this could be removed later
@@ -22,51 +19,36 @@ def find_usermod(mod: str):
2219
return mp
2320
mp = usermod_dir / f"{mod}_v2"
2421
if mp.exists():
25-
return mp
22+
return mp
2623
mp = usermod_dir / f"usermod_v2_{mod}"
2724
if mp.exists():
2825
return mp
2926
raise RuntimeError(f"Couldn't locate module {mod} in usermods directory!")
3027

28+
def is_wled_module(dep: LibBuilderBase) -> bool:
29+
"""Returns true if the specified library is a wled module
30+
"""
31+
return usermod_dir in Path(dep.src_dir).parents or str(dep.name).startswith("wled-")
32+
33+
## Script starts here
34+
# Process usermod option
3135
usermods = env.GetProjectOption("custom_usermods","")
36+
37+
# Handle "all usermods" case
38+
if usermods == '*':
39+
usermods = [f.name for f in usermod_dir.iterdir() if f.is_dir() and f.joinpath('library.json').exists()]
40+
else:
41+
usermods = usermods.split()
42+
3243
if usermods:
3344
# Inject usermods in to project lib_deps
34-
proj = env.GetProjectConfig()
35-
deps = env.GetProjectOption('lib_deps')
36-
src_dir = proj.get("platformio", "src_dir")
37-
src_dir = src_dir.replace('\\','/')
38-
mod_paths = {mod: find_usermod(mod) for mod in usermods.split()}
39-
usermods = [f"{mod} = symlink://{path}" for mod, path in mod_paths.items()]
40-
proj.set("env:" + env['PIOENV'], 'lib_deps', deps + usermods)
41-
# Force usermods to be installed in to the environment build state before the LDF runs
42-
# Otherwise we won't be able to see them until it's too late to change their paths for LDF
43-
# Logic is largely borrowed from PlaformIO internals
44-
not_found_specs = []
45-
for spec in usermods:
46-
found = False
47-
for storage_dir in env.GetLibSourceDirs():
48-
#print(f"Checking {storage_dir} for {spec}")
49-
lm = LibraryPackageManager(storage_dir)
50-
if lm.get_package(spec):
51-
#print("Found!")
52-
found = True
53-
break
54-
if not found:
55-
#print("Missing!")
56-
not_found_specs.append(spec)
57-
if not_found_specs:
58-
lm = LibraryPackageManager(
59-
env.subst(os.path.join("$PROJECT_LIBDEPS_DIR", "$PIOENV"))
60-
)
61-
for spec in not_found_specs:
62-
#print(f"LU: forcing install of {spec}")
63-
lm.install(spec)
64-
45+
symlinks = [f"symlink://{find_usermod(mod).resolve()}" for mod in usermods]
46+
env.GetProjectConfig().set("env:" + env['PIOENV'], 'lib_deps', env.GetProjectOption('lib_deps') + symlinks)
6547

6648
# Utility function for assembling usermod include paths
6749
def cached_add_includes(dep, dep_cache: set, includes: deque):
6850
""" Add dep's include paths to includes if it's not in the cache """
69-
if dep not in dep_cache:
51+
if dep not in dep_cache:
7052
dep_cache.add(dep)
7153
for include in dep.get_include_dirs():
7254
if include not in includes:
@@ -82,13 +64,6 @@ def cached_add_includes(dep, dep_cache: set, includes: deque):
8264

8365
# Our new wrapper
8466
def wrapped_ConfigureProjectLibBuilder(xenv):
85-
# Update usermod properties
86-
# Set libArchive before build actions are added
87-
for um in (um for um in xenv.GetLibBuilders() if usermod_dir in Path(um.src_dir).parents):
88-
build = um._manifest.get("build", {})
89-
build["libArchive"] = False
90-
um._manifest["build"] = build
91-
9267
# Call the wrapped function
9368
result = old_ConfigureProjectLibBuilder.clone(xenv)()
9469

@@ -102,12 +77,25 @@ def wrapped_ConfigureProjectLibBuilder(xenv):
10277
for dep in result.depbuilders:
10378
cached_add_includes(dep, processed_deps, extra_include_dirs)
10479

105-
for um in [dep for dep in result.depbuilders if usermod_dir in Path(dep.src_dir).parents]:
106-
# Add the wled folder to the include path
107-
um.env.PrependUnique(CPPPATH=wled_dir)
108-
# Add WLED's own dependencies
109-
for dir in extra_include_dirs:
110-
um.env.PrependUnique(CPPPATH=dir)
80+
broken_usermods = []
81+
for dep in result.depbuilders:
82+
if is_wled_module(dep):
83+
# Add the wled folder to the include path
84+
dep.env.PrependUnique(CPPPATH=str(wled_dir))
85+
# Add WLED's own dependencies
86+
for dir in extra_include_dirs:
87+
dep.env.PrependUnique(CPPPATH=str(dir))
88+
# Enforce that libArchive is not set; we must link them directly to the executable
89+
if dep.lib_archive:
90+
broken_usermods.append(dep)
91+
92+
if broken_usermods:
93+
broken_usermods = [usermod.name for usermod in broken_usermods]
94+
secho(
95+
f"ERROR: libArchive=false is missing on usermod(s) {' '.join(broken_usermods)} -- modules will not compile in correctly",
96+
fg="red",
97+
err=True)
98+
Exit(1)
11199

112100
return result
113101

pio-scripts/validate_modules.py

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

platformio.ini

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +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_modules.py ;; double-check the build output usermods
119120
; post:pio-scripts/obj-dump.py ;; convenience script to create a disassembly dump of the firmware (hardcore debugging)
120121

121122
# ------------------------------------------------------------------------------
@@ -659,5 +660,5 @@ build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_
659660
lib_deps = ${esp32_idf_V4.lib_deps}
660661
monitor_filters = esp32_exception_decoder
661662
board_build.flash_mode = dio
662-
; custom_usermods = *every folder with library.json* -- injected by pio-scripts/load_usermods.py
663+
custom_usermods = * ; Expands to all usermods in usermods folder
663664
board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat

usermods/ADS1115_v2/library.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"name": "ADS1115_v2",
3+
"build": { "libArchive": false },
34
"dependencies": {
45
"Adafruit BusIO": "https://github.com/adafruit/Adafruit_BusIO#1.13.2",
56
"Adafruit ADS1X15": "https://github.com/adafruit/Adafruit_ADS1X15#2.4.0"

usermods/AHT10_v2/library.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"name": "AHT10_v2",
3+
"build": { "libArchive": false },
34
"dependencies": {
45
"enjoyneering/AHT10":"~1.1.0"
56
}

usermods/Analog_Clock/library.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
{
2-
"name": "Analog_Clock"
2+
"name": "Analog_Clock",
3+
"build": { "libArchive": false }
34
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
{
2-
"name": "Animated_Staircase"
2+
"name": "Animated_Staircase",
3+
"build": { "libArchive": false }
34
}

usermods/BH1750_v2/library.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"name": "BH1750_v2",
3+
"build": { "libArchive": false },
34
"dependencies": {
45
"claws/BH1750":"^1.2.0"
56
}

usermods/BME280_v2/library.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"name": "BME280_v2",
3+
"build": { "libArchive": false },
34
"dependencies": {
45
"finitespace/BME280":"~3.0.0"
56
}

usermods/BME68X_v2/library.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"name": "BME68X",
3+
"build": { "libArchive": false },
34
"dependencies": {
45
"boschsensortec/BSEC Software Library":"^1.8.1492"
56
}

0 commit comments

Comments
 (0)