scripts: add check-cargo-patches.sh (Phase J verification + Improvement C)

The new `check-cargo-patches.sh` script verifies that all
[patch.crates-io] and [patch.'<URL>'] sections in the local
sources' Cargo.toml files actually resolve to the expected
local fork paths. It does this by running `cargo metadata`
on each source's workspace and checking that the
resolved source URL (or manifest_path for path-deps)
matches the expected local fork path.

This is the Phase J / Improvement C verification step
that the user explicitly requested: 'Build system must
report complete when upstream have our patches applied.'

The script handles the known-large workspaces gracefully:
* relibc is explicitly skipped — its [patch] section is
  only the cc-rs git branch override (no `path` patches),
  and `cargo metadata` on relibc takes minutes (hundreds
  of deps) which would hang the script.
* All other `cargo metadata` calls are wrapped in a
  30-second timeout.

Hardware-agnostic: works on any Red Bear OS checkout
regardless of which OEMs are added to the local sources
(Phase I/II/J DMI matches).
This commit is contained in:
2026-07-01 15:01:26 +03:00
parent 339cd4e223
commit 32403ccf4b
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env bash
# check-cargo-patches.sh — verify all [patch.crates-io] and [patch."<URL>"]
# sections in local sources' Cargo.toml resolve to the expected paths.
#
# Scans local/sources/*/Cargo.toml for [patch.*] sections and runs
# `cargo metadata` on each source to confirm the patch is applied
# (i.e. the resolved source URL matches the expected local path).
#
# This is the Phase J / Improvement C verification step. The cookbook's
# own patch validation handles file-level patches; this script handles
# the Cargo-level [patch] sections which are needed for transitive
# deps like redox_syscall, libredox, etc.
#
# Usage: ./local/scripts/check-cargo-patches.sh [--strict]
# --strict: exit non-zero if any patch fails verification (for CI)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
STRICT=false
[[ "${1:-}" == "--strict" ]] && STRICT=true
PASSED=0
FAILED=0
SKIPPED=0
echo "=== Cargo [patch] Verification ==="
# Find all Cargo.toml files in local sources
# Skip relibc explicitly — its [patch] section is only the
# cc-rs git branch override (no `path` patches), and
# `cargo metadata` on relibc takes minutes (hundreds of
# deps) which would hang the script.
while IFS= read -r -d '' cargo_toml; do
case "$cargo_toml" in
*local/sources/relibc/Cargo.toml) continue ;;
esac
source_dir="$(dirname "$cargo_toml")"
source_name="$(basename "$source_dir")"
relative_to_root="${cargo_toml#$ROOT/}"
# Find [patch.crates-io] and [patch."<URL>"] sections
patch_lines=$(grep -E '^\[patch\.' "$cargo_toml" 2>/dev/null || true)
if [[ -z "$patch_lines" ]]; then
continue
fi
echo ""
echo "--- $relative_to_root ---"
echo " $patch_lines" | sed 's/^/ /'
# Check if the source is a workspace member
if ! grep -q '^\[workspace\]' "$cargo_toml" 2>/dev/null; then
# Single-package — [patch] sections need [workspace] to apply
# (cargo treats them as ignored). This is a common bug.
if grep -q '^\[patch\.' "$cargo_toml" 2>/dev/null; then
echo " ⚠️ WARN: source has [patch] sections but no [workspace]"
echo " cargo silently ignores [patch] in non-workspace manifests"
SKIPPED=$((SKIPPED + 1))
fi
continue
fi
# Check if cargo is available
if ! command -v cargo >/dev/null 2>&1; then
echo " SKIP: cargo not available"
SKIPPED=$((SKIPPED + 1))
continue
fi
# Run cargo metadata and parse the resolved source URLs.
# Use a 30-second timeout to prevent hangs on large
# workspaces like relibc (which has hundreds of deps).
if ! timeout 30 cargo metadata --format-version 1 --manifest-path "$cargo_toml" >/dev/null 2>&1; then
echo " SKIP: cargo metadata timed out (30s)"
SKIPPED=$((SKIPPED + 1))
continue
fi
# Extract the resolved source URLs and check against the
# expected [patch] paths. The expected [patch] entries have
# a `path = "..."` field; the resolved source URL from
# cargo metadata should match the local path.
expected_paths=$(grep -A1 '^\[patch\.' "$cargo_toml" 2>/dev/null | grep -oP 'path\s*=\s*"\K[^"]+' | sort -u)
if [[ -z "$expected_paths" ]]; then
# No `path` entries in the [patch] sections. This is
# common for git-branch patches (e.g. relibc's cc-rs).
# The verification only applies to `path` patches.
# Continue to next file.
continue
fi
resolved_urls=$(cargo metadata --format-version 1 --manifest-path "$cargo_toml" 2>/dev/null \
| python3 -c '
import json, sys
data = json.load(sys.stdin)
# For path-deps, source is null but manifest_path points to
# the local fork. Print both forms.
for pkg in data.get("packages", []):
src = pkg.get("source", "")
if src:
print(src)
mp = pkg.get("manifest_path", "")
if mp:
print(mp)
' 2>/dev/null | sort -u)
good=0
bad=0
# Skip the resolution loop if there are no `path` entries.
# (Empty `expected_paths` would loop with empty lines.)
if [[ -n "$expected_paths" ]]; then
while IFS= read -r expected; do
# Skip empty lines (the heredoc may emit one)
if [[ -z "$expected" ]]; then
continue
fi
# expected is a path like "../syscall" or "../libredox"
# Resolve to absolute
expected_abs="$(cd "$source_dir" && realpath "$expected" 2>/dev/null || echo "")"
if [[ -z "$expected_abs" ]]; then
continue
fi
if echo "$resolved_urls" | grep -qF "$expected_abs"; then
good=$((good + 1))
else
# Check if the expected path is the source itself
if [[ "$expected_abs" == "$source_dir" ]]; then
# self-patch is OK
good=$((good + 1))
else
bad=$((bad + 1))
echo " ⚠️ [patch] path $expected$expected_abs NOT in resolved sources"
fi
fi
done <<< "$expected_paths"
fi
if [[ $bad -eq 0 ]]; then
echo " ✅ All $good [patch] entries resolved correctly"
PASSED=$((PASSED + 1))
else
echo "$bad [patch] entries did not resolve"
FAILED=$((FAILED + 1))
fi
done < <(find "$ROOT/local/sources" -name 'Cargo.toml' -print0 2>/dev/null)
echo ""
echo "=== Summary ==="
echo " Passed: $PASSED"
echo " Failed: $FAILED"
echo " Skipped: $SKIPPED"
if [[ $FAILED -gt 0 ]]; then
echo ""
echo "❌ Cargo [patch] verification FAILED"
$STRICT && exit 1
exit 1
fi
echo ""
echo "✅ Cargo [patch] verification PASSED"
exit 0