test_deps.sh (9176B)
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 # Hand-maintaining the `needs` edges drifts, and the failure mode is a package 69 # building against a stale copy of what it links against -- quietly, and still 70 # succeeding. 71 [ -f "${ROOT}/.conductor.yml" ] || { echo "FAIL: no .conductor.yml" >&2; exit 1; } 72 out=$("${ROOT}/mk/gen-pipeline.sh" --check 2>&1) || { 73 echo "FAIL: .conductor.yml is out of date; run ./mk/gen-pipeline.sh --write" >&2 74 echo "$out" | head -20 >&2 75 exit 1 76 } 77 echo "PASS: .conductor.yml matches the dependency graph" 78 79 # Every package of every published channel must have a task, or a push would 80 # silently not build it. 81 expected=0 82 for c in $("${ROOT}/mk/deps.sh" channels); do 83 n=$("${ROOT}/mk/deps.sh" --channel "$c" list | wc -l | tr -d ' ') 84 expected=$((expected + n)) 85 ntask=$(grep -c "^ pkg-${c}-" "${ROOT}/.conductor.yml") 86 assert_eq "$ntask" "$n" "every ${c} package has a pipeline task" 87 grep -q "^ index-${c}:" "${ROOT}/.conductor.yml" \ 88 && echo "PASS: ${c} has an index task" \ 89 || { echo "FAIL: ${c} has no index task" >&2; exit 1; } 90 done 91 ntask=$(grep -c '^ pkg-' "${ROOT}/.conductor.yml") 92 assert_eq "$ntask" "$expected" "no pipeline task outside a published channel" 93 94 # --- builder image ----------------------------------------------------------- 95 # A stale tag is invisible: tasks keep running on the previously pushed image 96 # and only fail once something actually needs the missing tool. 97 read -r bver < "${ROOT}/builder/VERSION" 98 assert_file_contains "${ROOT}/.conductor.yml" "image: finwo/unos-builder:${bver}" \ 99 "pipeline default image matches builder/VERSION" 100 101 grep -q '^ builder:' "${ROOT}/.conductor.yml" \ 102 || { echo "FAIL: no builder task" >&2; exit 1; } 103 echo "PASS: pipeline has a builder task" 104 105 grep -q '^ - builder$' "${ROOT}/.conductor.yml" \ 106 || { echo "FAIL: check does not depend on builder" >&2; exit 1; } 107 echo "PASS: check runs after builder" 108 109 # The image bakes the toolchain; installing it again per task is the thing the 110 # builder exists to avoid. 111 if grep -q -- '- \./mk/ci-setup\.sh$' "${ROOT}/.conductor.yml"; then 112 echo "FAIL: a task installs the toolchain instead of using the builder image" >&2 113 grep -n -- '- \./mk/ci-setup\.sh$' "${ROOT}/.conductor.yml" | head -5 >&2 114 exit 1 115 fi 116 echo "PASS: no task reinstalls the toolchain" 117 118 assert_file_contains "${ROOT}/builder/Dockerfile" "ci-setup.sh --install-only" \ 119 "builder image is built from mk/ci-setup.sh, not a copy of its apt list" 120 121 # The credentials the runner provides, and nothing invented alongside them. 122 # The rest of the BUCKET_* set is read inside publish.sh and bootstrap-repo.sh, 123 # which the pipeline calls but does not parameterise. 124 assert_file_contains "${ROOT}/.conductor.yml" "BUCKET_PUBLIC_URL" "pipeline uses BUCKET_PUBLIC_URL" 125 if grep -q 'REPO_URL' "${ROOT}/.conductor.yml"; then 126 echo "FAIL: pipeline references REPO_URL, which the runner does not provide" >&2 127 exit 1 128 fi 129 echo "PASS: pipeline references only provided environment" 130 131 # $ARCH is only injected into tasks that declare arch; using it without is an 132 # empty string and a silently wrong repo URL. 133 if grep -q '\$ARCH' "${ROOT}/.conductor.yml"; then 134 nuse=$(grep -c '\$ARCH' "${ROOT}/.conductor.yml") 135 narch=$(grep -c '^ arch: \[' "${ROOT}/.conductor.yml") 136 [ "$narch" -gt 0 ] || { echo "FAIL: pipeline uses \$ARCH but no task declares arch" >&2; exit 1; } 137 echo "PASS: \$ARCH used in ${nuse} places, ${narch} tasks declare arch" 138 fi 139 140 # Feature names are a global namespace on the conductor, which hosts more than 141 # UNOS, so ours are project-scoped. A task asking for a feature no worker 142 # advertises is not scheduled at all -- it does not fail loudly, it simply 143 # never runs, which is far harder to diagnose than a build error. 144 if grep -q 'requires:.*[][ ]sign-key[],]' "${ROOT}/.conductor.yml"; then 145 echo "FAIL: pipeline requires bare 'sign-key'; the feature is 'unos-sign-key'" >&2 146 grep -n 'sign-key' "${ROOT}/.conductor.yml" | head -5 >&2 147 exit 1 148 fi 149 nsign=$(grep -c 'requires: \[unos-sign-key\]' "${ROOT}/.conductor.yml") 150 [ "${nsign}" -gt 0 ] || { echo "FAIL: no task requires unos-sign-key" >&2; exit 1; } 151 echo "PASS: ${nsign} tasks require the project-scoped unos-sign-key feature" 152 153 # Every task that signs must ask for the key. Building without it fails late, 154 # after the whole package has compiled. 155 nbuild=$(grep -c 'mk/build.sh' "${ROOT}/.conductor.yml") 156 [ "${nsign}" -ge "${nbuild}" ] || { 157 echo "FAIL: ${nbuild} tasks build but only ${nsign} require the signing key" >&2 158 exit 1 159 } 160 echo "PASS: every building task requires the signing key" 161 162 # --- build.sh actually reads the declarations ------------------------------ 163 assert_file_contains "${ROOT}/mk/build.sh" "hostmakedepends" "build.sh reads hostmakedepends" 164 assert_file_contains "${ROOT}/mk/build.sh" "makedepends" "build.sh reads makedepends" 165 166 # PATH ordering trap: hostmakedepends is checked before the build phases, so 167 # build/host/bin must already be on PATH or a package declaring `muon` fails 168 # its own check despite muon being present. 169 # Anchor on the executable check (`command -v "${_h}"`), not on the word 170 # hostmakedepends, which also appears in comments above it. 171 pathline=$(grep -n 'export PATH=.*build/host/bin' "${ROOT}/mk/build.sh" | head -1 | cut -d: -f1) 172 checkline=$(grep -n 'command -v "\${_h}"' "${ROOT}/mk/build.sh" | head -1 | cut -d: -f1) 173 if [ -z "$pathline" ] || [ -z "$checkline" ] || [ "$pathline" -ge "$checkline" ]; then 174 echo "FAIL: build/host/bin must be on PATH before hostmakedepends is checked (PATH line $pathline, check line $checkline)" >&2 175 exit 1 176 fi 177 echo "PASS: host tools are on PATH before hostmakedepends is checked" 178 179 echo "==> test_deps done"