Skip to content

Commit b79916c

Browse files
committed
feat(docs): add Python script for building Arduino examples and update CI workflow
1 parent b20655a commit b79916c

File tree

6 files changed

+673
-16
lines changed

6 files changed

+673
-16
lines changed
Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Python port of .github/scripts/docs_build_examples.sh
4+
Preserves behavior and CLI options of the original bash script.
5+
6+
Usage: docs_build_examples.py -ai <arduino_cli_path> -au <arduino_user_path> [options]
7+
"""
8+
9+
import argparse
10+
from argparse import RawDescriptionHelpFormatter
11+
import json
12+
import os
13+
import shutil
14+
import subprocess
15+
import sys
16+
from pathlib import Path
17+
18+
SCRIPT_DIR = Path(__file__).resolve().parent
19+
20+
# Determine SDKCONFIG_DIR like the shell script
21+
ARDUINO_ESP32_PATH = os.environ.get("ARDUINO_ESP32_PATH")
22+
GITHUB_WORKSPACE = os.environ.get("GITHUB_WORKSPACE")
23+
24+
if ARDUINO_ESP32_PATH and (Path(ARDUINO_ESP32_PATH) / "tools" / "esp32-arduino-libs").is_dir():
25+
SDKCONFIG_DIR = Path(ARDUINO_ESP32_PATH) / "tools" / "esp32-arduino-libs"
26+
elif GITHUB_WORKSPACE and (Path(GITHUB_WORKSPACE) / "tools" / "esp32-arduino-libs").is_dir():
27+
SDKCONFIG_DIR = Path(GITHUB_WORKSPACE) / "tools" / "esp32-arduino-libs"
28+
else:
29+
SDKCONFIG_DIR = Path("tools/esp32-arduino-libs")
30+
31+
# Wrapper functions to call sketch_utils.sh
32+
SKETCH_UTILS = SCRIPT_DIR / "sketch_utils.sh"
33+
34+
KEEP_FILES = ["*.merged.bin", "ci.json"]
35+
DOCS_BINARIES_DIR = Path("docs/_static/binaries")
36+
GENERATE_DIAGRAMS = False
37+
LAUNCHPAD_STORAGE_URL = ""
38+
39+
40+
def run_cmd(cmd, check=True, capture_output=False, text=True):
41+
try:
42+
return subprocess.run(cmd, check=check, capture_output=capture_output, text=text)
43+
except subprocess.CalledProcessError as e:
44+
# CalledProcessError is raised only when check=True and the command exits non-zero
45+
print(f"ERROR: Command failed: {' '.join(cmd)}")
46+
print(f"Exit code: {e.returncode}")
47+
if hasattr(e, 'stdout') and e.stdout:
48+
print("--- stdout ---")
49+
print(e.stdout)
50+
if hasattr(e, 'stderr') and e.stderr:
51+
print("--- stderr ---")
52+
print(e.stderr)
53+
# Exit the whole script with the same return code to mimic shell behavior
54+
sys.exit(e.returncode)
55+
except FileNotFoundError:
56+
print(f"ERROR: Command not found: {cmd[0]}")
57+
sys.exit(127)
58+
59+
60+
def check_requirements(sketch_dir, sdkconfig_path):
61+
# Call sketch_utils.sh check_requirements
62+
cmd = [str(SKETCH_UTILS), "check_requirements", sketch_dir, str(sdkconfig_path)]
63+
try:
64+
res = run_cmd(cmd, check=False, capture_output=True)
65+
return res.returncode == 0
66+
except Exception:
67+
return False
68+
69+
70+
def install_libs(*args):
71+
cmd = [str(SKETCH_UTILS), "install_libs"] + list(args)
72+
return run_cmd(cmd, check=False)
73+
74+
75+
def build_sketch(args_list):
76+
cmd = [str(SKETCH_UTILS), "build"] + args_list
77+
return run_cmd(cmd, check=False)
78+
79+
80+
def parse_args(argv):
81+
epilog_text = (
82+
"Example:\n"
83+
" docs_build_examples.py -ai /usr/local/bin -au ~/.arduino15 -d -l https://storage.example.com\n\n"
84+
"This script finds Arduino sketches that include a 'ci.json' with an 'upload-binary'\n"
85+
"section and builds binaries for the listed targets. The built outputs are placed\n"
86+
"under docs/_static/binaries/<sketch_path>/<target>/\n"
87+
)
88+
89+
p = argparse.ArgumentParser(
90+
description="Build examples that have ci.json with upload-binary targets",
91+
formatter_class=RawDescriptionHelpFormatter,
92+
epilog=epilog_text,
93+
)
94+
p.add_argument(
95+
"-ai",
96+
dest="arduino_cli_path",
97+
help=(
98+
"Path to Arduino CLI installation (directory containing the 'arduino-cli' binary)"
99+
),
100+
)
101+
p.add_argument(
102+
"-au",
103+
dest="user_path",
104+
help="Arduino user path (for example: ~/.arduino15)",
105+
)
106+
p.add_argument(
107+
"-c",
108+
dest="cleanup",
109+
action="store_true",
110+
help="Clean up docs binaries directory and exit",
111+
)
112+
p.add_argument(
113+
"-d",
114+
dest="generate_diagrams",
115+
action="store_true",
116+
help="Generate diagrams for built examples using docs-embed",
117+
)
118+
p.add_argument(
119+
"-l",
120+
dest="launchpad_storage_url",
121+
help="LaunchPad storage URL to include in generated config",
122+
)
123+
return p.parse_args(argv)
124+
125+
126+
def validate_prerequisites(args):
127+
if not args.arduino_cli_path:
128+
print("ERROR: Arduino CLI path not provided (-ai option required)")
129+
sys.exit(1)
130+
if not args.user_path:
131+
print("ERROR: Arduino user path not provided (-au option required)")
132+
sys.exit(1)
133+
arduino_cli_exe = Path(args.arduino_cli_path) / "arduino-cli"
134+
if not arduino_cli_exe.exists():
135+
print(f"ERROR: arduino-cli not found at {arduino_cli_exe}")
136+
sys.exit(1)
137+
if not Path(args.user_path).is_dir():
138+
print(f"ERROR: Arduino user path does not exist: {args.user_path}")
139+
sys.exit(1)
140+
141+
142+
def cleanup_binaries():
143+
print(f"Cleaning up binaries directory: {DOCS_BINARIES_DIR}")
144+
if not DOCS_BINARIES_DIR.exists():
145+
print("Binaries directory does not exist, nothing to clean")
146+
return
147+
for root, dirs, files in os.walk(DOCS_BINARIES_DIR):
148+
for fname in files:
149+
fpath = Path(root) / fname
150+
parent = Path(root).name
151+
# Always remove sketch/ci.json
152+
if parent == "sketch" and fname == "ci.json":
153+
fpath.unlink()
154+
continue
155+
keep = False
156+
for pattern in KEEP_FILES:
157+
if Path(fname).match(pattern):
158+
keep = True
159+
break
160+
if not keep:
161+
print(f"Removing: {fpath}")
162+
fpath.unlink()
163+
else:
164+
print(f"Keeping: {fpath}")
165+
# remove empty dirs
166+
for root, dirs, files in os.walk(DOCS_BINARIES_DIR, topdown=False):
167+
if not os.listdir(root):
168+
try:
169+
os.rmdir(root)
170+
except Exception:
171+
pass
172+
print("Cleanup completed")
173+
174+
175+
def find_examples_with_upload_binary():
176+
res = []
177+
for ino in Path('.').rglob('*.ino'):
178+
sketch_dir = ino.parent
179+
sketch_name = ino.stem
180+
dir_name = sketch_dir.name
181+
if dir_name != sketch_name:
182+
continue
183+
ci_json = sketch_dir / 'ci.json'
184+
if ci_json.exists():
185+
try:
186+
data = json.loads(ci_json.read_text())
187+
if 'upload-binary' in data and data['upload-binary']:
188+
res.append(str(ino))
189+
except Exception:
190+
continue
191+
return res
192+
193+
194+
def get_upload_binary_targets(sketch_dir):
195+
ci_json = Path(sketch_dir) / 'ci.json'
196+
try:
197+
data = json.loads(ci_json.read_text())
198+
targets = data.get('upload-binary', {}).get('targets', [])
199+
return targets
200+
except Exception:
201+
return []
202+
203+
204+
def build_example_for_target(sketch_dir, target, relative_path, args):
205+
print(f"\n > Building example: {relative_path} for target: {target}")
206+
output_dir = DOCS_BINARIES_DIR / relative_path / target
207+
output_dir.mkdir(parents=True, exist_ok=True)
208+
209+
sdkconfig = SDKCONFIG_DIR / target / 'sdkconfig'
210+
if not check_requirements(str(sketch_dir), sdkconfig):
211+
print(f"Target {target} does not meet the requirements for {Path(sketch_dir).name}. Skipping.")
212+
return True
213+
214+
# Build the sketch using sketch_utils.sh build - pass args as in shell script
215+
build_args = [
216+
"-ai",
217+
args.arduino_cli_path,
218+
"-au",
219+
args.user_path,
220+
"-s",
221+
str(sketch_dir),
222+
"-t",
223+
target,
224+
"-b",
225+
str(output_dir),
226+
"--first-only",
227+
]
228+
res = build_sketch(build_args)
229+
if res.returncode == 0:
230+
print(f"Successfully built {relative_path} for {target}")
231+
ci_json = Path(sketch_dir) / 'ci.json'
232+
if ci_json.exists():
233+
shutil.copy(ci_json, output_dir / 'ci.json')
234+
if GENERATE_DIAGRAMS:
235+
print(f"Generating diagram for {relative_path} ({target})...")
236+
rc = run_cmd(
237+
[
238+
"docs-embed",
239+
"--path",
240+
str(output_dir),
241+
"diagram-from-ci",
242+
"--platform",
243+
target,
244+
"--override",
245+
],
246+
check=False,
247+
)
248+
if rc.returncode == 0:
249+
print("Diagram generated successfully for {relative_path} ({target})")
250+
else:
251+
print("WARNING: Failed to generate diagram for {relative_path} ({target})")
252+
if LAUNCHPAD_STORAGE_URL:
253+
print(f"Generating LaunchPad config for {relative_path} ({target})...")
254+
rc = run_cmd(
255+
[
256+
"docs-embed",
257+
"--path",
258+
str(output_dir),
259+
"launchpad-config",
260+
LAUNCHPAD_STORAGE_URL,
261+
"--repo-url-prefix",
262+
"https://github.com/espressif/arduino-esp32/tree/master",
263+
"--override",
264+
],
265+
check=False,
266+
)
267+
if rc.returncode == 0:
268+
print("LaunchPad config generated successfully for {relative_path} ({target})")
269+
else:
270+
print("WARNING: Failed to generate LaunchPad config for {relative_path} ({target})")
271+
return True
272+
else:
273+
print(f"ERROR: Failed to build {relative_path} for {target}")
274+
return False
275+
276+
277+
def build_all_examples(args):
278+
total_built = 0
279+
total_failed = 0
280+
281+
if DOCS_BINARIES_DIR.exists():
282+
shutil.rmtree(DOCS_BINARIES_DIR)
283+
print(f"Removed existing build directory: {DOCS_BINARIES_DIR}")
284+
285+
examples = find_examples_with_upload_binary()
286+
if not examples:
287+
print("No examples found with upload-binary configuration")
288+
return 0
289+
290+
print('\nExamples to be built:')
291+
print('====================')
292+
for i, example in enumerate(examples, start=1):
293+
sketch_dir = Path(example).parent
294+
relative_path = str(sketch_dir).lstrip('./')
295+
targets = get_upload_binary_targets(sketch_dir)
296+
if targets:
297+
print(f"{i}. {relative_path} (targets: {' '.join(targets)})")
298+
print()
299+
300+
for example in examples:
301+
sketch_dir = Path(example).parent
302+
relative_path = str(sketch_dir).lstrip('./')
303+
targets = get_upload_binary_targets(sketch_dir)
304+
if not targets:
305+
print(f"WARNING: No targets found for {relative_path}")
306+
continue
307+
print(f"Building {relative_path} for targets: {targets}")
308+
for target in targets:
309+
ok = build_example_for_target(sketch_dir, target, relative_path, args)
310+
if ok:
311+
total_built += 1
312+
else:
313+
total_failed += 1
314+
315+
print('\nBuild summary:')
316+
print(f" Successfully built: {total_built}")
317+
print(f" Failed builds: {total_failed}")
318+
print(f" Output directory: {DOCS_BINARIES_DIR}")
319+
return total_failed
320+
321+
322+
def main(argv):
323+
global GENERATE_DIAGRAMS, LAUNCHPAD_STORAGE_URL
324+
args = parse_args(argv)
325+
if args.cleanup:
326+
cleanup_binaries()
327+
return
328+
validate_prerequisites(args)
329+
GENERATE_DIAGRAMS = args.generate_diagrams
330+
LAUNCHPAD_STORAGE_URL = args.launchpad_storage_url or ""
331+
DOCS_BINARIES_DIR.mkdir(parents=True, exist_ok=True)
332+
result = build_all_examples(args)
333+
if result == 0:
334+
print('\nAll examples built successfully!')
335+
else:
336+
print('\nSome builds failed. Check the output above for details.')
337+
sys.exit(1)
338+
339+
340+
if __name__ == '__main__':
341+
main(sys.argv[1:])

0 commit comments

Comments
 (0)