Update build helper

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Joey Yakimowich-Payne 2026-08-11 12:23:07 +09:00
commit 05790b4f99

121
build.py Normal file → Executable file
View file

@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Build and flash the project with optional grip color overrides.""" """Build and flash the project with optional grip color overrides."""
import argparse import argparse
import os import os
import random import random
@ -8,12 +9,15 @@ import shutil
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Final, Literal, TypeAlias, final
SCRIPT_DIR = Path(__file__).resolve().parent SCRIPT_DIR = Path(__file__).resolve().parent
CONFIG_FILE = SCRIPT_DIR / "controller_color_config.h" CONFIG_FILE = SCRIPT_DIR / "controller_color_config.h"
BUILD_DIR = SCRIPT_DIR / "build" 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 = ( MACROS = (
"SWITCH_COLOR_LEFT_GRIP_R", "SWITCH_COLOR_LEFT_GRIP_R",
@ -24,36 +28,73 @@ MACROS = (
"SWITCH_COLOR_RIGHT_GRIP_B", "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( parser = argparse.ArgumentParser(
description="Build and flash the project, optionally setting grip colors.", description="Build and flash the project, optionally setting grip colors.",
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="Default behavior leaves controller_color_config.h unchanged.", 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 = parser.add_mutually_exclusive_group()
group.add_argument( _ = group.add_argument(
"--random-grip-color", "--random-grip-color",
action="store_true", action="store_true",
help="Randomize both grip colors before building.", help="Randomize both grip colors before building.",
) )
group.add_argument( _ = group.add_argument(
"--grip-color", "--grip-color",
metavar="RRGGBB", metavar="RRGGBB",
help="Set both grip colors to the provided hex value.", 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)) 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): 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 return value
def update_grip_colors(rgb_hex):
def update_grip_colors(rgb_hex: str) -> None:
if not CONFIG_FILE.exists(): 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) sys.exit(1)
r, g, b = rgb_hex[:2], rgb_hex[2:4], rgb_hex[4:6] 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: try:
text = CONFIG_FILE.read_text(encoding="utf-8") text = CONFIG_FILE.read_text(encoding="utf-8")
except OSError as exc: 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) 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}}" 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) updated, count = re.subn(pattern, rf"\g<1>0x{val.upper()}", data)
if count == 0: 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) sys.exit(1)
return updated return updated
@ -77,26 +118,30 @@ def update_grip_colors(rgb_hex):
text = replace(macro, val, text) text = replace(macro, val, text)
try: try:
CONFIG_FILE.write_text(text, encoding="utf-8") _ = CONFIG_FILE.write_text(text, encoding="utf-8")
except OSError as exc: 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) sys.exit(1)
def run_cmd(command):
def run_cmd(command: list[str]) -> None:
try: try:
subprocess.run(command, cwd=SCRIPT_DIR, check=True) _ = subprocess.run(command, cwd=SCRIPT_DIR, check=True)
except FileNotFoundError as exc: 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) sys.exit(1)
except subprocess.CalledProcessError as exc: except subprocess.CalledProcessError as exc:
sys.exit(exc.returncode) sys.exit(exc.returncode)
def resolve_picotool():
def resolve_picotool() -> Path:
env_val = os.environ.get("PICOTOOL_PATH") env_val = os.environ.get("PICOTOOL_PATH")
if env_val: if env_val:
env_path = Path(env_val).expanduser() env_path = Path(env_val).expanduser()
if not env_path.exists(): 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) sys.exit(1)
return env_path return env_path
@ -104,10 +149,13 @@ def resolve_picotool():
if found: if found:
return Path(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) sys.exit(1)
def build():
def build(protocol: BuildProtocol) -> None:
run_cmd( run_cmd(
[ [
"cmake", "cmake",
@ -116,20 +164,23 @@ def build():
"-B", "-B",
str(BUILD_DIR), str(BUILD_DIR),
"-DSWITCH_PICO_LOG=OFF", "-DSWITCH_PICO_LOG=OFF",
f"-DSWITCH_PICO_PROTOCOL={protocol}",
] ]
) )
run_cmd(["cmake", "--build", str(BUILD_DIR)]) run_cmd(["cmake", "--build", str(BUILD_DIR)])
def flash():
def flash() -> None:
picotool = resolve_picotool() picotool = resolve_picotool()
if not ELF_PATH.exists(): 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" f"Error: Cannot find ELF at {ELF_PATH}. Set ELF_PATH to override.\n"
) )
sys.exit(1) sys.exit(1)
run_cmd([str(picotool), "load", str(ELF_PATH), "-fx"]) run_cmd([str(picotool), "load", str(ELF_PATH), "-fx"])
def main():
def main() -> None:
args = parse_args() args = parse_args()
color = None color = None
@ -138,16 +189,30 @@ def main():
elif args.grip_color: elif args.grip_color:
try: try:
color = validate_custom_color(args.grip_color) color = validate_custom_color(args.grip_color)
except ValueError as exc: except argparse.ArgumentTypeError as exc:
sys.stderr.write(f"Error: {exc}\n") _ = sys.stderr.write(f"Error: {exc}\n")
sys.exit(1) sys.exit(1)
if color: if color:
update_grip_colors(color) update_grip_colors(color)
print(f"Grip color set to #{color} in {CONFIG_FILE.name}") 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() flash()
if __name__ == "__main__": if __name__ == "__main__":
main() main()