test_deps.sh (7534B)
1 #!/bin/sh 2 # test_deps.sh - the build-dependency graph and host bootstrap stay honest. 3 # 4 # Both of these were gaps that only bite in CI or on a fresh checkout, which is 5 # exactly when nobody is around to debug them: 6 # - build/host/bin/{apk,muon} were built by hand from prose in mk/README.md 7 # - hostmakedepends/makedepends were documented but read by nothing 8 set -eu 9 10 HERE=$(cd "$(dirname "$0")" && pwd) 11 ROOT=$(cd "${HERE}/../.." && pwd) 12 . "${ROOT}/tests/helpers.sh" 13 14 echo "==> test_deps: build graph + host bootstrap" 15 16 # --- the graph itself ------------------------------------------------------ 17 out=$("${ROOT}/mk/deps.sh" check 2>&1) || { echo "FAIL: deps.sh check: $out" >&2; exit 1; } 18 assert_contains "$out" "graph OK" "dependency graph validates (no cycles, no dangling makedepends)" 19 20 # Ordering must be a real topological sort, not just a list. Every package must 21 # appear exactly once, and every makedepends strictly before its dependent. 22 order=$("${ROOT}/mk/deps.sh" order) 23 n_all=$("${ROOT}/mk/deps.sh" list | wc -l | tr -d ' ') 24 n_ord=$(printf '%s\n' "$order" | wc -l | tr -d ' ') 25 n_uniq=$(printf '%s\n' "$order" | sort -u | wc -l | tr -d ' ') 26 assert_eq "$n_ord" "$n_all" "every package appears in the build order" 27 assert_eq "$n_uniq" "$n_all" "no package appears twice in the build order" 28 29 # This is the property that actually matters, and the one a naive recursive 30 # shell function gets wrong (no function-local variables in POSIX sh). 31 fail=0 32 "${ROOT}/mk/deps.sh" graph | while IFS="$(printf '\t')" read -r pkg dep; do 33 [ -n "$pkg" ] || continue 34 pos_pkg=$(printf '%s\n' "$order" | grep -nxF "$pkg" | cut -d: -f1) 35 pos_dep=$(printf '%s\n' "$order" | grep -nxF "$dep" | cut -d: -f1) 36 if [ -z "$pos_dep" ] || [ -z "$pos_pkg" ] || [ "$pos_dep" -ge "$pos_pkg" ]; then 37 echo "FAIL: $dep must be built before $pkg (positions $pos_dep vs $pos_pkg)" >&2 38 exit 1 39 fi 40 done || fail=1 41 [ "$fail" = "0" ] || exit 1 42 echo "PASS: every makedepends is ordered before its dependent" 43 44 # The graph must not be vacuously empty -- that was the original bug report. 45 edges=$("${ROOT}/mk/deps.sh" graph | wc -l | tr -d ' ') 46 if [ "$edges" -lt 5 ]; then 47 echo "FAIL: dependency graph has only ${edges} edges; templates are not declaring makedepends" >&2 48 exit 1 49 fi 50 echo "PASS: graph is populated (${edges} edges)" 51 52 # --- host bootstrap -------------------------------------------------------- 53 [ -x "${ROOT}/mk/bootstrap-host.sh" ] || { echo "FAIL: mk/bootstrap-host.sh missing" >&2; exit 1; } 54 out=$("${ROOT}/mk/bootstrap-host.sh" --check 2>&1) || { echo "FAIL: host tools not bootstrapped: $out" >&2; exit 1; } 55 assert_contains "$out" "host tools present and pinned" "bootstrap-host.sh --check passes" 56 57 # --check must verify VERSION, not just existence: a stale binary from an older 58 # pin is worse than a missing one because everything downstream still "works". 59 assert_file_contains "${ROOT}/mk/bootstrap-host.sh" "MUON_SHA256" "muon is pinned by sha256" 60 assert_file_contains "${ROOT}/mk/bootstrap-host.sh" "APK_SHA256" "apk-tools is pinned by sha256" 61 62 # The bootstrap must not reach the network beyond its pinned tarballs. muon's 63 # setup git-clones meson-docs/meson-tests unless they are disabled. 64 assert_file_contains "${ROOT}/mk/bootstrap-host.sh" "-Dmeson-docs=disabled" "muon docs subproject disabled (no build-time git fetch)" 65 assert_file_contains "${ROOT}/mk/bootstrap-host.sh" "-Dmeson-tests=disabled" "muon tests subproject disabled (no build-time git fetch)" 66 67 # --- the CI pipeline is generated from that same graph --------------------- 68 # .conductor.yml carries ~30 tasks and their `needs` edges. Hand-maintaining 69 # those against packages/*/template drifts, and the failure mode is a package 70 # building before the library it links against -- which usually still succeeds, 71 # quietly, against a stale copy. 72 [ -f "${ROOT}/.conductor.yml" ] || { echo "FAIL: no .conductor.yml" >&2; exit 1; } 73 out=$("${ROOT}/mk/gen-pipeline.sh" --check 2>&1) || { 74 echo "FAIL: .conductor.yml is out of date; run ./mk/gen-pipeline.sh --write" >&2 75 echo "$out" | head -20 >&2 76 exit 1 77 } 78 echo "PASS: .conductor.yml matches the dependency graph" 79 80 # Every package must have a task, or a push would silently not build it. 81 npkg=$("${ROOT}/mk/deps.sh" list | wc -l | tr -d ' ') 82 ntask=$(grep -c '^ pkg-' "${ROOT}/.conductor.yml") 83 assert_eq "$ntask" "$npkg" "every package has a pipeline task" 84 85 # The credentials the runner provides, and nothing invented alongside them. 86 assert_file_contains "${ROOT}/.conductor.yml" "BUCKET_ENDPOINT" "pipeline uses BUCKET_ENDPOINT" 87 assert_file_contains "${ROOT}/.conductor.yml" "BUCKET_NAME" "pipeline uses BUCKET_NAME" 88 if grep -q 'REPO_URL' "${ROOT}/.conductor.yml"; then 89 echo "FAIL: pipeline references REPO_URL, which the runner does not provide" >&2 90 exit 1 91 fi 92 echo "PASS: pipeline references only provided environment" 93 94 # $ARCH is only injected into tasks that declare arch; using it without is an 95 # empty string and a silently wrong repo URL. 96 if grep -q '\$ARCH' "${ROOT}/.conductor.yml"; then 97 nuse=$(grep -c '\$ARCH' "${ROOT}/.conductor.yml") 98 narch=$(grep -c '^ arch: \[' "${ROOT}/.conductor.yml") 99 [ "$narch" -gt 0 ] || { echo "FAIL: pipeline uses \$ARCH but no task declares arch" >&2; exit 1; } 100 echo "PASS: \$ARCH used in ${nuse} places, ${narch} tasks declare arch" 101 fi 102 103 # Feature names are a global namespace on the conductor, which hosts more than 104 # UNOS, so ours are project-scoped. A task asking for a feature no worker 105 # advertises is not scheduled at all -- it does not fail loudly, it simply 106 # never runs, which is far harder to diagnose than a build error. 107 if grep -q 'requires:.*[][ ]sign-key[],]' "${ROOT}/.conductor.yml"; then 108 echo "FAIL: pipeline requires bare 'sign-key'; the feature is 'unos-sign-key'" >&2 109 grep -n 'sign-key' "${ROOT}/.conductor.yml" | head -5 >&2 110 exit 1 111 fi 112 nsign=$(grep -c 'requires: \[unos-sign-key\]' "${ROOT}/.conductor.yml") 113 [ "${nsign}" -gt 0 ] || { echo "FAIL: no task requires unos-sign-key" >&2; exit 1; } 114 echo "PASS: ${nsign} tasks require the project-scoped unos-sign-key feature" 115 116 # Every task that signs must ask for the key. Building without it fails late, 117 # after the whole package has compiled. 118 nbuild=$(grep -c 'mk/build.sh' "${ROOT}/.conductor.yml") 119 [ "${nsign}" -ge "${nbuild}" ] || { 120 echo "FAIL: ${nbuild} tasks build but only ${nsign} require the signing key" >&2 121 exit 1 122 } 123 echo "PASS: every building task requires the signing key" 124 125 # --- build.sh actually reads the declarations ------------------------------ 126 assert_file_contains "${ROOT}/mk/build.sh" "hostmakedepends" "build.sh reads hostmakedepends" 127 assert_file_contains "${ROOT}/mk/build.sh" "makedepends" "build.sh reads makedepends" 128 129 # PATH ordering trap: hostmakedepends is checked before the build phases, so 130 # build/host/bin must already be on PATH or a package declaring `muon` fails 131 # its own check despite muon being present. 132 # Anchor on the executable check (`command -v "${_h}"`), not on the word 133 # hostmakedepends, which also appears in comments above it. 134 pathline=$(grep -n 'export PATH=.*build/host/bin' "${ROOT}/mk/build.sh" | head -1 | cut -d: -f1) 135 checkline=$(grep -n 'command -v "\${_h}"' "${ROOT}/mk/build.sh" | head -1 | cut -d: -f1) 136 if [ -z "$pathline" ] || [ -z "$checkline" ] || [ "$pathline" -ge "$checkline" ]; then 137 echo "FAIL: build/host/bin must be on PATH before hostmakedepends is checked (PATH line $pathline, check line $checkline)" >&2 138 exit 1 139 fi 140 echo "PASS: host tools are on PATH before hostmakedepends is checked" 141 142 echo "==> test_deps done"