Skip to content

Commit ed7bb36

Browse files
committed
Add a nightly matrix for the ESP-IDF component
nightly-espressif-idf.yml runs the component across every target, ESP-IDF release and configuration that the per-PR jobs only sample. A prepare job generates and packs the component into a file:// mirror and exports the smoke, esp_nn:: mul, ESP-NN delegate and Cadence wake word models with their kernel bundles, plus an all_ops bundle that registers every op of every kernel library. Twelve build jobs (ESP-IDF 6.0.1, 6.1 and master, four shards each) take that artifact and nothing else: they list the idf_component_consumer configurations with idf-build-apps (each sdkconfig.ci.* file for ESP32, -S3, -C3, -C5, -C6 and -P4, filtered by .build-test-rules.yml), build their shard from the mirror with ESP-IDF's own Python environment, check that every setting of the configuration reached the build's sdkconfig, since ESP-IDF only warns about an unknown symbol, and run each build on QEMU or esp-emu. Each job uploads its build and emulator logs. The configurations are default, nolog (logging compiled out), perf (ExecuTorch built without -Os, and the project for speed), debug (debug assertions and full program verification), wakeword, delegate, oplib, and all_ops on ESP32-S3 and -C6 with an 8 MB flash layout, so every vendored kernel compiles under ESP-IDF's flags and every registration resolves when the image links; plus a build-only ESP32-P4 configuration for silicon before rev3, which esp-emu does not run. The consumer takes the model directory as ET_CONSUMER_MODELS_DIR, from which CONFIG_ET_CONSUMER_MODEL picks the .pte and bundle. It has its own sdkconfig.defaults instead of overriding SDKCONFIG_DEFAULTS. The script builds each listed configuration itself with its own sdkconfig, because idf-build-apps keeps one sdkconfig in the project directory, which the next build overwrites before the emulator run needs it, and runs idf-build-apps from a venv of its own so ESP-IDF's environment stays as ESP-IDF installs it. Jobs on macOS and Windows configure the two-model idf_component_smoke project with the prepared component, once with its bundle and once in automatic mode, and check that both select the same kernels. The registry validation (compote component upload --dry-run --allow-existing) runs here on the packed archive. setup.sh accepts an ESP-IDF branch such as master. The workflow also runs for the ciflow/nightly label and on pull requests that change what it runs, since workflow_dispatch only works from the default branch. Authored with Claude Code.
1 parent 610f909 commit ed7bb36

21 files changed

Lines changed: 626 additions & 12 deletions
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) Meta Platforms, Inc. and affiliates.
3+
# All rights reserved.
4+
#
5+
# This source code is licensed under the BSD-style license found in the
6+
# LICENSE file in the root directory of this source tree.
7+
8+
"""Configure the ESP-IDF component's two-model consumer on this host.
9+
10+
For the nightly macOS and Windows jobs. Installs ESP-IDF and its ESP32-S3 tools
11+
under --work-dir/<--idf-version> with ESP-IDF's own Python environment, then runs
12+
`idf.py reconfigure` on idf_component_smoke with the smoke and mul models and
13+
the component from test_espressif_idf_nightly.sh prepare: once with the kernel
14+
bundle prepare made, and once in automatic mode, where this Python (which must
15+
import executorch) makes the bundle. Both bundles must select the same kernels.
16+
"""
17+
18+
import argparse
19+
import json
20+
import os
21+
import subprocess
22+
import sys
23+
from pathlib import Path
24+
25+
ET_ROOT = Path(__file__).resolve().parents[2]
26+
27+
28+
def run(cmd, env):
29+
print("+", " ".join(str(c) for c in cmd), flush=True)
30+
subprocess.run([str(c) for c in cmd], env=env, check=True)
31+
32+
33+
def idf_environment(work, idf_version):
34+
idf_path = work / "esp-idf"
35+
env = dict(
36+
os.environ,
37+
IDF_PATH=str(idf_path),
38+
IDF_TOOLS_PATH=str(work / "idf-tools"),
39+
IDF_PYTHON_ENV_PATH=str(work / "idf_python_env"),
40+
)
41+
# windows_job runs under Git Bash, whose MSYSTEM makes idf_tools.py refuse to run.
42+
env.pop("MSYSTEM", None)
43+
if not idf_path.exists():
44+
run(
45+
["git", "-c", "core.longpaths=true", "clone", "--depth", "1"]
46+
+ ["--branch", idf_version, "--recurse-submodules", "--shallow-submodules"]
47+
+ ["https://github.com/espressif/esp-idf.git", idf_path],
48+
env,
49+
)
50+
idf_tools = [sys.executable, idf_path / "tools/idf_tools.py"]
51+
run(
52+
idf_tools + ["install", "--targets", "esp32s3", "required", "cmake", "ninja"],
53+
env,
54+
)
55+
# install-python-env fails if IDF_PYTHON_ENV_PATH doesn't exist yet.
56+
Path(env["IDF_PYTHON_ENV_PATH"]).mkdir(exist_ok=True)
57+
run(idf_tools + ["install-python-env"], env)
58+
exported = subprocess.run(
59+
[str(c) for c in idf_tools + ["export", "--format", "key-value"]],
60+
env=env,
61+
check=True,
62+
capture_output=True,
63+
text=True,
64+
).stdout
65+
for line in exported.splitlines():
66+
key, _, value = line.partition("=")
67+
if key == "PATH":
68+
value = value.replace("%PATH%", env["PATH"]).replace("$PATH", env["PATH"])
69+
if key:
70+
env[key] = value
71+
scripts = "Scripts/python.exe" if os.name == "nt" else "bin/python"
72+
return env, Path(env["IDF_PYTHON_ENV_PATH"]) / scripts
73+
74+
75+
def main(argv):
76+
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
77+
parser.add_argument(
78+
"--inputs", type=Path, required=True, help="prepare's directory"
79+
)
80+
parser.add_argument("--work-dir", type=Path, required=True)
81+
parser.add_argument("--idf-version", default="v6.1")
82+
args = parser.parse_args(argv)
83+
work = args.work_dir.resolve() / args.idf_version
84+
work.mkdir(parents=True, exist_ok=True)
85+
inputs = args.inputs.resolve()
86+
env, idf_python = idf_environment(work, args.idf_version)
87+
env["IDF_TARGET"] = "esp32s3"
88+
89+
models = ";".join(
90+
(inputs / "models" / f"{m}.pte").as_posix() for m in ("smoke", "mul")
91+
)
92+
common = [
93+
idf_python,
94+
Path(env["IDF_PATH"]) / "tools/idf.py",
95+
"-C",
96+
ET_ROOT / "examples/espressif/idf_component_smoke",
97+
f"-DET_COMPONENT_DIR={(inputs / 'component/executorch').as_posix()}",
98+
f"-DET_SMOKE_MODELS={models}",
99+
]
100+
bundled = work / "bundled"
101+
run(
102+
common
103+
+ ["-B", bundled, f"-DSDKCONFIG={(bundled / 'sdkconfig').as_posix()}"]
104+
+ [f"-DET_SMOKE_KERNELS={(inputs / 'models/smoke_mul_kernels').as_posix()}"]
105+
+ ["reconfigure"],
106+
env,
107+
)
108+
automatic = work / "automatic"
109+
run(
110+
common
111+
+ ["-B", automatic, f"-DSDKCONFIG={(automatic / 'sdkconfig').as_posix()}"]
112+
+ [f"-DEXECUTORCH_PYTHON={Path(sys.executable).as_posix()}", "reconfigure"],
113+
env,
114+
)
115+
116+
def selection(bundle):
117+
kernels = json.loads((bundle / "kernels.json").read_text())
118+
return {k: kernels[k] for k in ("models", "libraries", "prim_ops")}
119+
120+
made = selection(automatic / "executorch_kernels")
121+
if made != selection(inputs / "models/smoke_mul_kernels"):
122+
sys.exit(f"FAIL: automatic mode selected {made}, unlike prepare's bundle")
123+
print(
124+
"PASS: the two-model consumer configured with a bundle and in automatic mode."
125+
)
126+
127+
128+
if __name__ == "__main__":
129+
main(sys.argv[1:])

‎.ci/scripts/test_espressif_idf_consumer.sh‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,7 @@ sources="${build_dir}/consumer sources"
126126
rm -rf "${sources}"
127127
mkdir -p "${sources}/idf_component_smoke"
128128
cp -r examples/espressif/idf_component_consumer "${sources}/"
129-
cp -r examples/espressif/idf_component_smoke/main \
130-
examples/espressif/idf_component_smoke/sdkconfig.defaults "${sources}/idf_component_smoke/"
129+
cp -r examples/espressif/idf_component_smoke/main "${sources}/idf_component_smoke/"
131130
project="${sources}/idf_component_consumer"
132131
firmware_root="${build_dir}/consumer builds"
133132

0 commit comments

Comments
 (0)