Skip to content

Instantly share code, notes, and snippets.

@JakubAndrysek
Last active June 16, 2025 11:02
Show Gist options
  • Select an option

  • Save JakubAndrysek/15ac9aeb37663cce85432fa30568e82a to your computer and use it in GitHub Desktop.

Select an option

Save JakubAndrysek/15ac9aeb37663cce85432fa30568e82a to your computer and use it in GitHub Desktop.

build-run Tool

A command-line utility for building and running ESP32 Arduino-based tests using GitHub CI-compatible scripts.

Overview

This script simplifies managing builds and tests across multiple ESP32 platforms. It supports chunked testing, cleaning, QEMU emulation, and optional shell completions.


Usage

build-run -s <sketch> -t <target> [options]

Required Options:

  • -s, --sketch <name>: Sketch name to build and/or test
  • -t, --target <name>: Target board(s), e.g. esp32, esp32s3, or all

Optional Flags:

  • -W, --wokwi-timeout <sec>: Timeout for Wokwi in seconds (converted to ms)

  • -Q, --qemu: Use QEMU platform instead of hardware

  • --build-only: Only build, skip test

  • --test-only: Only test, skip build

  • -c, --chunk: Enable chunked execution

    • -i, --chunk-index <i>: Index of current chunk
    • -m, --chunk-max <n>: Total number of chunks
  • -o, --options: Enable additional testing options

  • -e, --erase: Erase flash before running test

  • --type <type>: Define test type (e.g., validation, performance)

  • --clean: Clean all generated files and exit

  • --verbose: Enable verbose output (default: True)


Examples

Build and run gpio sketch for ESP32-S3 with a 10s Wokwi timeout:

build-run -s gpio -t esp32s3 -W 10

Run gpio sketch for all targets with test-only:

build-run -s gpio -t all --test-only -W 10

Build only:

build-run -s hello_world -t esp32 --build-only

Clean everything:

build-run --clean

Script Details

  • Invokes .github/scripts/tests_build.sh and tests_run.sh

  • Manages all available targets:

    • esp32, esp32c3, esp32c6, esp32h2, esp32p4, esp32s2, esp32s3
  • Supports shell completion via auto_click_auto


Shell Completion

Autocompletion is enabled automatically for Zsh, Bash, and Fish using the helper enable_click_shell_completion.


Exit Codes

  • 0: Success
  • 1: Error (invalid options, build/test failure, etc.)

License

MIT or as per upstream repository.

#!/usr/bin/env python3
import click
import subprocess
import sys
import os
from pathlib import Path
from auto_click_auto import enable_click_shell_completion
from auto_click_auto.constants import ShellType
@click.command()
@click.option('-s', '--sketch', required=True, help='Sketch name to build/run')
@click.option('-t', '--target', multiple=True, required=True, help='Target platform(s) (e.g., esp32, esp32s3, esp32c3) or "all" for all targets')
@click.option('-W', '--wokwi-timeout', type=int, help='Wokwi timeout in seconds (will be multiplied by 1000)')
@click.option('-Q', '--qemu', is_flag=True, help='Use QEMU platform instead of hardware')
@click.option('--build-only', is_flag=True, help='Only run the build step')
@click.option('--test-only', is_flag=True, help='Only run the test step (skip build)')
@click.option('-c', '--chunk', is_flag=True, help='Use chunk build/run mode')
@click.option('-i', '--chunk-index', type=int, help='Chunk index for parallel execution')
@click.option('-m', '--chunk-max', type=int, help='Maximum number of chunks')
@click.option('-o', '--options', is_flag=True, help='Enable options mode for testing')
@click.option('-e', '--erase', is_flag=True, help='Erase flash before testing')
@click.option('--type', 'test_type', help='Test type (validation, performance, etc.)')
@click.option('--clean', is_flag=True, help='Clean build and test files')
@click.option('--verbose', is_flag=True, help='Enable verbose output', default=True)
def main(sketch, target, wokwi_timeout, qemu, build_only, test_only, chunk, chunk_index,
chunk_max, options, erase, test_type, clean, verbose):
"""
Build and run ESP32 Arduino tests using the GitHub CI scripts.
Examples:
build-run -s gpio -t esp32s3 -W 10
build-run -s gpio -t esp32s2 esp32p4 -W 20
build-run -s hello_world -t esp32 --build-only
build-run -s gpio -t all --test-only -W 10
build-run --clean
"""
# Define all available targets
ALL_TARGETS = ['esp32', 'esp32c3', 'esp32c6', 'esp32h2', 'esp32p4', 'esp32s2', 'esp32s3']
# Process target list - handle "all" special case
targets = list(target)
if 'all' in targets:
if len(targets) > 1:
click.echo("Warning: 'all' specified with other targets, using all targets", err=True)
targets = ALL_TARGETS
# Validate targets
for t in targets:
if t not in ALL_TARGETS:
click.echo(f"Error: Unknown target '{t}'. Available targets: {', '.join(ALL_TARGETS)}", err=True)
sys.exit(1)
# Get the ESP32 Arduino core directory (assuming script is run from project root)
esp32_dir = Path.cwd()
build_script = esp32_dir / ".github" / "scripts" / "tests_build.sh"
run_script = esp32_dir / ".github" / "scripts" / "tests_run.sh"
# Verify scripts exist
if not build_script.exists():
click.echo(f"Error: Build script not found at {build_script}", err=True)
sys.exit(1)
if not run_script.exists():
click.echo(f"Error: Run script not found at {run_script}", err=True)
sys.exit(1)
# Handle clean option
if clean:
if verbose:
click.echo("Cleaning build and test files...")
result = subprocess.run([str(build_script), "-clean"], capture_output=not verbose)
sys.exit(result.returncode)
# Validate mutually exclusive options
if build_only and test_only:
click.echo("Error: --build-only and --test-only are mutually exclusive", err=True)
sys.exit(1)
# Validate chunk options
if chunk and (chunk_index is None or chunk_max is None):
click.echo("Error: --chunk requires both --chunk-index and --chunk-max", err=True)
sys.exit(1)
# Track results for each target
results = {}
# Process each target
for current_target in targets:
if verbose and len(targets) > 1:
click.echo(f"\n{'='*50}")
click.echo(f"Processing target: {current_target}")
click.echo(f"{'='*50}")
target_success = True
# Build step
if not test_only:
build_cmd = [str(build_script)]
if chunk:
build_cmd.extend(["-c", "-type", test_type or "validation"])
build_cmd.extend(["-t", current_target, "-i", str(chunk_index), "-m", str(chunk_max)])
else:
build_cmd.extend(["-s", sketch, "-t", current_target])
if verbose:
click.echo(f"Running build command: {' '.join(build_cmd)}")
result = subprocess.run(build_cmd, capture_output=not verbose)
if result.returncode != 0:
click.echo(f"Build failed for target {current_target} with exit code {result.returncode}", err=True)
target_success = False
results[current_target] = "BUILD_FAILED"
continue # Skip to next target
if verbose:
click.echo(f"Build completed successfully for {current_target}")
# Test step
if not build_only and target_success:
run_cmd = [str(run_script)]
if chunk:
run_cmd.append("-c")
run_cmd.extend(["-s", sketch, "-t", current_target])
if wokwi_timeout is not None:
# Convert seconds to milliseconds as the script expects
wokwi_timeout_ms = wokwi_timeout * 1000
run_cmd.extend(["-W", str(wokwi_timeout_ms)])
if qemu:
run_cmd.append("-Q")
if options:
run_cmd.append("-o")
if erase:
run_cmd.append("-e")
if chunk:
run_cmd.extend(["-i", str(chunk_index), "-m", str(chunk_max)])
if test_type:
run_cmd.extend(["-type", test_type])
if verbose:
click.echo(f"Running test command: {' '.join(run_cmd)}")
result = subprocess.run(run_cmd, capture_output=not verbose)
if result.returncode != 0:
click.echo(f"Test failed for target {current_target} with exit code {result.returncode}", err=True)
target_success = False
results[current_target] = "TEST_FAILED"
else:
if verbose:
click.echo(f"Test completed successfully for {current_target}")
results[current_target] = "SUCCESS"
elif target_success:
results[current_target] = "SUCCESS"
# Print summary if multiple targets were processed
if len(targets) > 1:
if verbose:
click.echo(f"\n{'='*50}")
click.echo("SUMMARY")
click.echo(f"{'='*50}")
for target_name, status in results.items():
status_color = "green" if status == "SUCCESS" else "red"
click.echo(f"{target_name}: ", nl=False)
click.secho(status, fg=status_color)
# Exit with error code if any target failed
failed_targets = [t for t, s in results.items() if s != "SUCCESS"]
if failed_targets:
click.echo(f"\nFailed targets: {', '.join(failed_targets)}", err=True)
sys.exit(1)
if verbose:
click.echo("All operations completed successfully")
# Enable shell autocompletion for Zsh (and other shells)
enable_click_shell_completion(
program_name="build-run",
shells={ShellType.ZSH, ShellType.BASH, ShellType.FISH},
verbose=False
)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment