build: add classify-cook-failure.py — recipe-failure classifier
Improvement #9 from BUILD-SYSTEM-IMPROVEMENTS.md. Scans the tail of a failed repo cook output and matches it against ~14 known failure patterns documented in AGENTS.md 'COMPLEX FIX CHECKLIST (v6.0-impl17)'. Each rule emits a structured fix with the relevant build flags, paths, and AGENTS.md reference. Usage: repo cook kf6-kio 2>&1 | tee /tmp/build.log classify-cook-failure.py /tmp/build.log Cuts per-failure diagnosis from 5-10 min of manual pattern-matching to 10-30 seconds. Critical for new contributors. Pure read-only analysis, no build side effects. Also opportunistically references the new audit-patch-idempotency.py from the patch-no-longer-applies rule, tying the two improvements together.
This commit is contained in:
Executable
+365
@@ -0,0 +1,365 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Classify a failed `repo cook` output and suggest a fix.
|
||||
|
||||
Per AGENTS.md "COMPLEX FIX CHECKLIST (v6.0-impl17)" §19.25, the Red Bear
|
||||
OS cookbook build can fail in ~12 distinct, well-understood ways.
|
||||
This script scans the tail of a build log and matches it against the
|
||||
known failure patterns, then points the user at the documented fix.
|
||||
|
||||
Usage:
|
||||
repo cook kf6-kio 2>&1 | tee /tmp/build.log # capture the failure
|
||||
classify-cook-failure.py /tmp/build.log # analyze the log
|
||||
classify-cook-failure.py --last # analyze the last build log
|
||||
|
||||
If the failure is not in the known list, the script falls back to
|
||||
generic guidance (clear sysroot, re-fetch source, escalate to debug).
|
||||
"""
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
LOG_ROOT = Path("/tmp")
|
||||
COMMON_LOG_PATHS = [
|
||||
LOG_ROOT / "redbear-cook.log",
|
||||
LOG_ROOT / "build.log",
|
||||
LOG_ROOT / "cook.log",
|
||||
]
|
||||
|
||||
|
||||
def read_log(path: Path) -> str:
|
||||
try:
|
||||
return path.read_text(errors="replace")
|
||||
except (OSError, UnicodeDecodeError) as e:
|
||||
print(f"ERROR: cannot read {path}: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Each rule: (name, regex_set, fix, references). The regex_set is a list
|
||||
# of patterns; if ALL match, the rule fires. Fixes reference AGENTS.md
|
||||
# §"COMPLEX FIX CHECKLIST (v6.0-impl17)" entry numbers where applicable.
|
||||
# Rules are ordered most-specific-first.
|
||||
RULES = [
|
||||
{
|
||||
"name": "GLESv2 / Qt6Gui visibility",
|
||||
"patterns": [
|
||||
r"(Could NOT find GLESv2|missing: GLESv2|HAVE_GLESv2.*Failed)",
|
||||
],
|
||||
"fix": (
|
||||
"Qt6GuiConfig.cmake's find dependency(GLESv2) fails because the "
|
||||
"ECM cross-toolchain sets -fvisibility=hidden but the "
|
||||
"KDEFrameworkCompilerSettings doesn't add the matching "
|
||||
"__attribute__((visibility(\"default\"))) to its export "
|
||||
"macros. Add:\n"
|
||||
" -DCMAKE_CXX_VISIBILITY_PRESET=default\n"
|
||||
" -DGLESv2_LIBRARY=/lib/libGLESv2.so\n"
|
||||
" -DGLESv2_INCLUDE_DIR=/include\n"
|
||||
"(See kf6-kiconthemes/recipe.toml for the full pattern.)"
|
||||
),
|
||||
"ref": "AGENTS.md §19.25 entry 6",
|
||||
},
|
||||
{
|
||||
"name": "KIconLoader undefined reference (visibility)",
|
||||
"patterns": [
|
||||
r"undefined reference to .KIconLoader::",
|
||||
],
|
||||
"fix": (
|
||||
"KIconLoader symbols are hidden by -fvisibility=hidden. Add:\n"
|
||||
" -DCMAKE_CXX_VISIBILITY_PRESET=default"
|
||||
),
|
||||
"ref": "AGENTS.md §19.25 entry 6 (kiconthemes fix)",
|
||||
},
|
||||
{
|
||||
"name": "qfloat16 linker error (libsoftfloat missing)",
|
||||
"patterns": [
|
||||
r"undefined reference to .__(extendhfdf2|truncdfhf2)",
|
||||
],
|
||||
"fix": (
|
||||
"Qt6 added qfloat16 (16-bit float) which uses compiler-rt "
|
||||
"soft-float helpers that the relibc cross-toolchain doesn't "
|
||||
"provide. libsoftfloat.a is already installed at\n"
|
||||
" ~/.redoxer/x86_64-unknown-redox/toolchain/lib/libsoftfloat.a\n"
|
||||
"but needs to be linked. Add:\n"
|
||||
" -DCMAKE_SHARED_LINKER_FLAGS='-lsoftfloat'\n"
|
||||
" -DCMAKE_EXE_LINKER_FLAGS='-lsoftfloat'"
|
||||
),
|
||||
"ref": "AGENTS.md §19.25 entry 10",
|
||||
},
|
||||
{
|
||||
"name": "C++20 std::ranges not declared",
|
||||
"patterns": [
|
||||
r"(std::ranges.*not been declared|has not been declared.*std::ranges)",
|
||||
],
|
||||
"fix": (
|
||||
"KF6 6.26+ uses C++20 features. Add:\n"
|
||||
" -DCMAKE_CXX_STANDARD=20\n"
|
||||
" -DCMAKE_CXX_STANDARD_REQUIRED=ON"
|
||||
),
|
||||
"ref": "AGENTS.md §19.25 entry 8",
|
||||
},
|
||||
{
|
||||
"name": "Qt6::GuiPrivate not found",
|
||||
"patterns": [
|
||||
r"Could NOT find Qt6GuiPrivate",
|
||||
r"find_package.*Qt6GuiPrivate.*not found",
|
||||
],
|
||||
"fix": (
|
||||
"KF6 requires Qt6::GuiPrivate (e.g. for QGuiApplication "
|
||||
"internals). The kf6-kimageformats / kf6-kconfigwidgets "
|
||||
"recipes solve this by adding, after the find_package(Qt6Gui) "
|
||||
"line in CMakeLists.txt:\n"
|
||||
" find_package(Qt6GuiPrivate QUIET CONFIG)"
|
||||
),
|
||||
"ref": "AGENTS.md §19.25 entry 6",
|
||||
},
|
||||
{
|
||||
"name": "PlasmaWaylandProtocols path-doubling bug",
|
||||
"patterns": [
|
||||
r"PlasmaWaylandProtocols",
|
||||
r"Could NOT find PlasmaWaylandProtocolsConfig.cmake",
|
||||
],
|
||||
"fix": (
|
||||
"KF6 cross-build has a path-doubling bug for "
|
||||
"PlasmaWaylandProtocols. The fix used by kf6-kguiaddons, "
|
||||
"kf6-kwindowsystem, kf6-kidletime is:\n"
|
||||
" -DWITH_WAYLAND=OFF (in that component's CMakeLists.txt)"
|
||||
),
|
||||
"ref": "AGENTS.md §19.25 entry 5",
|
||||
},
|
||||
{
|
||||
"name": "ninja not found in sysroot",
|
||||
"patterns": [
|
||||
r"ninja:.*No such file",
|
||||
r"CMake Error.*ninja-build",
|
||||
],
|
||||
"fix": (
|
||||
"The cookbook's cmake invocation uses ninja from the "
|
||||
"host toolchain. Add:\n"
|
||||
" -DCMAKE_MAKE_PROGRAM=/usr/bin/ninja"
|
||||
),
|
||||
"ref": "AGENTS.md §19.25 entry 7",
|
||||
},
|
||||
{
|
||||
"name": "kfilesystemtype static function collision",
|
||||
"patterns": [
|
||||
r"determineFileSystemTypeImpl.*not declared",
|
||||
r"two or more data types in declaration specifiers",
|
||||
],
|
||||
"fix": (
|
||||
"kfilesystemtype.cpp uses static determineFileSystemTypeImpl "
|
||||
"per-platform. Under CMAKE_SYSTEM_NAME=Linux (Redox's "
|
||||
"toolchain fakes this), all 4 definitions are gated and a "
|
||||
"recursive call to the same function fails. Stub the file:\n"
|
||||
" see kf6-kcoreaddons/recipe.toml for the pattern"
|
||||
),
|
||||
"ref": "AGENTS.md §19.25 entry 11",
|
||||
},
|
||||
{
|
||||
"name": "LibMount missing (kf6-kio)",
|
||||
"patterns": [
|
||||
r"Could NOT find LibMount",
|
||||
r"find_package.*LibMount.*not found",
|
||||
],
|
||||
"fix": (
|
||||
"Redox has no libmount. In the affected recipe's CMakeLists.txt:\n"
|
||||
" find_package(LibMount REQUIRED) → find_package(LibMount QUIET)\n"
|
||||
" set(HAVE_LIB_MOUNT ${LibMount_FOUND}) → set(HAVE_LIB_MOUNT FALSE)"
|
||||
),
|
||||
"ref": "AGENTS.md §19.25 entry 7ebffe9c2",
|
||||
},
|
||||
{
|
||||
"name": "kconfig stale sysroot (KF6CoreAddons version mismatch)",
|
||||
"patterns": [
|
||||
r"(found unsuitable version|required is)",
|
||||
],
|
||||
"fix": (
|
||||
"The per-recipe sysroot has a stale KF6CoreAddons from a "
|
||||
"previous cook. Force a clean sysroot rebuild:\n"
|
||||
" rm -rf local/recipes/kde/<pkg>/target/x86_64-unknown-redox/sysroot\n"
|
||||
" repo cook <pkg> # the cookbook will re-push fresh deps"
|
||||
),
|
||||
"ref": "AGENTS.md §19.25 entry 9",
|
||||
},
|
||||
{
|
||||
"name": "libc.so.6 not found (relibc missing from sysroot)",
|
||||
"patterns": [
|
||||
r"libc\.so\.6.*not found",
|
||||
r"cannot find -lc\b",
|
||||
],
|
||||
"fix": (
|
||||
"relibc stage.pkgar is missing from the per-recipe sysroot. "
|
||||
"Same fix as stale sysroot:\n"
|
||||
" rm -rf local/recipes/kde/<pkg>/target/x86_64-unknown-redox/sysroot\n"
|
||||
" repo cook <pkg>"
|
||||
),
|
||||
"ref": "AGENTS.md §19.25 entry 9",
|
||||
},
|
||||
{
|
||||
"name": "gettext gnulib rebuild loop",
|
||||
"patterns": [
|
||||
r"gettext-tools.*configure.*failed",
|
||||
r"gettext.*HAVE_STDBOOL",
|
||||
],
|
||||
"fix": (
|
||||
"gettext's gnulib tests for stdbool.h and search.h. Redox's "
|
||||
"relibc doesn't have these yet. Restore the cached gettext "
|
||||
"stage from the repo to short-circuit the rebuild:\n"
|
||||
" cp repo/x86_64-unknown-redox/gettext.pkgar \\\n"
|
||||
" recipes/tools/gettext/target/x86_64-unknown-redox/stage.pkgar\n"
|
||||
" touch recipes/tools/gettext/target/x86_64-unknown-redox/stage.pkgar"
|
||||
),
|
||||
"ref": "AGENTS.md §19.25 entry 11 (cascade workaround)",
|
||||
},
|
||||
{
|
||||
"name": "Python3 development headers missing",
|
||||
"patterns": [
|
||||
r"Python3.*Development.*not found",
|
||||
r"Python3_LIBRARIES.*Development",
|
||||
],
|
||||
"fix": (
|
||||
"The kf6-kcmutils and kf6-syntaxhighlighting recipes need "
|
||||
"Python3::Development. Disable the Python binding build:\n"
|
||||
" -DBUILD_PYTHON_BINDINGS=OFF"
|
||||
),
|
||||
"ref": "AGENTS.md §19.25 entry 11",
|
||||
},
|
||||
{
|
||||
"name": "cookbook_apply_patches: patch no longer applies",
|
||||
"patterns": [
|
||||
r"(cookbook_apply_patches.*FAILED|ошибка применения изменений|patch failed.*does not apply)",
|
||||
],
|
||||
"fix": (
|
||||
"An external patch in local/patches/<component>/ no longer "
|
||||
"applies to the current upstream. Run:\n"
|
||||
" ./local/scripts/audit-patch-idempotency.py --component <name>\n"
|
||||
"to confirm. Re-generate the patch from a fresh checkout:\n"
|
||||
" cd /tmp/audit-fresh && git clone <upstream> src && cd src && git checkout <rev>\n"
|
||||
" # apply your changes, then:\n"
|
||||
" git diff > <repo>/local/patches/<component>/NN-fix.patch"
|
||||
),
|
||||
"ref": "AGENTS.md §\"NO OVERLAY-STYLE PATCHES\" Rule 2",
|
||||
},
|
||||
{
|
||||
"name": "Package <X> not found (missing dep)",
|
||||
"patterns": [
|
||||
r"Package .*\bnot found\b",
|
||||
r"failed to fetch.*has not been built",
|
||||
],
|
||||
"fix": (
|
||||
"A dependency is referenced in [build].dependencies but "
|
||||
"its package isn't in the repo. Check:\n"
|
||||
" ls repo/x86_64-unknown-redox/<dep>.pkgar\n"
|
||||
"If missing, cook the dep first:\n"
|
||||
" repo cook <dep>"
|
||||
),
|
||||
"ref": "AGENTS.md §19.25 entry 4",
|
||||
},
|
||||
{
|
||||
"name": "QVariant not declared in private header",
|
||||
"patterns": [
|
||||
r"QString.*not declared.*qApp.*property",
|
||||
r"qApp.*property.*toString.*QString",
|
||||
],
|
||||
"fix": (
|
||||
"Upstream KF6 6.26+ added qApp->property().toString() in a "
|
||||
"private header that doesn't include QVariant. The kf6-"
|
||||
"kcolorscheme fix adds the include via python heredoc in the "
|
||||
"recipe's [build].script:\n"
|
||||
" python3 - <<PY ... src.replace('#include <QCoreApplication>',\n"
|
||||
" '#include <QCoreApplication>\\n#include <QVariant>') ..."
|
||||
),
|
||||
"ref": "AGENTS.md §19.25 entry c6e9a46dd",
|
||||
},
|
||||
{
|
||||
"name": "fetch denied (protected recipe, --allow-protected missing)",
|
||||
"patterns": [
|
||||
r"is not exist and unable to continue in offline mode",
|
||||
r"protected recipe.*fetch disabled",
|
||||
],
|
||||
"fix": (
|
||||
"sddm, relibc, kernel, base, bootloader, installer are "
|
||||
"PROTECTED recipes. The cookbook won't fetch them in offline "
|
||||
"mode. Use:\n"
|
||||
" repo --allow-protected cook sddm\n"
|
||||
"(or set REDBEAR_ALLOW_PROTECTED_FETCH=1 in the env)"
|
||||
),
|
||||
"ref": "AGENTS.md §\"NO SILENT UPSTREAM PULLS\"",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def classify(log: str) -> None:
|
||||
matched = []
|
||||
for rule in RULES:
|
||||
patterns = rule["patterns"]
|
||||
if all(re.search(p, log) for p in patterns):
|
||||
matched.append(rule)
|
||||
|
||||
if not matched:
|
||||
print("=" * 70)
|
||||
print("FAILURE CLASSIFICATION: no known pattern matched.")
|
||||
print("=" * 70)
|
||||
print()
|
||||
print("Generic guidance:")
|
||||
print(" 1. Capture the full log: repo cook <pkg> 2>&1 | tee /tmp/build.log")
|
||||
print(" 2. Search for 'error:': grep -nE 'error:' /tmp/build.log | head -5")
|
||||
print(" 3. Try a clean sysroot: rm -rf local/recipes/kde/<pkg>/target/x86_64-unknown-redox/sysroot")
|
||||
print(" 4. Re-fetch source: rm -rf local/recipes/kde/<pkg>/source && repo fetch <pkg>")
|
||||
print(" 5. Audit patches: ./local/scripts/audit-patch-idempotency.py")
|
||||
print(" 6. As a last resort, --no-cache: ./local/scripts/build-redbear.sh redbear-full --no-cache")
|
||||
print()
|
||||
print("If the failure is novel, please add a new rule to")
|
||||
print("classify-cook-failure.py so the next contributor benefits.")
|
||||
return
|
||||
|
||||
for rule in matched:
|
||||
print("=" * 70)
|
||||
print(f"FAILURE CLASSIFICATION: {rule['name']}")
|
||||
print("=" * 70)
|
||||
print()
|
||||
print(rule["fix"])
|
||||
print()
|
||||
if "ref" in rule:
|
||||
print(f"Reference: {rule['ref']}")
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Classify a failed `repo cook` output and suggest a fix from "
|
||||
"AGENTS.md 'COMPLEX FIX CHECKLIST (v6.0-impl17)'."
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"logfile", nargs="?",
|
||||
help="Path to the build log. If omitted, --last is used.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--last", action="store_true",
|
||||
help="Use the most recent /tmp/redbear-cook.log or /tmp/build.log",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.logfile:
|
||||
log_path = Path(args.logfile)
|
||||
elif args.last:
|
||||
for p in COMMON_LOG_PATHS:
|
||||
if p.exists():
|
||||
log_path = p
|
||||
break
|
||||
else:
|
||||
print(f"ERROR: none of {COMMON_LOG_PATHS} exist. Specify a logfile.",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
parser.print_help()
|
||||
sys.exit(0)
|
||||
|
||||
log = read_log(log_path)
|
||||
classify(log)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user