1111ci.yml files and builds them for specified targets.
1212"""
1313
14- import argparse
15- from argparse import RawDescriptionHelpFormatter
14+ import click
1615from esp_docs .esp_extensions .docs_embed .tool .wokwi_tool import DiagramSync
1716import os
1817import shutil
7069SKETCH_UTILS = SCRIPT_DIR / "sketch_utils.sh"
7170
7271
73- def run_sketch_utils (subcommand , * args , check = True , capture_output = False , text = True ):
74- """Execute sketch_utils.sh with a controlled subcommand.
75-
76- Security:
77- * Uses a fixed script path (SKETCH_UTILS)
78- * Only allows known subcommands
79- * Always calls subprocess.run with shell=False and a list of arguments
80- """
81- allowed_subcommands = {"check_requirements" , "install_libs" , "build" }
82- if subcommand not in allowed_subcommands :
83- raise ValueError (f"Unsupported sketch_utils.sh subcommand: { subcommand } " )
84-
85- cmd = [str (SKETCH_UTILS ), subcommand ]
86- for arg in args :
87- if not isinstance (arg , (str , os .PathLike )):
88- raise ValueError (f"Invalid argument type for sketch_utils.sh: { type (arg )!r} " )
89- cmd .append (str (arg ))
90-
72+ def run_cmd (cmd , check = True , capture_output = False , text = True ):
73+ """Execute a shell command with error handling."""
9174 try :
9275 return subprocess .run (
93- cmd ,
94- check = check ,
95- capture_output = capture_output ,
96- text = text ,
97- shell = False , # explicit for static analyzers
76+ cmd , check = check , capture_output = capture_output , text = text
9877 )
9978 except subprocess .CalledProcessError as e :
79+ # CalledProcessError is raised only when check=True and the command exits non-zero
10080 print (f"ERROR: Command failed: { ' ' .join (cmd )} " )
10181 print (f"Exit code: { e .returncode } " )
102- if e .stdout :
82+ if hasattr ( e , "stdout" ) and e .stdout :
10383 print ("--- stdout ---" )
10484 print (e .stdout )
105- if e .stderr :
85+ if hasattr ( e , "stderr" ) and e .stderr :
10686 print ("--- stderr ---" )
10787 print (e .stderr )
88+ # Exit the whole script with the same return code to mimic shell behavior
10889 sys .exit (e .returncode )
10990 except FileNotFoundError :
11091 print (f"ERROR: Command not found: { cmd [0 ]} " )
@@ -121,103 +102,43 @@ def check_requirements(sketch_dir, sdkconfig_path):
121102 Returns:
122103 bool: True if requirements are met, False otherwise
123104 """
105+ cmd = [str (SKETCH_UTILS ), "check_requirements" , sketch_dir , str (sdkconfig_path )]
124106 try :
125- res = run_sketch_utils (
126- "check_requirements" ,
127- sketch_dir ,
128- str (sdkconfig_path ),
129- check = False ,
130- capture_output = True ,
131- )
107+ res = run_cmd (cmd , check = False , capture_output = True )
132108 return res .returncode == 0
133109 except Exception :
134110 return False
135111
136112
137113def install_libs (* args ):
138114 """Install Arduino libraries using sketch_utils.sh"""
139- return run_sketch_utils ("install_libs" , * args , check = False )
115+ cmd = [str (SKETCH_UTILS ), "install_libs" ] + list (args )
116+ return run_cmd (cmd , check = False )
140117
141118
142119def build_sketch (args_list ):
143120 """Build a sketch using sketch_utils.sh"""
144- return run_sketch_utils ("build" , * args_list , check = False )
145-
146-
147- def parse_args (argv ):
148- """Parse command line arguments"""
149- epilog_text = (
150- "Examples:\n "
151- " docs_build_examples.py -c # Clean up binaries directory\n "
152- " docs_build_examples.py --build # Build all examples (use env vars)\n "
153- " docs_build_examples.py --build -ai /path/to/cli -au /path/to/user # Build with explicit paths\n "
154- " docs_build_examples.py --build --diagram --launchpad # Build with diagrams and LaunchPad (with env vars)\n \n "
155- "Path detection:\n "
156- " All paths can be set via environment variables or command line options.\n "
157- " ARDUINO_IDE_PATH and ARDUINO_USR_PATH environment variables are used by default.\n "
158- " Set by running install-arduino-cli.sh first, or use -ai/-au to override\n \n "
159- )
121+ cmd = [str (SKETCH_UTILS ), "build" ] + args_list
122+ return run_cmd (cmd , check = False )
160123
161- p = argparse .ArgumentParser (
162- description = "Build examples that have ci.yml with upload-binary targets" ,
163- formatter_class = RawDescriptionHelpFormatter ,
164- epilog = epilog_text ,
165- )
166- p .add_argument (
167- "-c" ,
168- "--cleanup" ,
169- dest = "cleanup" ,
170- action = "store_true" ,
171- help = "Clean up docs binaries directory and exit" ,
172- )
173- p .add_argument (
174- "-ai" ,
175- "--arduino-cli-path" ,
176- dest = "arduino_cli_path" ,
177- help = "Path to Arduino CLI installation directory (overrides ARDUINO_IDE_PATH env var)" ,
178- )
179- p .add_argument (
180- "-au" ,
181- "--arduino-user-path" ,
182- dest = "user_path" ,
183- help = "Path to Arduino user directory (overrides ARDUINO_USR_PATH env var)" ,
184- )
185- p .add_argument (
186- "-b" ,
187- "--build" ,
188- dest = "build" ,
189- action = "store_true" ,
190- help = "Build all examples" ,
191- )
192- p .add_argument (
193- "-d" ,
194- "--diagram" ,
195- dest = "generate_diagrams" ,
196- action = "store_true" ,
197- help = "Generate diagrams for prepared examples using docs-embed" ,
198- )
199- p .add_argument (
200- "-l" ,
201- "--launchpad" ,
202- dest = "generate_launchpad_config" ,
203- action = "store_true" ,
204- help = "Generate LaunchPad config for prepared examples" ,
205- )
206- return p .parse_args (argv )
207124
208125
209- def validate_prerequisites (args ):
210- """Validate that required prerequisites are available and get paths from env vars if needed."""
211126
127+ def validate_prerequisites (arduino_cli_path , user_path ):
128+ """Validate that required prerequisites are available and get paths from env vars if needed.
129+
130+ Returns:
131+ tuple: (arduino_cli_path, user_path) with values from env vars if not provided
132+ """
212133 # Get paths from environment variables if not provided via arguments
213- if not args . arduino_cli_path or not args . user_path :
134+ if not arduino_cli_path or not user_path :
214135 print ("Getting Arduino paths from environment variables..." )
215136 arduino_ide_path = os .environ .get ("ARDUINO_IDE_PATH" )
216137 arduino_usr_path = os .environ .get ("ARDUINO_USR_PATH" )
217138
218- if not args . arduino_cli_path :
139+ if not arduino_cli_path :
219140 if arduino_ide_path :
220- args . arduino_cli_path = arduino_ide_path
141+ arduino_cli_path = arduino_ide_path
221142 print (f" Arduino CLI path (ARDUINO_IDE_PATH): { arduino_ide_path } " )
222143 else :
223144 print (
@@ -226,9 +147,9 @@ def validate_prerequisites(args):
226147 print ("Run install-arduino-cli.sh first to set environment variables" )
227148 sys .exit (1 )
228149
229- if not args . user_path :
150+ if not user_path :
230151 if arduino_usr_path :
231- args . user_path = arduino_usr_path
152+ user_path = arduino_usr_path
232153 print (f" Arduino user path (ARDUINO_USR_PATH): { arduino_usr_path } " )
233154 else :
234155 print (
@@ -238,14 +159,16 @@ def validate_prerequisites(args):
238159 sys .exit (1 )
239160
240161 # Create Arduino user path if it doesn't exist
241- user_path = Path (args . user_path )
242- if not user_path .is_dir ():
162+ user_path_obj = Path (user_path )
163+ if not user_path_obj .is_dir ():
243164 try :
244- user_path .mkdir (parents = True , exist_ok = True )
245- print (f"Created Arduino user directory: { user_path } " )
165+ user_path_obj .mkdir (parents = True , exist_ok = True )
166+ print (f"Created Arduino user directory: { user_path_obj } " )
246167 except Exception as e :
247- print (f"ERROR: Failed to create Arduino user path { user_path } : { e } " )
168+ print (f"ERROR: Failed to create Arduino user path { user_path_obj } : { e } " )
248169 sys .exit (1 )
170+
171+ return arduino_cli_path , user_path
249172
250173
251174def cleanup_binaries ():
@@ -281,8 +204,8 @@ def cleanup_binaries():
281204 if not os .listdir (root ):
282205 try :
283206 os .rmdir (root )
284- except Exception as e :
285- print ( f"WARNING: Failed to remove empty directory { root } : { e } " )
207+ except Exception :
208+ pass
286209 print ("Cleanup completed" )
287210
288211
@@ -305,8 +228,7 @@ def find_examples_with_upload_binary():
305228 data = yaml .safe_load (ci_yml .read_text ())
306229 if "upload-binary" in data and data ["upload-binary" ]:
307230 res .append (str (ino ))
308- except Exception as e :
309- print (f"WARNING: Failed to parse ci.yml for { ci_yml } : { e } " )
231+ except Exception :
310232 continue
311233 return res
312234
@@ -329,14 +251,16 @@ def get_upload_binary_targets(sketch_dir):
329251 return []
330252
331253
332- def build_example_for_target (sketch_dir , target , relative_path , args ):
254+ def build_example_for_target (sketch_dir , target , relative_path , arduino_cli_path , user_path , generate_diagrams ):
333255 """Build a single example for a specific target.
334256
335257 Args:
336258 sketch_dir (Path): Path to the sketch directory
337259 target (str): Target board/configuration name
338260 relative_path (str): Relative path for output organization
339- args (argparse.Namespace): Parsed command line arguments
261+ arduino_cli_path (str): Path to Arduino CLI installation directory
262+ user_path (str): Path to Arduino user directory
263+ generate_diagrams (bool): Whether to generate diagrams
340264
341265 Returns:
342266 bool: True if build succeeded, False otherwise
@@ -355,9 +279,9 @@ def build_example_for_target(sketch_dir, target, relative_path, args):
355279 # Build the sketch using sketch_utils.sh build - pass args as in shell script
356280 build_args = [
357281 "-ai" ,
358- args . arduino_cli_path ,
282+ arduino_cli_path ,
359283 "-au" ,
360- args . user_path ,
284+ user_path ,
361285 "-s" ,
362286 str (sketch_dir ),
363287 "-t" ,
@@ -372,7 +296,7 @@ def build_example_for_target(sketch_dir, target, relative_path, args):
372296 ci_yml = Path (sketch_dir ) / "ci.yml"
373297 if ci_yml .exists ():
374298 shutil .copy (ci_yml , output_dir / "ci.yml" )
375- if args . generate_diagrams :
299+ if generate_diagrams :
376300 print (f"Generating diagram for { relative_path } ({ target } )..." )
377301 try :
378302 sync = DiagramSync (output_dir )
@@ -387,7 +311,7 @@ def build_example_for_target(sketch_dir, target, relative_path, args):
387311 return True
388312
389313
390- def build_all_examples (args ):
314+ def build_all_examples (arduino_cli_path , user_path , generate_diagrams , generate_launchpad_config ):
391315 """Build all examples that have upload-binary configuration
392316
393317 Prerequisites are validated in main() before calling this function
@@ -428,7 +352,7 @@ def build_all_examples(args):
428352 continue
429353 print (f"Building { relative_path } for targets: { targets } " )
430354 for target in targets :
431- if build_example_for_target (sketch_dir , target , relative_path , args ):
355+ if build_example_for_target (sketch_dir , target , relative_path , arduino_cli_path , user_path , generate_diagrams ):
432356 total_built += 1
433357 else :
434358 total_failed += 1
@@ -441,7 +365,7 @@ def build_all_examples(args):
441365 if ci_yml .exists ():
442366 shutil .copy (ci_yml , output_sketch_dir / "ci.yml" )
443367
444- if args . generate_launchpad_config :
368+ if generate_launchpad_config :
445369 print (f"Generating LaunchPad config for { relative_path } /{ target } ..." )
446370 try :
447371 sync = DiagramSync (output_sketch_dir )
@@ -463,35 +387,80 @@ def build_all_examples(args):
463387 return total_failed
464388
465389
466- def main (argv ):
390+ @click .command (
391+ help = """Build examples that have ci.yml with upload-binary targets.
392+
393+ \b
394+ Examples:
395+ docs_build_examples.py -c # Clean up binaries directory
396+ docs_build_examples.py --build # Build all examples (use env vars)
397+ docs_build_examples.py --build -ai /path/to/cli -au /path/to/user # Build with explicit paths
398+ docs_build_examples.py --build --diagram --launchpad # Build with diagrams and LaunchPad (with env vars)
399+
400+ \b
401+ Path detection:
402+ All paths can be set via environment variables or command line options.
403+ ARDUINO_IDE_PATH and ARDUINO_USR_PATH environment variables are used by default.
404+ Set by running install-arduino-cli.sh first, or use -ai/-au to override
405+ """ )
406+ @click .option (
407+ "-c" , "--cleanup" ,
408+ is_flag = True ,
409+ help = "Clean up docs binaries directory and exit"
410+ )
411+ @click .option (
412+ "-ai" , "--arduino-cli-path" ,
413+ default = None ,
414+ help = "Path to Arduino CLI installation directory (overrides ARDUINO_IDE_PATH env var)"
415+ )
416+ @click .option (
417+ "-au" , "--arduino-user-path" ,
418+ default = None ,
419+ help = "Path to Arduino user directory (overrides ARDUINO_USR_PATH env var)"
420+ )
421+ @click .option (
422+ "-b" , "--build" ,
423+ is_flag = True ,
424+ help = "Build all examples"
425+ )
426+ @click .option (
427+ "-d" , "--diagram" ,
428+ is_flag = True ,
429+ help = "Generate diagrams for prepared examples using docs-embed"
430+ )
431+ @click .option (
432+ "-l" , "--launchpad" ,
433+ is_flag = True ,
434+ help = "Generate LaunchPad config for prepared examples"
435+ )
436+ def main (cleanup , arduino_cli_path , arduino_user_path , build , diagram , launchpad ):
467437 """Main entry point for the script"""
468- args = parse_args (argv )
469-
470- if args .cleanup :
438+ if cleanup :
471439 cleanup_binaries ()
472440 return
473441
474- if args . build :
442+ if build :
475443 # Validate prerequisites and auto-detect paths if needed
476- validate_prerequisites (args )
444+ arduino_cli_path , arduino_user_path = validate_prerequisites (arduino_cli_path , arduino_user_path )
477445
478- result = build_all_examples (args )
446+ result = build_all_examples (arduino_cli_path , arduino_user_path , diagram , launchpad )
479447 if result == 0 :
480448 print ("\n All examples built successfully!" )
481449 else :
482450 print ("\n Some builds failed. Check the output above for details." )
483451 sys .exit (1 )
484452 return
485453
486- if args . generate_diagrams or args . generate_launchpad_config :
454+ if diagram or launchpad :
487455 print (
488456 "ERROR: --diagram and --launchpad options are only available when building examples (--build)"
489457 )
490458 sys .exit (1 )
491459
492460 # If no specific action is requested, show help
493- parse_args (["--help" ])
461+ ctx = click .get_current_context ()
462+ click .echo (ctx .get_help ())
494463
495464
496465if __name__ == "__main__" :
497- main (sys . argv [ 1 :] )
466+ main ()
0 commit comments