#!/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//target/x86_64-unknown-redox/sysroot\n" " repo cook # 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//target/x86_64-unknown-redox/sysroot\n" " repo cook " ), "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// no longer " "applies to the current upstream. Run:\n" " ./local/scripts/audit-patch-idempotency.py --component \n" "to confirm. Re-generate the patch from a fresh checkout:\n" " cd /tmp/audit-fresh && git clone src && cd src && git checkout \n" " # apply your changes, then:\n" " git diff > /local/patches//NN-fix.patch" ), "ref": "AGENTS.md §\"NO OVERLAY-STYLE PATCHES\" Rule 2", }, { "name": "Package 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/.pkgar\n" "If missing, cook the dep first:\n" " repo cook " ), "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 - <',\n" " '#include \\n#include ') ..." ), "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 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//target/x86_64-unknown-redox/sysroot") print(" 4. Re-fetch source: rm -rf local/recipes/kde//source && repo fetch ") 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())