diff --git a/backend/app/api/routes/compile.py b/backend/app/api/routes/compile.py index 803969b1..8d46bf50 100644 --- a/backend/app/api/routes/compile.py +++ b/backend/app/api/routes/compile.py @@ -163,6 +163,13 @@ def _build_identity(board_fqbn: str) -> str: is the previous behaviour and never less strict than it needs to be for a board whose variant we could not read. """ + # Only the ESP-IDF lane HAS a shared build dir (the predicate is the same + # one _run_compile routes on). An AVR / RP2040 / STM32 FQBN has no IDF + # target at all, and _idf_target defaults unknown boards to 'esp32' — so + # every non-ESP32 board used to collapse onto the `esp32::esp32` key and + # queue behind unrelated ESP32 builds for no reason. + if not board_fqbn.startswith("esp32:"): + return board_fqbn try: target = espidf_compiler._idf_target(board_fqbn) variant = espidf_compiler._arduino_variant(board_fqbn, target) diff --git a/backend/app/services/arduino_cli.py b/backend/app/services/arduino_cli.py index 1701f167..0496a383 100644 --- a/backend/app/services/arduino_cli.py +++ b/backend/app/services/arduino_cli.py @@ -140,6 +140,9 @@ class ArduinoCLIService: def __init__(self, cli_path: str = "arduino-cli"): self.cli_path = cli_path + # Cached `core list` output; see _is_core_installed. Invalidated + # wherever a core is installed so a fresh install is seen immediately. + self._installed_cores: str | None = None self._ensure_board_urls() self._ensure_core_installed() @@ -235,12 +238,24 @@ class ArduinoCLIService: return None def _is_core_installed(self, core_id: str) -> bool: - """Check whether a core is currently installed.""" - result = subprocess.run( - [self.cli_path, "core", "list"], - capture_output=True, text=True - ) - return core_id in result.stdout + """Check whether a core is currently installed. + + Memoized: this shells out to `arduino-cli core list` (~1.5 s measured) + and it used to run on EVERY compile of a non-AVR board, from inside an + async handler - so it blocked the event loop for the whole process, not + just the caller. Cores only appear via install_core() below, which + refreshes the set, and they are never removed at runtime. + """ + if self._installed_cores is None: + try: + result = subprocess.run( + [self.cli_path, "core", "list"], + capture_output=True, text=True, timeout=60, + ) + self._installed_cores = result.stdout + except (subprocess.SubprocessError, OSError): + return False # unknown: let the caller try to install + return core_id in self._installed_cores async def ensure_core_for_board(self, fqbn: str) -> dict: """ @@ -260,6 +275,8 @@ class ArduinoCLIService: install_spec = f"{core_id}@{version}" if version else core_id print(f"[arduino-cli] Auto-installing core {install_spec} for board {fqbn}...") + self._installed_cores = None # a core is about to appear + def _install(): return subprocess.run( [self.cli_path, "core", "install", install_spec], diff --git a/backend/app/services/espidf_compiler.py b/backend/app/services/espidf_compiler.py index ab6f9343..ea390169 100644 --- a/backend/app/services/espidf_compiler.py +++ b/backend/app/services/espidf_compiler.py @@ -3967,6 +3967,11 @@ class ESPIDFCompiler: f'-DIDF_TARGET={idf_target}', '-DCMAKE_BUILD_TYPE=Release', f'-DSDKCONFIG_DEFAULTS={project_dir / "sdkconfig.defaults"}', + # IDF re-validates its python env on every configure (~1s of a + # ~9s configure). The image pins that env at build time and the + # entrypoint has already exercised it, so tell IDF it was checked + # externally - the same escape hatch idf.py uses for itself. + '-DPYTHON_DEPS_CHECKED=1', str(project_dir), ]