Enhance firmware builder with failure summary and error reporting
- Added `failure_summary` function to extract actionable errors from compiler logs. - Updated manifest to include error details when firmware build fails. - Added unit tests to verify the functionality of the new failure summary feature.
This commit is contained in:
@@ -284,6 +284,31 @@ def write_manifest(output_dir: Path, manifest: dict[str, object]) -> None:
|
||||
temporary.replace(path)
|
||||
|
||||
|
||||
def failure_summary(log_path: Path) -> str:
|
||||
"""Extract a concise actionable failure from a compiler log."""
|
||||
if not log_path.is_file():
|
||||
return "Firmware build failed"
|
||||
ansi_escape = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]")
|
||||
lines = [
|
||||
ansi_escape.sub("", line).strip()
|
||||
for line in log_path.read_text(encoding="utf-8", errors="replace").splitlines()
|
||||
]
|
||||
meaningful = [
|
||||
line for line in lines
|
||||
if line
|
||||
and not line.startswith(("XIAOZHI_STAGE ", "XIAOZHI_SOURCE_REVISION "))
|
||||
]
|
||||
patterns = (
|
||||
re.compile(r"(?:fatal error|error:|ValueError:|RuntimeError:|FileNotFoundError:)", re.I),
|
||||
re.compile(r"(?:Kconfig rejected|Unsupported build option|build stopped|failed with exit code)", re.I),
|
||||
)
|
||||
for pattern in patterns:
|
||||
for line in reversed(meaningful):
|
||||
if pattern.search(line):
|
||||
return line[:500]
|
||||
return meaningful[-1][:500] if meaningful else "Firmware build failed"
|
||||
|
||||
|
||||
def is_retryable_oss_error(error: Exception, oss2: Any) -> bool:
|
||||
exceptions = getattr(oss2, "exceptions", None)
|
||||
request_error = getattr(exceptions, "RequestError", None)
|
||||
@@ -447,6 +472,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
manifest["exit_code"] = return_code
|
||||
else:
|
||||
manifest["status"] = "failed"
|
||||
manifest["error"] = failure_summary(log_path)
|
||||
|
||||
write_manifest(args.output_dir, manifest)
|
||||
if upload_config is not None:
|
||||
|
||||
@@ -135,6 +135,23 @@ sys.exit(%d)
|
||||
manifest = json.loads((output / "manifest.json").read_text())
|
||||
self.assertEqual(manifest["status"], "failed")
|
||||
self.assertEqual(manifest["exit_code"], 7)
|
||||
self.assertEqual(manifest["error"], "fake compiler output")
|
||||
|
||||
def test_failure_summary_prefers_compiler_error(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
log_path = Path(temporary) / "build.log"
|
||||
log_path.write_text(
|
||||
"FAILED: component.o\n"
|
||||
"config.h:48:2: error: OLED display type is not selected\n"
|
||||
"ninja: build stopped: subcommand failed.\n"
|
||||
"XIAOZHI_STAGE uploading\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
firmware_builder.failure_summary(log_path),
|
||||
"config.h:48:2: error: OLED display type is not selected",
|
||||
)
|
||||
|
||||
def test_success_uploads_outputs_and_manifest_last(self) -> None:
|
||||
uploads: list[tuple[str, str]] = []
|
||||
|
||||
@@ -577,6 +577,9 @@ choice BOARD_TYPE
|
||||
config BOARD_TYPE_WDMOMO_CGC_144
|
||||
bool "WDMomo ESP32-CGC-144"
|
||||
depends on IDF_TARGET_ESP32
|
||||
config BOARD_TYPE_WK_ESP32S3_DEV
|
||||
bool "WK ESP32-S3 Dev Board (维控智能开发板)"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_XMINI_C3
|
||||
bool "Xmini C3"
|
||||
depends on IDF_TARGET_ESP32C3
|
||||
@@ -607,9 +610,6 @@ choice BOARD_TYPE
|
||||
config BOARD_TYPE_ZHENGCHEN_CAM
|
||||
bool "Zhengchen AI Camera Wi-Fi (征辰科技)"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_WK_ESP32S3_DEV
|
||||
bool "维控智能开发板"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
endchoice
|
||||
|
||||
choice XIAOZHI_NETWORK_TYPE
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
#if CONFIG_OLED_SSD1306_128X32
|
||||
#define DISPLAY_HEIGHT 32
|
||||
#elif CONFIG_OLED_SSD1306_128X64
|
||||
#elif CONFIG_OLED_SSD1306_128X64 || CONFIG_OLED_SH1106_128X64
|
||||
#define DISPLAY_HEIGHT 64
|
||||
#else
|
||||
#error "OLED display type is not selected"
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
{
|
||||
"name": "esp32-s3-touch-lcd-4.3c",
|
||||
"sdkconfig_append": [
|
||||
"CONFIG_BOARD_TYPE_ESP32S3_Touch_LCD_4_3C=y",
|
||||
"CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_4_3C=y",
|
||||
"CONFIG_USE_DEVICE_AEC=y",
|
||||
"CONFIG_SPIRAM_FETCH_INSTRUCTIONS=y",
|
||||
"CONFIG_SPIRAM_RODATA=y"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"type": "wk-esp32s3-dev",
|
||||
"target": "esp32s3",
|
||||
"builds": [
|
||||
{
|
||||
"name": "wk-esp32s3-dev"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+44
-9
@@ -296,6 +296,31 @@ def _enabled_default_wake_word_symbols(target: str) -> list[str]:
|
||||
return symbols
|
||||
|
||||
|
||||
def _board_supports_wake_word(
|
||||
target: str,
|
||||
sdkconfig_append: list[str],
|
||||
) -> bool:
|
||||
"""Return whether one board variant satisfies the wake-word Kconfig deps."""
|
||||
if target in _LITE_WAKE_WORD_TARGETS:
|
||||
return True
|
||||
if target not in _AFE_WAKE_WORD_TARGETS | {"esp32"}:
|
||||
return False
|
||||
|
||||
defaults: list[str] = []
|
||||
for path in (Path("sdkconfig.defaults"), Path(f"sdkconfig.defaults.{target}")):
|
||||
if not path.exists():
|
||||
continue
|
||||
defaults.extend(
|
||||
line.strip()
|
||||
for line in path.read_text(encoding="utf-8").splitlines()
|
||||
if line.strip().startswith("CONFIG_") and "=" in line
|
||||
)
|
||||
assignments = _sdkconfig_assignments(
|
||||
_merge_sdkconfig_options(defaults, sdkconfig_append)
|
||||
)
|
||||
return assignments.get("CONFIG_SPIRAM") == "y"
|
||||
|
||||
|
||||
def _wake_word_sdkconfig_options(
|
||||
wake_word: str,
|
||||
target: str,
|
||||
@@ -925,6 +950,10 @@ def _collect_variants(
|
||||
)
|
||||
variant["config"] = config_symbol
|
||||
variant["display_name"] = _get_board_display_name(config_symbol)
|
||||
variant["wake_word_supported"] = _board_supports_wake_word(
|
||||
variant["target"],
|
||||
sdkconfig_append,
|
||||
)
|
||||
variant["build_options"] = _build_option_definitions(
|
||||
variant["board"],
|
||||
variant["target"],
|
||||
@@ -1432,26 +1461,32 @@ def build_board(
|
||||
# Process sdkconfig_append
|
||||
build_sdkconfig_append = build.get("sdkconfig_append", [])
|
||||
explicit_board_cfg = _extract_board_config_from_sdkconfig_append(build_sdkconfig_append)
|
||||
if explicit_board_cfg:
|
||||
board_type_config = _resolve_board_config(
|
||||
board_type,
|
||||
target,
|
||||
build_sdkconfig_append,
|
||||
variant_name=name,
|
||||
)
|
||||
if explicit_board_cfg == board_type_config:
|
||||
print(
|
||||
f"[INFO] Board config explicitly set in config.json: {explicit_board_cfg}, "
|
||||
"skip auto-select.",
|
||||
)
|
||||
sdkconfig_append = list(build_sdkconfig_append)
|
||||
else:
|
||||
board_type_config = _resolve_board_config(
|
||||
board_type,
|
||||
target,
|
||||
build_sdkconfig_append,
|
||||
variant_name=name,
|
||||
)
|
||||
# Replace a stale/misspelled explicit symbol with the canonical
|
||||
# Kconfig symbol. Listing and building must resolve board identity
|
||||
# through the same path or their exposed options can diverge.
|
||||
sdkconfig_append = [f"{board_type_config}=y"]
|
||||
sdkconfig_append.extend(build_sdkconfig_append)
|
||||
sdkconfig_append.extend(
|
||||
item for item in build_sdkconfig_append
|
||||
if item.strip() != f"{explicit_board_cfg}=y"
|
||||
)
|
||||
|
||||
option_definitions = _build_option_definitions(
|
||||
board_type,
|
||||
target,
|
||||
board_type_config if not explicit_board_cfg else explicit_board_cfg,
|
||||
board_type_config,
|
||||
build,
|
||||
)
|
||||
|
||||
|
||||
@@ -930,6 +930,16 @@ class BuildOptionTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, "Invalid wake word"):
|
||||
build._wake_word_sdkconfig_options("jarvis", "esp32s3")
|
||||
|
||||
def test_board_wake_word_support_obeys_psram_dependency(self):
|
||||
self.assertTrue(build._board_supports_wake_word("esp32c3", []))
|
||||
self.assertFalse(build._board_supports_wake_word("esp32", []))
|
||||
self.assertTrue(
|
||||
build._board_supports_wake_word("esp32", ["CONFIG_SPIRAM=y"])
|
||||
)
|
||||
self.assertFalse(
|
||||
build._board_supports_wake_word("esp32s3", ["CONFIG_SPIRAM=n"])
|
||||
)
|
||||
|
||||
def test_user_options_override_board_options(self):
|
||||
merged = build._merge_sdkconfig_options(
|
||||
[
|
||||
@@ -1046,6 +1056,12 @@ class BuildOptionTests(unittest.TestCase):
|
||||
sdkconfig = build._build_options_sdkconfig(definitions, normalized, {})
|
||||
self.assertIn("CONFIG_LCD_CUSTOM=n", sdkconfig)
|
||||
|
||||
def test_bread_compact_esp32_config_supports_sh1106(self):
|
||||
config_header = (
|
||||
ROOT / "main/boards/bread-compact-esp32/config.h"
|
||||
).read_text(encoding="utf-8")
|
||||
self.assertIn("CONFIG_OLED_SH1106_128X64", config_header)
|
||||
|
||||
def test_non_default_style_disables_multiline_chat(self):
|
||||
definitions = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user