#!/usr/bin/env bash # check-cargo-patches.sh — verify all [patch.crates-io] and [patch.""] # 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.""] 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