Files
RedBear-OS/local/scripts/classify-cook-failure.py
T
vasilito ffbe098ef8 config: add tlc to redbear-mini and redbear-full; create recipe symlink
TLC (Twilight Commander) was missing from both ISO configs. Added
tlc = {} to [packages] in redbear-mini.toml and redbear-full.toml.
Created missing symlink: recipes/tui/tlc -> ../../local/recipes/tui/tlc.
2026-06-19 11:47:25 +03:00

457 lines
17 KiB
Python
Executable File

#!/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
JSON SCHEMA (with --json):
Top-level:
log: str path to the analyzed log file
matched: [Rule, ...] one per rule that fired
matched_count: int len(matched)
Per-rule:
name: str rule name
patterns: [str, ...] regex patterns (raw)
context_required: [str, ...] tokens that must appear in the log
fix: str multi-line fix text
ref: str AGENTS.md §19.25 reference (or "")
Exit code: 0 if matched_count == 0, 1 if matched_count > 0. This is
CI-safe: a non-zero exit is the SIGNAL "I found a known failure".
JSON exit code is INTENTIONALLY inverted vs the audit scripts (which
return 0 on clean). Here, exit 0 = "no known pattern matched" (novel
failure, need human triage) and exit 1 = "I identified the problem
and told you the fix". A CI job that wants to PASS on a known fix
should treat exit 1 as a pass; a job that wants to detect novel
failures should treat exit 0 as a fail.
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",
],
"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",
],
"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",
],
"context_required": ["kfilesystemtype", "determineFileSystemTypeImpl"],
"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",
],
"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.*KF6(?:CoreAddons|Config)",
],
"context_required": ["KF6CoreAddons", "KF6Config"],
"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",
],
"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",
],
"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",
],
"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": [
# Real cmake errors put QVariant and qApp on different lines;
# use [\s\S] (or re.DOTALL) to span. We deliberately do NOT
# require them to be on the same line.
r"QVariant[\s\S]{0,400}not declared[\s\S]{0,400}qApp[\s\S]{0,200}property",
],
"context_required": ["QString", "QCoreApplication"],
"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",
],
"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 _match_rules(log: str):
"""Return every RULES entry that matches `log`.
A rule matches when:
1. every regex in `rule["patterns"]` matches somewhere in the
log (AND across patterns), AND
2. every token in `rule["context_required"]` (if any) appears
as a substring of the log. The context gate prevents generic
C++ errors from triggering rules they don't apply to.
"""
matched = []
for rule in RULES:
patterns = rule["patterns"]
if not all(re.search(p, log) for p in patterns):
continue
context = rule.get("context_required")
if context and not all(token in log for token in context):
continue
matched.append(rule)
return matched
def classify(log: str) -> None:
matched = _match_rules(log)
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",
)
parser.add_argument(
"--explain-rule", metavar="NAME",
help="Print a single rule's name/patterns/fix/ref by name "
"(substring match). Useful when the generic guidance fires "
"and you want to see which rules would have applied.",
)
parser.add_argument(
"--json", action="store_true",
help="Emit a machine-readable JSON summary on stdout "
"(use for CI hooks or `make lint` integration).",
)
args = parser.parse_args()
if args.explain_rule:
needle = args.explain_rule.lower()
for rule in RULES:
if needle in rule["name"].lower():
print("=" * 70)
print(f"RULE: {rule['name']}")
print("=" * 70)
print("Patterns:")
for p in rule["patterns"]:
print(f" {p}")
if rule.get("context_required"):
print("Context required:")
for tok in rule["context_required"]:
print(f" {tok!r} must appear in the log")
print()
print("Fix:")
for line in rule["fix"].split("\n"):
print(f" {line}")
print()
print(f"Reference: {rule.get('ref', '(none)')}")
return 0
print(f"No rule matches {args.explain_rule!r}. Listing all rules:",
file=sys.stderr)
for rule in RULES:
print(f" - {rule['name']}", file=sys.stderr)
return 1
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)
if args.json:
import json
matched_rules = _match_rules(log)
matched = [{
"name": r["name"],
"patterns": list(r["patterns"]),
"context_required": r.get("context_required", []),
"fix": r["fix"],
"ref": r.get("ref", ""),
} for r in matched_rules]
print(json.dumps({
"log": str(log_path),
"matched": matched,
"matched_count": len(matched),
}, indent=2))
return 1 if matched else 0
classify(log)
if __name__ == "__main__":
sys.exit(main())