326 lines
9.5 KiB
Python
326 lines
9.5 KiB
Python
"""
|
|
Runs the compilation process for all examples on all boards in parallel.
|
|
Build artifacts are recycled within a board group so that subsequent ino
|
|
files are built faster.
|
|
"""
|
|
|
|
import argparse
|
|
import sys
|
|
import time
|
|
import warnings
|
|
from pathlib import Path
|
|
|
|
from ci.boards import Board, get_board # type: ignore
|
|
from ci.concurrent_run import ConcurrentRunArgs, concurrent_run
|
|
from ci.locked_print import locked_print
|
|
|
|
HERE = Path(__file__).parent.resolve()
|
|
|
|
LIBS = ["src"]
|
|
EXTRA_LIBS = [
|
|
"https://github.com/me-no-dev/ESPAsyncWebServer.git",
|
|
"ArduinoOTA",
|
|
"SD",
|
|
"FS",
|
|
"ESPmDNS",
|
|
"WiFi",
|
|
"WebSockets",
|
|
]
|
|
BUILD_FLAGS = ["-Wl,-Map,firmware.map", "-fopt-info-all=optimization_report.txt"]
|
|
|
|
# Default boards to compile for. You can use boards not defined here but
|
|
# if the board isn't part of the officially supported platformio boards then
|
|
# you will need to add the board to the ~/.platformio/platforms directory.
|
|
# prior to running this script. This happens automatically as of 2024-08-20
|
|
# with the github workflow scripts.
|
|
DEFAULT_BOARDS_NAMES = [
|
|
"apollo3_red",
|
|
"apollo3_thing_explorable",
|
|
"web", # work in progress
|
|
"uno", # Build is faster if this is first, because it's used for global init.
|
|
"esp32dev",
|
|
"esp01", # ESP8266
|
|
"esp32c3",
|
|
"attiny85",
|
|
"ATtiny1616",
|
|
"esp32c6",
|
|
"esp32s3",
|
|
"esp32p4",
|
|
"yun",
|
|
"digix",
|
|
"teensy30",
|
|
"teensy41",
|
|
"adafruit_feather_nrf52840_sense",
|
|
"xiaoblesense_adafruit",
|
|
"rpipico",
|
|
"rpipico2",
|
|
"uno_r4_wifi",
|
|
"esp32rmt_51",
|
|
"esp32dev_idf44",
|
|
"bluepill",
|
|
"esp32rmt_51",
|
|
"giga_r1",
|
|
"sparkfun_xrp_controller",
|
|
]
|
|
|
|
OTHER_BOARDS_NAMES = [
|
|
"nano_every",
|
|
"esp32-c2-devkitm-1",
|
|
]
|
|
|
|
# Examples to compile.
|
|
DEFAULT_EXAMPLES = [
|
|
"Animartrix",
|
|
"Apa102",
|
|
"Apa102HD",
|
|
"Apa102HDOverride",
|
|
"Audio",
|
|
"Blink",
|
|
"Blur",
|
|
"Chromancer",
|
|
"ColorPalette",
|
|
"ColorTemperature",
|
|
"Corkscrew",
|
|
"CompileTest",
|
|
"Cylon",
|
|
"DemoReel100",
|
|
"Downscale",
|
|
"FestivalStick",
|
|
"FirstLight",
|
|
"Fire2012",
|
|
"Multiple/MultipleStripsInOneArray",
|
|
"Multiple/ArrayOfLedArrays",
|
|
"Noise",
|
|
"NoisePlayground",
|
|
"NoisePlusPalette",
|
|
"LuminescentGrand",
|
|
"Pacifica",
|
|
"Pride2015",
|
|
"RGBCalibrate",
|
|
"RGBSetDemo",
|
|
"RGBW",
|
|
"Overclock",
|
|
"RGBWEmulated",
|
|
"TwinkleFox",
|
|
"XYMatrix",
|
|
"FireMatrix",
|
|
"FireCylinder",
|
|
"FxGfx2Video",
|
|
"FxSdCard",
|
|
"FxCylon",
|
|
"FxDemoReel100",
|
|
"FxTwinkleFox",
|
|
"FxFire2012",
|
|
"FxNoisePlusPalette",
|
|
"FxPacifica",
|
|
"FxEngine",
|
|
"WS2816",
|
|
]
|
|
|
|
EXTRA_EXAMPLES: dict[Board, list[str]] = {
|
|
# ESP32DEV: ["EspI2SDemo"],
|
|
# ESP32_S3_DEVKITC_1: ["EspS3I2SDemo"],
|
|
}
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(
|
|
description="Compile FastLED examples for various boards."
|
|
)
|
|
# parser.add_argument(
|
|
# "--boards", type=str, help="Comma-separated list of boards to compile for"
|
|
# )
|
|
# needs to be a positional argument instead
|
|
parser.add_argument(
|
|
"boards",
|
|
type=str,
|
|
help="Comma-separated list of boards to compile for",
|
|
nargs="?",
|
|
)
|
|
parser.add_argument(
|
|
"--examples", type=str, help="Comma-separated list of examples to compile"
|
|
)
|
|
parser.add_argument(
|
|
"--exclude-examples", type=str, help="Examples that should be excluded"
|
|
)
|
|
parser.add_argument(
|
|
"--skip-init", action="store_true", help="Skip the initialization step"
|
|
)
|
|
parser.add_argument(
|
|
"--defines", type=str, help="Comma-separated list of compiler definitions"
|
|
)
|
|
parser.add_argument(
|
|
"--extra-packages",
|
|
type=str,
|
|
help="Comma-separated list of extra packages to install",
|
|
)
|
|
parser.add_argument(
|
|
"--add-extra-esp32-libs",
|
|
action="store_true",
|
|
help="Add extra libraries to the libraries list to check against compiler errors.",
|
|
)
|
|
parser.add_argument(
|
|
"--build-dir", type=str, help="Override the default build directory"
|
|
)
|
|
parser.add_argument(
|
|
"--no-project-options",
|
|
action="store_true",
|
|
help="Don't use custom project options",
|
|
)
|
|
parser.add_argument(
|
|
"--interactive",
|
|
action="store_true",
|
|
help="Enable interactive mode to choose a board",
|
|
)
|
|
# Passed by the github action to disable interactive mode.
|
|
parser.add_argument(
|
|
"--no-interactive", action="store_true", help="Disable interactive mode"
|
|
)
|
|
parser.add_argument(
|
|
"-v", "--verbose", action="store_true", help="Enable verbose output"
|
|
)
|
|
parser.add_argument(
|
|
"--supported-boards",
|
|
action="store_true",
|
|
help="Print the list of supported boards and exit",
|
|
)
|
|
args, unknown = parser.parse_known_args()
|
|
if unknown:
|
|
warnings.warn(f"Unknown arguments: {unknown}")
|
|
# if --interactive and --no-interative are both passed, --no-interactive takes precedence.
|
|
if args.interactive and args.no_interactive:
|
|
warnings.warn(
|
|
"Both --interactive and --no-interactive were passed, --no-interactive takes precedence."
|
|
)
|
|
args.interactive = False
|
|
return args
|
|
|
|
|
|
def remove_duplicates(items: list[str]) -> list[str]:
|
|
seen = set()
|
|
out = []
|
|
for item in items:
|
|
if item not in seen:
|
|
seen.add(item)
|
|
out.append(item)
|
|
return out
|
|
|
|
|
|
def choose_board_interactively(boards: list[str]) -> list[str]:
|
|
print("Available boards:")
|
|
boards = remove_duplicates(sorted(boards))
|
|
for i, board in enumerate(boards):
|
|
print(f"[{i}]: {board}")
|
|
print("[all]: All boards")
|
|
out: list[str] = []
|
|
while True:
|
|
try:
|
|
# choice = int(input("Enter the number of the board(s) you want to compile to: "))
|
|
input_str = input(
|
|
"Enter the number of the board(s) you want to compile to, or it's name(s): "
|
|
)
|
|
if "all" in input_str:
|
|
return boards
|
|
for board in input_str.split(","):
|
|
if board == "":
|
|
continue
|
|
if not board.isdigit():
|
|
out.append(board) # Assume it's a board name.
|
|
else:
|
|
index = int(board) # Find the board from the index.
|
|
if 0 <= index < len(boards):
|
|
out.append(boards[index])
|
|
else:
|
|
warnings.warn(f"invalid board index: {index}, skipping")
|
|
if not out:
|
|
print("Please try again.")
|
|
continue
|
|
return out
|
|
except ValueError:
|
|
print("Invalid input. Please enter a number.")
|
|
|
|
|
|
def resolve_example_path(example: str) -> Path:
|
|
example_path = HERE.parent / "examples" / example
|
|
if not example_path.exists():
|
|
raise FileNotFoundError(f"Example '{example}' not found at '{example_path}'")
|
|
return example_path
|
|
|
|
|
|
def create_concurrent_run_args(args: argparse.Namespace) -> ConcurrentRunArgs:
|
|
skip_init = args.skip_init
|
|
if args.interactive:
|
|
boards = choose_board_interactively(DEFAULT_BOARDS_NAMES + OTHER_BOARDS_NAMES)
|
|
else:
|
|
boards = args.boards.split(",") if args.boards else DEFAULT_BOARDS_NAMES
|
|
projects: list[Board] = []
|
|
for board in boards:
|
|
projects.append(get_board(board, no_project_options=args.no_project_options))
|
|
extra_examples: dict[Board, list[Path]] = {}
|
|
if args.examples is None:
|
|
for b, _examples in EXTRA_EXAMPLES.items():
|
|
resolved_examples = [resolve_example_path(example) for example in _examples]
|
|
extra_examples[b] = resolved_examples
|
|
examples = args.examples.split(",") if args.examples else DEFAULT_EXAMPLES
|
|
examples_paths = [resolve_example_path(example) for example in examples]
|
|
# now process example exclusions.
|
|
if args.exclude_examples:
|
|
exclude_examples = args.exclude_examples.split(",")
|
|
examples_paths = [
|
|
example
|
|
for example in examples_paths
|
|
if example.name not in exclude_examples
|
|
]
|
|
for exclude in exclude_examples:
|
|
examples.remove(exclude)
|
|
defines: list[str] = []
|
|
if args.defines:
|
|
defines.extend(args.defines.split(","))
|
|
extra_packages: list[str] = []
|
|
if args.extra_packages:
|
|
extra_packages.extend(args.extra_packages.split(","))
|
|
build_dir = args.build_dir
|
|
extra_scripts = "pre:lib/ci/ci-flags.py"
|
|
verbose = args.verbose
|
|
|
|
out: ConcurrentRunArgs = ConcurrentRunArgs(
|
|
projects=projects,
|
|
examples=examples_paths,
|
|
skip_init=skip_init,
|
|
defines=defines,
|
|
extra_packages=extra_packages,
|
|
libs=LIBS,
|
|
build_dir=build_dir,
|
|
extra_scripts=extra_scripts,
|
|
cwd=str(HERE.parent),
|
|
board_dir=(HERE / "boards").absolute().as_posix(),
|
|
build_flags=BUILD_FLAGS,
|
|
verbose=verbose,
|
|
extra_examples=extra_examples,
|
|
)
|
|
return out
|
|
|
|
|
|
def main() -> int:
|
|
"""Main function."""
|
|
args = parse_args()
|
|
if args.supported_boards:
|
|
print(",".join(DEFAULT_BOARDS_NAMES))
|
|
return 0
|
|
if args.add_extra_esp32_libs:
|
|
LIBS.extend(EXTRA_LIBS)
|
|
|
|
# Set the working directory to the script's parent directory.
|
|
run_args = create_concurrent_run_args(args)
|
|
start_time = time.time()
|
|
rtn = concurrent_run(args=run_args)
|
|
time_taken = time.strftime("%Mm:%Ss", time.gmtime(time.time() - start_time))
|
|
locked_print(f"Compilation finished in {time_taken}.")
|
|
return rtn
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
sys.exit(main())
|
|
except KeyboardInterrupt:
|
|
sys.exit(1)
|