From 05790b4f99065f7d990ab3f89911ff186a99f6a7 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 11 Aug 2026 12:23:07 +0900 Subject: [PATCH] Update build helper Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- build.py | 121 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 93 insertions(+), 28 deletions(-) mode change 100644 => 100755 build.py diff --git a/build.py b/build.py old mode 100644 new mode 100755 index dabefe8..190bee6 --- a/build.py +++ b/build.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 """Build and flash the project with optional grip color overrides.""" + import argparse import os import random @@ -8,12 +9,15 @@ import shutil import subprocess import sys from pathlib import Path +from typing import Final, Literal, TypeAlias, final SCRIPT_DIR = Path(__file__).resolve().parent CONFIG_FILE = SCRIPT_DIR / "controller_color_config.h" BUILD_DIR = SCRIPT_DIR / "build" +BUILD_ELF_PATH = BUILD_DIR / "switch-pico.elf" +BUILD_UF2_PATH = BUILD_DIR / "switch-pico.uf2" -ELF_PATH = Path(os.environ.get("ELF_PATH", BUILD_DIR / "switch-pico.elf")).expanduser() +ELF_PATH = Path(os.environ.get("ELF_PATH", str(BUILD_ELF_PATH))).expanduser() MACROS = ( "SWITCH_COLOR_LEFT_GRIP_R", @@ -24,36 +28,73 @@ MACROS = ( "SWITCH_COLOR_RIGHT_GRIP_B", ) -def parse_args(): +BuildProtocol: TypeAlias = Literal["legacy", "switch2"] +PROTOCOL_CHOICES: Final[tuple[BuildProtocol, BuildProtocol]] = ("legacy", "switch2") + + +@final +class BuildArguments(argparse.Namespace): + def __init__(self) -> None: + super().__init__() + self.protocol: BuildProtocol = "legacy" + self.build_only: bool = False + self.random_grip_color: bool = False + self.grip_color: str = "" + + +def parse_args() -> BuildArguments: parser = argparse.ArgumentParser( description="Build and flash the project, optionally setting grip colors.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog="Default behavior leaves controller_color_config.h unchanged.", ) + _ = parser.add_argument( + "--protocol", + choices=PROTOCOL_CHOICES, + default="legacy", + help="USB protocol to build (default: legacy).", + ) + _ = parser.add_argument( + "--build-only", + action="store_true", + help="Build and print ELF/UF2 paths without flashing.", + ) group = parser.add_mutually_exclusive_group() - group.add_argument( + _ = group.add_argument( "--random-grip-color", action="store_true", help="Randomize both grip colors before building.", ) - group.add_argument( + _ = group.add_argument( "--grip-color", metavar="RRGGBB", help="Set both grip colors to the provided hex value.", ) - return parser.parse_args() + args = BuildArguments() + _ = parser.parse_args(namespace=args) + if args.protocol == "switch2" and (args.random_grip_color or bool(args.grip_color)): + parser.error( + "Switch 2 builds do not consume legacy grip colors; omit --random-grip-color " + + "and --grip-color." + ) + return args -def random_hex_color(): + +def random_hex_color() -> str: return "".join(f"{random.randrange(256):02X}" for _ in range(3)) -def validate_custom_color(value): + +def validate_custom_color(value: str) -> str: if not re.fullmatch(r"[0-9A-Fa-f]{6}", value): - raise ValueError("Color must be a 6-digit hex value like FF8800.") + raise argparse.ArgumentTypeError( + "Color must be a 6-digit hex value like FF8800." + ) return value -def update_grip_colors(rgb_hex): + +def update_grip_colors(rgb_hex: str) -> None: if not CONFIG_FILE.exists(): - sys.stderr.write(f"Error: Cannot find {CONFIG_FILE}\n") + _ = sys.stderr.write(f"Error: Cannot find {CONFIG_FILE}\n") sys.exit(1) r, g, b = rgb_hex[:2], rgb_hex[2:4], rgb_hex[4:6] @@ -61,14 +102,14 @@ def update_grip_colors(rgb_hex): try: text = CONFIG_FILE.read_text(encoding="utf-8") except OSError as exc: - sys.stderr.write(f"Error reading {CONFIG_FILE}: {exc}\n") + _ = sys.stderr.write(f"Error reading {CONFIG_FILE}: {exc}\n") sys.exit(1) - def replace(name, val, data): + def replace(name: str, val: str, data: str) -> str: pattern = rf"(?m)^(#define\s+{name}\s+)0x[0-9A-Fa-f]{{2}}" updated, count = re.subn(pattern, rf"\g<1>0x{val.upper()}", data) if count == 0: - sys.stderr.write(f"Error: Could not find {name} in {CONFIG_FILE}\n") + _ = sys.stderr.write(f"Error: Could not find {name} in {CONFIG_FILE}\n") sys.exit(1) return updated @@ -77,26 +118,30 @@ def update_grip_colors(rgb_hex): text = replace(macro, val, text) try: - CONFIG_FILE.write_text(text, encoding="utf-8") + _ = CONFIG_FILE.write_text(text, encoding="utf-8") except OSError as exc: - sys.stderr.write(f"Error writing {CONFIG_FILE}: {exc}\n") + _ = sys.stderr.write(f"Error writing {CONFIG_FILE}: {exc}\n") sys.exit(1) -def run_cmd(command): + +def run_cmd(command: list[str]) -> None: try: - subprocess.run(command, cwd=SCRIPT_DIR, check=True) + _ = subprocess.run(command, cwd=SCRIPT_DIR, check=True) except FileNotFoundError as exc: - sys.stderr.write(f"Error running {command[0]}: {exc}\n") + _ = sys.stderr.write(f"Error running {command[0]}: {exc}\n") sys.exit(1) except subprocess.CalledProcessError as exc: sys.exit(exc.returncode) -def resolve_picotool(): + +def resolve_picotool() -> Path: env_val = os.environ.get("PICOTOOL_PATH") if env_val: env_path = Path(env_val).expanduser() if not env_path.exists(): - sys.stderr.write(f"Error: PICOTOOL_PATH set to {env_path}, but it does not exist.\n") + _ = sys.stderr.write( + f"Error: PICOTOOL_PATH set to {env_path}, but it does not exist.\n" + ) sys.exit(1) return env_path @@ -104,10 +149,13 @@ def resolve_picotool(): if found: return Path(found) - sys.stderr.write("Error: picotool not found. Put it on your PATH or set PICOTOOL_PATH.\n") + _ = sys.stderr.write( + "Error: picotool not found. Put it on your PATH or set PICOTOOL_PATH.\n" + ) sys.exit(1) -def build(): + +def build(protocol: BuildProtocol) -> None: run_cmd( [ "cmake", @@ -116,20 +164,23 @@ def build(): "-B", str(BUILD_DIR), "-DSWITCH_PICO_LOG=OFF", + f"-DSWITCH_PICO_PROTOCOL={protocol}", ] ) run_cmd(["cmake", "--build", str(BUILD_DIR)]) -def flash(): + +def flash() -> None: picotool = resolve_picotool() if not ELF_PATH.exists(): - sys.stderr.write( + _ = sys.stderr.write( f"Error: Cannot find ELF at {ELF_PATH}. Set ELF_PATH to override.\n" ) sys.exit(1) run_cmd([str(picotool), "load", str(ELF_PATH), "-fx"]) -def main(): + +def main() -> None: args = parse_args() color = None @@ -138,16 +189,30 @@ def main(): elif args.grip_color: try: color = validate_custom_color(args.grip_color) - except ValueError as exc: - sys.stderr.write(f"Error: {exc}\n") + except argparse.ArgumentTypeError as exc: + _ = sys.stderr.write(f"Error: {exc}\n") sys.exit(1) if color: update_grip_colors(color) print(f"Grip color set to #{color} in {CONFIG_FILE.name}") - build() + build(args.protocol) + if args.build_only: + outputs = (BUILD_ELF_PATH, BUILD_UF2_PATH) + for output in outputs: + if not output.is_file(): + _ = sys.stderr.write( + f"Error: Expected build output not found: {output}\n" + ) + sys.exit(1) + print("Build outputs:") + for output in outputs: + print(f" {output}") + return + flash() + if __name__ == "__main__": main()