cb424d7448
verify-patch-sanity.py validates every active recipe .patch has internally- consistent hunk line counts — catching the 'malformed patch at line N' failure at commit/CI/preflight time instead of hours into a cook. This cycle hit that class three times (qtwaylandscanner, sddm, xwayland), each only discovered when cookbook tried to apply the patch. Running it across the repo found 29 latent malformed patches (validated against GNU patch: e.g. relibc/P3-sysv-ipc reproduces 'malformed patch at line 22'). They were harmless only because they sit in vendored recipes (baked, not re- applied) — but would fail on any version-bump re-derivation. --fix recounts the hunk headers (body untouched) and repaired all 29. Wired into build-preflight.sh (Phase 1.0D) and redbear-ci.yml, with a unit test (test-patch-sanity.sh). Skips archived/legacy trees and unvalidatable formats (empty placeholders, bare-@@ git hunks).
227 lines
7.2 KiB
Python
Executable File
227 lines
7.2 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
import argparse
|
|
import itertools
|
|
import os
|
|
import re
|
|
import sys
|
|
from collections import defaultdict
|
|
|
|
from use_lldb_suite import lldb_root
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="Analyze LLDB project #include dependencies."
|
|
)
|
|
parser.add_argument(
|
|
"--show-counts",
|
|
default=False,
|
|
action="store_true",
|
|
help="When true, show the number of dependencies from each subproject",
|
|
)
|
|
parser.add_argument(
|
|
"--discover-cycles",
|
|
default=False,
|
|
action="store_true",
|
|
help="When true, find and display all project dependency cycles. Note,"
|
|
"this option is very slow",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
src_dir = os.path.join(lldb_root, "source")
|
|
inc_dir = os.path.join(lldb_root, "include")
|
|
|
|
src_map = {}
|
|
|
|
include_regex = re.compile('#include "((lldb|Plugins|clang)(.*/)+).*"')
|
|
|
|
|
|
def is_sublist(small, big):
|
|
it = iter(big)
|
|
return all(c in it for c in small)
|
|
|
|
|
|
def normalize_host(str):
|
|
if str.startswith("lldb/Host"):
|
|
return "lldb/Host"
|
|
if str.startswith("Plugins"):
|
|
return "lldb/" + str
|
|
if str.startswith("lldb/../../source"):
|
|
return str.replace("lldb/../../source", "lldb")
|
|
return str
|
|
|
|
|
|
def scan_deps(this_dir, file):
|
|
global src_map
|
|
deps = {}
|
|
this_dir = normalize_host(this_dir)
|
|
if this_dir in src_map:
|
|
deps = src_map[this_dir]
|
|
|
|
with open(file) as f:
|
|
for line in list(f):
|
|
m = include_regex.match(line)
|
|
if m is None:
|
|
continue
|
|
relative = m.groups()[0].rstrip("/")
|
|
if relative == this_dir:
|
|
continue
|
|
relative = normalize_host(relative)
|
|
if relative in deps:
|
|
deps[relative] += 1
|
|
elif relative != this_dir:
|
|
deps[relative] = 1
|
|
if this_dir not in src_map and len(deps) > 0:
|
|
src_map[this_dir] = deps
|
|
|
|
|
|
for base, dirs, files in os.walk(inc_dir):
|
|
dir = os.path.basename(base)
|
|
relative = os.path.relpath(base, inc_dir)
|
|
inc_files = [x for x in files if os.path.splitext(x)[1] in [".h"]]
|
|
relative = relative.replace("\\", "/")
|
|
for inc in inc_files:
|
|
inc_path = os.path.join(base, inc)
|
|
scan_deps(relative, inc_path)
|
|
|
|
for base, dirs, files in os.walk(src_dir):
|
|
dir = os.path.basename(base)
|
|
relative = os.path.relpath(base, src_dir)
|
|
src_files = [x for x in files if os.path.splitext(x)[1] in [".cpp", ".h", ".mm"]]
|
|
norm_base_path = os.path.normpath(os.path.join("lldb", relative))
|
|
norm_base_path = norm_base_path.replace("\\", "/")
|
|
for src in src_files:
|
|
src_path = os.path.join(base, src)
|
|
scan_deps(norm_base_path, src_path)
|
|
pass
|
|
|
|
|
|
def is_existing_cycle(path, cycles):
|
|
# If we have a cycle like # A -> B -> C (with an implicit -> A at the end)
|
|
# then we don't just want to check for an occurrence of A -> B -> C in the
|
|
# list of known cycles, but every possible rotation of A -> B -> C. For
|
|
# example, if we previously encountered B -> C -> A (with an implicit -> B
|
|
# at the end), then A -> B -> C is also a cycle. This is an important
|
|
# optimization which reduces the search space by multiple orders of
|
|
# magnitude.
|
|
for i in range(0, len(path)):
|
|
if any(is_sublist(x, path) for x in cycles):
|
|
return True
|
|
path = [path[-1]] + path[0:-1]
|
|
return False
|
|
|
|
|
|
def expand(path_queue, path_lengths, cycles, src_map):
|
|
# We do a breadth first search, to make sure we visit all paths in order
|
|
# of ascending length. This is an important optimization to make sure that
|
|
# short cycles are discovered first, which will allow us to discard longer
|
|
# cycles which grow the search space exponentially the longer they get.
|
|
while len(path_queue) > 0:
|
|
cur_path = path_queue.pop(0)
|
|
if is_existing_cycle(cur_path, cycles):
|
|
continue
|
|
|
|
next_len = path_lengths.pop(0) + 1
|
|
last_component = cur_path[-1]
|
|
|
|
for item in src_map.get(last_component, []):
|
|
if item.startswith("clang"):
|
|
continue
|
|
|
|
if item in cur_path:
|
|
# This is a cycle. Minimize it and then check if the result is
|
|
# already in the list of cycles. Insert it (or not) and then
|
|
# exit.
|
|
new_index = cur_path.index(item)
|
|
cycle = cur_path[new_index:]
|
|
if not is_existing_cycle(cycle, cycles):
|
|
cycles.append(cycle)
|
|
continue
|
|
|
|
path_lengths.append(next_len)
|
|
path_queue.append(cur_path + [item])
|
|
pass
|
|
|
|
|
|
cycles = []
|
|
|
|
path_queue = [[x] for x in iter(src_map)]
|
|
path_lens = [1] * len(path_queue)
|
|
|
|
items = list(src_map.items())
|
|
items.sort(key=lambda A: A[0])
|
|
|
|
for path, deps in items:
|
|
print(path + ":")
|
|
sorted_deps = list(deps.items())
|
|
if args.show_counts:
|
|
sorted_deps.sort(key=lambda A: (A[1], A[0]))
|
|
for dep in sorted_deps:
|
|
print("\t{} [{}]".format(dep[0], dep[1]))
|
|
else:
|
|
sorted_deps.sort(key=lambda A: A[0])
|
|
for dep in sorted_deps:
|
|
print("\t{}".format(dep[0]))
|
|
|
|
|
|
def iter_cycles(cycles):
|
|
global src_map
|
|
for cycle in cycles:
|
|
cycle.append(cycle[0])
|
|
zipper = list(zip(cycle[0:-1], cycle[1:]))
|
|
result = [(x, src_map[x][y], y) for (x, y) in zipper]
|
|
total = 0
|
|
smallest = result[0][1]
|
|
for first, value, last in result:
|
|
total += value
|
|
smallest = min(smallest, value)
|
|
yield (total, smallest, result)
|
|
|
|
|
|
if args.discover_cycles:
|
|
print("Analyzing cycles...")
|
|
|
|
expand(path_queue, path_lens, cycles, src_map)
|
|
|
|
average = sum([len(x) + 1 for x in cycles]) / len(cycles)
|
|
|
|
print("Found {} cycles. Average cycle length = {}.".format(len(cycles), average))
|
|
counted = list(iter_cycles(cycles))
|
|
if args.show_counts:
|
|
counted.sort(key=lambda A: A[0])
|
|
for total, smallest, cycle in counted:
|
|
sys.stdout.write("{} deps to break: ".format(total))
|
|
sys.stdout.write(cycle[0][0])
|
|
for first, count, last in cycle:
|
|
sys.stdout.write(" [{}->] {}".format(count, last))
|
|
sys.stdout.write("\n")
|
|
else:
|
|
for cycle in cycles:
|
|
cycle.append(cycle[0])
|
|
print(" -> ".join(cycle))
|
|
|
|
print("Analyzing islands...")
|
|
islands = []
|
|
outgoing_counts = defaultdict(int)
|
|
incoming_counts = defaultdict(int)
|
|
for total, smallest, cycle in counted:
|
|
for first, count, last in cycle:
|
|
outgoing_counts[first] += count
|
|
incoming_counts[last] += count
|
|
for cycle in cycles:
|
|
this_cycle = set(cycle)
|
|
disjoints = [x for x in islands if this_cycle.isdisjoint(x)]
|
|
overlaps = [x for x in islands if not this_cycle.isdisjoint(x)]
|
|
islands = disjoints + [set.union(this_cycle, *overlaps)]
|
|
print("Found {} disjoint cycle islands...".format(len(islands)))
|
|
for island in islands:
|
|
print("Island ({} elements)".format(len(island)))
|
|
sorted = []
|
|
for node in island:
|
|
sorted.append((node, incoming_counts[node], outgoing_counts[node]))
|
|
sorted.sort(key=lambda x: x[1] + x[2])
|
|
for node, inc, outg in sorted:
|
|
print(" {} [{} in, {} out]".format(node, inc, outg))
|
|
sys.stdout.flush()
|
|
pass
|