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).
177 lines
4.9 KiB
Python
177 lines
4.9 KiB
Python
import os
|
|
import sys
|
|
import time
|
|
|
|
from subprocess import CalledProcessError, check_call
|
|
from typing import List, IO, Optional, Tuple
|
|
|
|
|
|
def which(command: str, paths: Optional[str] = None) -> Optional[str]:
|
|
"""which(command, [paths]) - Look up the given command in the paths string
|
|
(or the PATH environment variable, if unspecified)."""
|
|
|
|
if paths is None:
|
|
paths = os.environ.get("PATH", "")
|
|
|
|
# Check for absolute match first.
|
|
if os.path.exists(command):
|
|
return command
|
|
|
|
# Would be nice if Python had a lib function for this.
|
|
if not paths:
|
|
paths = os.defpath
|
|
|
|
# Get suffixes to search.
|
|
# On Cygwin, 'PATHEXT' may exist but it should not be used.
|
|
if os.pathsep == ";":
|
|
pathext = os.environ.get("PATHEXT", "").split(";")
|
|
else:
|
|
pathext = [""]
|
|
|
|
# Search the paths...
|
|
for path in paths.split(os.pathsep):
|
|
for ext in pathext:
|
|
p = os.path.join(path, command + ext)
|
|
if os.path.exists(p):
|
|
return p
|
|
|
|
return None
|
|
|
|
|
|
def has_no_extension(file_name: str) -> bool:
|
|
root, ext = os.path.splitext(file_name)
|
|
return ext == ""
|
|
|
|
|
|
def is_valid_single_input_file(file_name: str) -> bool:
|
|
root, ext = os.path.splitext(file_name)
|
|
return ext in (".i", ".ii", ".c", ".cpp", ".m", "")
|
|
|
|
|
|
def time_to_str(time: float) -> str:
|
|
"""
|
|
Convert given time in seconds into a human-readable string.
|
|
"""
|
|
return f"{time:.2f}s"
|
|
|
|
|
|
def memory_to_str(memory: int) -> str:
|
|
"""
|
|
Convert given number of bytes into a human-readable string.
|
|
"""
|
|
if memory:
|
|
try:
|
|
import humanize
|
|
|
|
return humanize.naturalsize(memory, gnu=True)
|
|
except ImportError:
|
|
# no formatter installed, let's keep it in bytes
|
|
return f"{memory}B"
|
|
|
|
# If memory is 0, we didn't succeed measuring it.
|
|
return "N/A"
|
|
|
|
|
|
def check_and_measure_call(*popenargs, **kwargs) -> Tuple[float, int]:
|
|
"""
|
|
Run command with arguments. Wait for command to complete and measure
|
|
execution time and peak memory consumption.
|
|
If the exit code was zero then return, otherwise raise
|
|
CalledProcessError. The CalledProcessError object will have the
|
|
return code in the returncode attribute.
|
|
|
|
The arguments are the same as for the call and check_call functions.
|
|
|
|
Return a tuple of execution time and peak memory.
|
|
"""
|
|
peak_mem = 0
|
|
start_time = time.time()
|
|
|
|
try:
|
|
import psutil as ps
|
|
|
|
def get_memory(process: ps.Process) -> int:
|
|
mem = 0
|
|
|
|
# we want to gather memory usage from all of the child processes
|
|
descendants = list(process.children(recursive=True))
|
|
descendants.append(process)
|
|
|
|
for subprocess in descendants:
|
|
try:
|
|
mem += subprocess.memory_info().rss
|
|
except (ps.NoSuchProcess, ps.AccessDenied):
|
|
continue
|
|
|
|
return mem
|
|
|
|
with ps.Popen(*popenargs, **kwargs) as process:
|
|
# while the process is running calculate resource utilization.
|
|
while process.is_running() and process.status() != ps.STATUS_ZOMBIE:
|
|
# track the peak utilization of the process
|
|
peak_mem = max(peak_mem, get_memory(process))
|
|
time.sleep(0.5)
|
|
|
|
if process.is_running():
|
|
process.kill()
|
|
|
|
if process.returncode != 0:
|
|
cmd = kwargs.get("args")
|
|
if cmd is None:
|
|
cmd = popenargs[0]
|
|
raise CalledProcessError(process.returncode, cmd)
|
|
|
|
except ImportError:
|
|
# back off to subprocess if we don't have psutil installed
|
|
peak_mem = 0
|
|
check_call(*popenargs, **kwargs)
|
|
|
|
return time.time() - start_time, peak_mem
|
|
|
|
|
|
def run_script(
|
|
script_path: str,
|
|
build_log_file: IO,
|
|
cwd: str,
|
|
out=sys.stdout,
|
|
err=sys.stderr,
|
|
verbose: int = 0,
|
|
):
|
|
"""
|
|
Run the provided script if it exists.
|
|
"""
|
|
if os.path.exists(script_path):
|
|
try:
|
|
if verbose == 1:
|
|
out.write(f" Executing: {script_path}\n")
|
|
|
|
check_call(
|
|
f"chmod +x '{script_path}'",
|
|
cwd=cwd,
|
|
stderr=build_log_file,
|
|
stdout=build_log_file,
|
|
shell=True,
|
|
)
|
|
|
|
check_call(
|
|
f"'{script_path}'",
|
|
cwd=cwd,
|
|
stderr=build_log_file,
|
|
stdout=build_log_file,
|
|
shell=True,
|
|
)
|
|
|
|
except CalledProcessError:
|
|
err.write(
|
|
f"Error: Running {script_path} failed. "
|
|
f"See {build_log_file.name} for details.\n"
|
|
)
|
|
sys.exit(-1)
|
|
|
|
|
|
def is_comment_csv_line(entries: List[str]) -> bool:
|
|
"""
|
|
Treat CSV lines starting with a '#' as a comment.
|
|
"""
|
|
return len(entries) > 0 and entries[0].startswith("#")
|