ffbe098ef8
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.
457 lines
17 KiB
Python
Executable File
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())
|