Skip to content

Commit 603322a

Browse files
committed
Merge branch 'mdev' into copilot/verify-wokwi-ci-integration
2 parents e2abadc + f9c7828 commit 603322a

33 files changed

+5918
-4035
lines changed

package-lock.json

Lines changed: 457 additions & 3640 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
{
22
"name": "wled",
33
"version": "14.7.0-mdev",
4-
"description": "Tools for WLED project",
4+
"description": "Tools for WLED-MM project",
55
"main": "tools/cdata.js",
66
"directories": {
77
"lib": "lib",
88
"test": "test"
99
},
1010
"scripts": {
1111
"build": "node tools/cdata.js",
12-
"dev": "nodemon -e js,html,htm,css,png,jpg,gif,ico,js -w tools/ -w wled00/data/ -x node tools/cdata.js",
13-
"test": "npm run test:cdata",
14-
"test:cdata": "node tools/wokwi-test.js",
12+
"test": "node --test",
13+
"dev": "nodemon -e js,html,htm,css,png,jpg,gif,ico,js -w tools/ -w wled00/data/ -x node tools/cdata.js"
14+
"test:cdata": "node tools/cdata-test.js",
15+
"test:wokwi-cdata": "node tools/wokwi-test.js",
1516
"test:wokwi": "playwright test test/playwright/wokwi-basic.spec.js"
1617
},
1718
"repository": {
@@ -25,13 +26,13 @@
2526
},
2627
"homepage": "https://github.com/MoonModules/WLED-MM#readme",
2728
"dependencies": {
28-
"clean-css": "^4.2.3",
29-
"html-minifier-terser": "^5.1.1",
30-
"inliner": "^1.13.1",
31-
"nodemon": "^2.0.20",
32-
"zlib": "^1.0.5"
33-
},
34-
"devDependencies": {
29+
"clean-css": "^5.3.3",
30+
"html-minifier-terser": "^7.2.0",
31+
"web-resource-inliner": "^7.0.0",
32+
"nodemon": "^3.1.9",
3533
"@playwright/test": "^1.40.0"
34+
},
35+
"engines": {
36+
"node": ">=20.0.0"
3637
}
3738
}

pio-scripts/build_ui.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
Import("env")
2+
import shutil
3+
4+
node_ex = shutil.which("node")
5+
# Check if Node.js is installed and present in PATH if it failed, abort the build
6+
if node_ex is None:
7+
print('\x1b[0;31;43m' + 'Node.js is not installed or missing from PATH html css js will not be processed check https://kno.wled.ge/advanced/compiling-wled/' + '\x1b[0m')
8+
exitCode = env.Execute("null")
9+
exit(exitCode)
10+
else:
11+
# Install the necessary node packages for the pre-build asset bundling script
12+
print('\x1b[6;33;42m' + 'Installing node packages' + '\x1b[0m')
13+
env.Execute("npm ci")
14+
15+
# Call the bundling script
16+
exitCode = env.Execute("npm run build")
17+
18+
# If it failed, abort the build
19+
if (exitCode):
20+
print('\x1b[0;31;43m' + 'npm run build fails check https://kno.wled.ge/advanced/compiling-wled/' + '\x1b[0m')
21+
exit(exitCode)

pio-scripts/set_metadata.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
Import('env')
2+
import subprocess
3+
import json
4+
import re
5+
6+
def get_github_repo():
7+
"""Extract GitHub repository name from git remote URL.
8+
9+
Uses the remote that the current branch tracks, falling back to 'origin'.
10+
This handles cases where repositories have multiple remotes or where the
11+
main remote is not named 'origin'.
12+
13+
Returns:
14+
str: Repository name in 'owner/repo' format for GitHub repos,
15+
'unknown' for non-GitHub repos, missing git CLI, or any errors.
16+
"""
17+
try:
18+
remote_name = 'origin' # Default fallback
19+
20+
# Try to get the remote for the current branch
21+
try:
22+
# Get current branch name
23+
branch_result = subprocess.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
24+
capture_output=True, text=True, check=True)
25+
current_branch = branch_result.stdout.strip()
26+
27+
# Get the remote for the current branch
28+
remote_result = subprocess.run(['git', 'config', f'branch.{current_branch}.remote'],
29+
capture_output=True, text=True, check=True)
30+
tracked_remote = remote_result.stdout.strip()
31+
32+
# Use the tracked remote if we found one
33+
if tracked_remote:
34+
remote_name = tracked_remote
35+
except subprocess.CalledProcessError:
36+
# If branch config lookup fails, continue with 'origin' as fallback
37+
pass
38+
39+
# Get the remote URL for the determined remote
40+
result = subprocess.run(['git', 'remote', 'get-url', remote_name],
41+
capture_output=True, text=True, check=True)
42+
remote_url = result.stdout.strip()
43+
44+
# Check if it's a GitHub URL
45+
if 'github.com' not in remote_url.lower():
46+
return None
47+
48+
# Parse GitHub URL patterns:
49+
# https://github.com/owner/repo.git
50+
# [email protected]:owner/repo.git
51+
# https://github.com/owner/repo
52+
53+
# Remove .git suffix if present
54+
if remote_url.endswith('.git'):
55+
remote_url = remote_url[:-4]
56+
57+
# Handle HTTPS URLs
58+
https_match = re.search(r'github\.com/([^/]+/[^/]+)', remote_url, re.IGNORECASE)
59+
if https_match:
60+
return https_match.group(1)
61+
62+
# Handle SSH URLs
63+
ssh_match = re.search(r'github\.com:([^/]+/[^/]+)', remote_url, re.IGNORECASE)
64+
if ssh_match:
65+
return ssh_match.group(1)
66+
67+
return None
68+
69+
except FileNotFoundError:
70+
# Git CLI is not installed or not in PATH
71+
return None
72+
except subprocess.CalledProcessError:
73+
# Git command failed (e.g., not a git repo, no remote, etc.)
74+
return None
75+
except Exception:
76+
# Any other unexpected error
77+
return None
78+
79+
# WLED version is managed by package.json; this is picked up in several places
80+
# - It's integrated in to the UI code
81+
# - Here, for wled_metadata.cpp
82+
# - The output_bins script
83+
# We always take it from package.json to ensure consistency
84+
with open("package.json", "r") as package:
85+
WLED_VERSION = json.load(package)["version"]
86+
87+
def has_def(cppdefs, name):
88+
""" Returns true if a given name is set in a CPPDEFINES collection """
89+
for f in cppdefs:
90+
if isinstance(f, tuple):
91+
f = f[0]
92+
if f == name:
93+
return True
94+
return False
95+
96+
97+
def add_wled_metadata_flags(env, node):
98+
cdefs = env["CPPDEFINES"].copy()
99+
100+
if not has_def(cdefs, "WLED_REPO"):
101+
repo = get_github_repo()
102+
if repo:
103+
cdefs.append(("WLED_REPO", f"\\\"{repo}\\\""))
104+
105+
cdefs.append(("WLED_VERSION", WLED_VERSION))
106+
107+
# This transforms the node in to a Builder; it cannot be modified again
108+
return env.Object(
109+
node,
110+
CPPDEFINES=cdefs
111+
)
112+
113+
env.AddBuildMiddleware(
114+
add_wled_metadata_flags,
115+
"*/wled_metadata.cpp"
116+
)

pio-scripts/validate_modules.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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 builders, set by load_usermods.py
57+
module_lib_builders = env['WLED_MODULES']
58+
59+
# Extract the values we care about
60+
modules = {Path(builder.build_dir).name: builder.name for builder in module_lib_builders}
61+
secho(f"INFO: {len(modules)} libraries linked as WLED optional/user modules")
62+
63+
# Now parse the map file
64+
map_file_contents = read_lines(map_file_path)
65+
usermod_object_count = count_usermod_objects(map_file_contents)
66+
secho(f"INFO: {usermod_object_count} usermod object entries")
67+
68+
confirmed_modules = check_map_file_objects(map_file_contents, modules.keys())
69+
missing_modules = [modname for mdir, modname in modules.items() if mdir not in confirmed_modules]
70+
if missing_modules:
71+
secho(
72+
f"ERROR: No object files from {missing_modules} found in linked output!",
73+
fg="red",
74+
err=True)
75+
Exit(1)
76+
return None
77+
78+
Import("env")
79+
env.Append(LINKFLAGS=[env.subst("-Wl,--Map=${BUILD_DIR}/${PROGNAME}.map")])
80+
env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", Action(validate_map_file, cmdstr='Checking linked optional modules (usermods) in map file'))

0 commit comments

Comments
 (0)