#!/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//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", 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//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", 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// 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", 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/.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": [ 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 - <',\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", 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 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", ) 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()