Skip to content

Commit f410b40

Browse files
Merge pull request #15 from JakubAndrysek/wokwi-embed-launchpad
2 parents b20655a + 0c98e04 commit f410b40

File tree

9 files changed

+858
-25
lines changed

9 files changed

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

.github/scripts/on-push.sh

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
set -e
44

5-
export ARDUINO_BUILD_DIR="$HOME/.arduino/build.tmp"
6-
75
function build {
86
local target=$1
97
local chunk_index=$2

0 commit comments

Comments
 (0)