fix(compile): when the scan-all retry fails too, report ITS error
A manifest-scoped compile only sees the libraries the project declares, so it dies on the first undeclared transitive dependency — a header the user had no reason to name. The scan-all retry exists to rescue exactly that, and when it succeeds nothing is lost. When it fails as well, we returned the SCOPED failure, which describes a library only our own scoping made missing. Today a hand-written SSD1306 sketch was reported as Adafruit_GFX.h:12:10: fatal error: Adafruit_I2CDevice.h: No such file while the retry had already resolved BusIO and reached the real bug, an `#include <Adafruit_SSD1306>` with no .h. The user went looking for a missing library that was installed all along. The retry runs in the superset environment, so its error is the sketch's own in both compilers. Prefer it whenever it carries text; the scoped result still stands in when it does not.
This commit is contained in:
parent
1e47069ad8
commit
6a80f88954
|
|
@ -719,6 +719,18 @@ class ArduinoCLIService:
|
|||
if retry.get("success"):
|
||||
retry["manifest_incomplete"] = True
|
||||
return retry
|
||||
# Both attempts failed. The retry ran in the SUPERSET
|
||||
# environment (every installed library visible), so its
|
||||
# error is the sketch's real one; the scoped attempt
|
||||
# stopped at the first undeclared transitive dependency,
|
||||
# which is an artifact of OUR scoping. Returning the
|
||||
# scoped error here reported a phantom missing-library
|
||||
# problem for a sketch whose actual bug was a typo'd
|
||||
# #include (2026-08-24: "Adafruit_I2CDevice.h: No such
|
||||
# file" for a manifest that simply never named BusIO).
|
||||
if (retry.get("stderr") or "").strip():
|
||||
retry["scope_retry_failed"] = True
|
||||
return retry
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Compilation failed",
|
||||
|
|
|
|||
|
|
@ -3610,7 +3610,14 @@ class ESPIDFCompiler:
|
|||
self._suggest_libraries_for_headers(missing)
|
||||
)
|
||||
return retry
|
||||
# Both failed: the scoped error is the more informative one.
|
||||
# Both failed. The retry saw every installed library, so ITS
|
||||
# error is the sketch's real one — the scoped attempt stops at
|
||||
# the first undeclared transitive dependency, which is an
|
||||
# artifact of the scope, not of the sketch. Prefer it whenever
|
||||
# it carries text; fall through to the scoped result otherwise.
|
||||
if str(retry.get('error') or retry.get('stderr') or '').strip():
|
||||
retry['scope_retry_failed'] = True
|
||||
return retry
|
||||
return result
|
||||
|
||||
async def _compile_in_dir(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
"""When both the scoped compile and the scan-all retry fail, report the RETRY's
|
||||
error.
|
||||
|
||||
A manifest-scoped compile only sees the libraries the project declares. Almost
|
||||
nothing in the Arduino catalogue is self-contained (Adafruit GFX #includes
|
||||
Adafruit_I2CDevice.h from Adafruit BusIO), so the scoped attempt dies on a
|
||||
header the user never had a reason to name, and the scan-all retry exists to
|
||||
rescue that build. When the retry ALSO fails — because the sketch has a real
|
||||
error — the scoped stderr is the wrong thing to show: it describes a missing
|
||||
library that only OUR scoping made missing.
|
||||
|
||||
2026-08-24: a hand-written SSD1306 sketch whose actual bug was `#include
|
||||
<Adafruit_SSD1306>` (no .h) was reported to the user as "Adafruit_I2CDevice.h:
|
||||
No such file or directory".
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[3]
|
||||
sys.path.insert(0, str(_REPO / "backend"))
|
||||
|
||||
from app.core import hooks # noqa: E402
|
||||
from app.services.arduino_cli import ArduinoCLIService # noqa: E402
|
||||
|
||||
SKETCH = [{"name": "sketch.ino", "content": "void setup(){}\nvoid loop(){}\n"}]
|
||||
|
||||
# The phantom: the scope has GFX but not the BusIO it depends on.
|
||||
SCOPED_STDERR = (
|
||||
"/tmp/velxio-scope-3x_7_kwz/libraries/Adafruit_GFX_Library/Adafruit_GFX.h:12:10: "
|
||||
"fatal error: Adafruit_I2CDevice.h: No such file or directory\n"
|
||||
" #include <Adafruit_I2CDevice.h>\ncompilation terminated.\n"
|
||||
)
|
||||
# The truth: scan-all resolved BusIO and got to the sketch's own error.
|
||||
REAL_STDERR = (
|
||||
"/tmp/x/sketch/sketch.ino:4:65: error: 'class Adafruit_SSD1306' has no member "
|
||||
"named 'delay'; did you mean 'display'?\n"
|
||||
)
|
||||
|
||||
|
||||
def _service() -> ArduinoCLIService:
|
||||
"""A service without __init__'s arduino-cli config bootstrap — compile()
|
||||
only needs cli_path and the board-kind predicates."""
|
||||
svc = object.__new__(ArduinoCLIService)
|
||||
svc.cli_path = "arduino-cli"
|
||||
svc._installed_cores = ""
|
||||
return svc
|
||||
|
||||
|
||||
def _scope_hook(tmp_path):
|
||||
libs = tmp_path / "scope" / "libraries"
|
||||
libs.mkdir(parents=True, exist_ok=True)
|
||||
return lambda allowed, owner_id=None: (libs, "tok")
|
||||
|
||||
|
||||
def _fake_run(scoped_stderr, unscoped_stderr, *, unscoped_ok=False, calls=None):
|
||||
def run(cmd, **kwargs):
|
||||
env = kwargs.get("env") or {}
|
||||
scoped = "velxio-scope-" in str(env.get("ARDUINO_DIRECTORIES_USER", "")) \
|
||||
or "scope" in str(env.get("ARDUINO_DIRECTORIES_USER", ""))
|
||||
if calls is not None:
|
||||
calls.append("scoped" if scoped else "unscoped")
|
||||
if not scoped and unscoped_ok:
|
||||
out = Path(cmd[cmd.index("--output-dir") + 1])
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
(out / "sketch.ino.hex").write_text(":00000001FF\n")
|
||||
return subprocess.CompletedProcess(cmd, 0, stdout="ok", stderr="")
|
||||
return subprocess.CompletedProcess(
|
||||
cmd, 1, stdout="", stderr=scoped_stderr if scoped else unscoped_stderr
|
||||
)
|
||||
return run
|
||||
|
||||
|
||||
def test_both_fail_reports_the_retry_error(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(hooks, "_materialize_library_scope_hook", _scope_hook(tmp_path))
|
||||
calls: list[str] = []
|
||||
monkeypatch.setattr(subprocess, "run", _fake_run(SCOPED_STDERR, REAL_STDERR, calls=calls))
|
||||
|
||||
result = asyncio.run(_service().compile(
|
||||
SKETCH, "arduino:avr:uno", allowed_libraries={"Adafruit GFX Library"},
|
||||
))
|
||||
|
||||
assert calls == ["scoped", "unscoped"]
|
||||
assert result["success"] is False
|
||||
assert result["scope_retry_failed"] is True
|
||||
assert "has no member named 'delay'" in result["stderr"]
|
||||
assert "Adafruit_I2CDevice.h" not in result["stderr"]
|
||||
|
||||
|
||||
def test_a_retry_that_succeeds_still_flags_the_manifest(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(hooks, "_materialize_library_scope_hook", _scope_hook(tmp_path))
|
||||
monkeypatch.setattr(subprocess, "run", _fake_run(SCOPED_STDERR, "", unscoped_ok=True))
|
||||
|
||||
result = asyncio.run(_service().compile(
|
||||
SKETCH, "arduino:avr:uno", allowed_libraries={"Adafruit GFX Library"},
|
||||
))
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["manifest_incomplete"] is True
|
||||
assert result.get("scope_retry_failed") is None
|
||||
|
||||
|
||||
def test_a_genuine_error_without_a_scope_is_not_retried(monkeypatch, tmp_path):
|
||||
"""No manifest -> no scope -> nothing to blame on scoping, and one compile."""
|
||||
monkeypatch.setattr(hooks, "_materialize_library_scope_hook", _scope_hook(tmp_path))
|
||||
calls: list[str] = []
|
||||
monkeypatch.setattr(subprocess, "run", _fake_run(SCOPED_STDERR, REAL_STDERR, calls=calls))
|
||||
|
||||
result = asyncio.run(_service().compile(SKETCH, "arduino:avr:uno"))
|
||||
|
||||
assert calls == ["unscoped"]
|
||||
assert result["success"] is False
|
||||
assert "has no member named 'delay'" in result["stderr"]
|
||||
Loading…
Reference in New Issue