deps.sh (7949B)
1 #!/bin/sh 2 # mk/deps.sh - the build-dependency graph. 3 # 4 # ./mk/deps.sh list every package name 5 # ./mk/deps.sh vars <pkg> that package's declared dependencies 6 # ./mk/deps.sh order [pkg...] topological build order (all if no args) 7 # ./mk/deps.sh graph machine-readable "pkg<TAB>dep" edges 8 # ./mk/deps.sh rdeps <pkg> reverse makedepends (who needs <pkg>) 9 # ./mk/deps.sh changed <base> <head> packages changed between two refs 10 # ./mk/deps.sh check validate the whole graph, exit 1 if bad 11 # 12 # WHY THIS EXISTS 13 # --------------- 14 # packages/README.md has always documented hostmakedepends/makedepends, but 15 # nothing read them and no template set them, so build order lived only in 16 # whoever's head was driving the build. That is fine until CI, a fresh 17 # checkout, or a second person needs to build from nothing. 18 # 19 # Three kinds of dependency, deliberately distinct: 20 # 21 # depends runtime. Recorded in the .apk, resolved by apk on the 22 # switch. Does NOT affect build order. 23 # makedepends other UNOS packages whose headers/libs must be in 24 # build/sysroot before this one compiles. This is what 25 # determines build order. 26 # hostmakedepends commands that must exist on the build host (bison, 27 # flex, python3 for glibc...). Checked, never built. 28 # 29 # Templates are shell fragments, so reading them means sourcing them. That is 30 # exactly what mk/build.sh does; here it happens in a subshell that only ever 31 # reads variables, and the do_* functions are defined but never called. 32 set -eu 33 34 HERE=$(cd "$(dirname "$0")" && pwd) 35 ROOT=$(cd "${HERE}/.." && pwd) 36 PKGDIR="${ROOT}/packages" 37 38 die() { printf 'deps.sh: error: %s\n' "$*" >&2; exit 1; } 39 40 # Same PATH that mk/build.sh gives templates, so hostmakedepends like `muon` 41 # resolve from build/host/bin rather than appearing absent. 42 PATH="${ROOT}/build/host/bin:${PATH}" 43 export PATH 44 45 list_pkgs() { 46 for d in "${PKGDIR}"/*/; do 47 [ -f "${d}template" ] || continue 48 basename "${d}" 49 done | sort 50 } 51 52 # Print "depends|makedepends|hostmakedepends" for one package. 53 # Runs in a subshell: template assignments cannot leak into the caller. 54 read_vars() { 55 pkg="$1" 56 t="${PKGDIR}/${pkg}/template" 57 [ -f "${t}" ] || die "no such package: ${pkg}" 58 ( 59 depends= 60 makedepends= 61 hostmakedepends= 62 subpackages= 63 # Templates reference these; define them so sourcing cannot fail on an 64 # unset variable under `set -u`. 65 SYSROOT=; DESTDIR=; WRKSRC=; FILESDIR=; WORK=; SRCDEST=; UNOS_MKDIR= 66 die() { :; } 67 msg() { :; } 68 vinstall() { :; } 69 vmkdir() { :; } 70 # shellcheck disable=SC1090 71 . "${t}" >/dev/null 2>&1 || true 72 printf '%s|%s|%s\n' "${depends}" "${makedepends}" "${hostmakedepends}" 73 ) 74 } 75 76 field() { read_vars "$1" | cut -d'|' -f"$2"; } 77 78 cmd_vars() { 79 pkg="${1:?usage: deps.sh vars <pkg>}" 80 v=$(read_vars "${pkg}") 81 printf 'package: %s\n' "${pkg}" 82 printf 'depends: %s\n' "$(printf '%s' "${v}" | cut -d'|' -f1)" 83 printf 'makedepends: %s\n' "$(printf '%s' "${v}" | cut -d'|' -f2)" 84 printf 'hostmakedepends: %s\n' "$(printf '%s' "${v}" | cut -d'|' -f3)" 85 } 86 87 cmd_graph() { 88 for p in $(list_pkgs); do 89 for d in $(field "${p}" 2); do 90 printf '%s\t%s\n' "${p}" "${d}" 91 done 92 done 93 } 94 95 cmd_rdeps() { 96 # reverse dependencies: which packages have makedepends on <pkg> 97 [ $# -ge 1 ] || die "usage: deps.sh rdeps <pkg>" 98 target="$1" 99 for p in $(list_pkgs); do 100 for d in $(field "${p}" 2); do 101 if [ "${d}" = "${target}" ]; then 102 echo "${p}" 103 break 104 fi 105 done 106 done 107 } 108 109 cmd_changed() { 110 # changed packages between two git refs: maps git diff paths to package names. 111 # ./mk/deps.sh changed <base> <head> 112 # 113 # Only packages/ and mk/ are considered, because that is all this repo 114 # holds. First-party source (linkd) now arrives as a tag-pinned tarball, so 115 # editing it produces no diff here at all: bumping the template's version 116 # is what marks it changed, and that is a packages/ path like any other. 117 # Compares <base>...<head> (three-dot, like GitHub PR diff). Falls back to 118 # two-dot if base is not an ancestor. 119 base="${1:-}" 120 head_ref="${2:-}" 121 [ -n "${base}" ] || die "usage: deps.sh changed <base> <head>" 122 [ -n "${head_ref}" ] || die "usage: deps.sh changed <base> <head>" 123 124 # git diff --name-only base...head, or base..head if ... yields nothing due 125 # to non-ancestor base. 126 changed_files=$(git -C "${ROOT}" diff --name-only "${base}...${head_ref}" 2>/dev/null || true) 127 if [ -z "${changed_files}" ]; then 128 changed_files=$(git -C "${ROOT}" diff --name-only "${base}..${head_ref}" 2>/dev/null || true) 129 fi 130 131 pkgs="" 132 mk_changed=0 133 134 for f in ${changed_files}; do 135 case "${f}" in 136 packages/*) 137 # packages/<name>/template, patches/*, files/* 138 pkg=$(printf '%s' "${f}" | cut -d/ -f2) 139 if [ -f "${PKGDIR}/${pkg}/template" ]; then 140 case " ${pkgs} " in *" ${pkg} "*) ;; *) pkgs="${pkgs} ${pkg}" ;; esac 141 fi 142 ;; 143 mk/*) 144 mk_changed=1 145 ;; 146 esac 147 done 148 149 for p in ${pkgs}; do echo "${p}"; done 150 151 # mk/ changes do not automatically trigger a full rebuild; they signal the 152 # conductor to warn. Emit a sentinel on stderr so the caller can decide. 153 if [ "${mk_changed}" = "1" ]; then 154 echo "note: mk/ changed - full rebuild not triggered automatically" >&2 155 fi 156 } 157 158 # Depth-first topological sort. State lives in a temp dir because POSIX sh has 159 # no associative arrays: <tmp>/seen.<pkg> marks done, <tmp>/path.<pkg> marks 160 # "currently on the recursion stack", which is how cycles are detected. 161 TMP= 162 cleanup() { [ -n "${TMP}" ] && rm -rf "${TMP}"; } 163 trap cleanup EXIT 164 165 # NOTE: everything here uses "$1" rather than a named variable. POSIX sh has no 166 # function-local variables, so a recursive function that assigns `pkg="$1"` 167 # has that assignment clobbered by its own recursive calls -- on return it 168 # would mark and emit the last child instead of itself, producing duplicates 169 # and silently dropping every package that has dependencies. Positional 170 # parameters, by contrast, are saved and restored per invocation. 171 visit() { 172 [ -e "${TMP}/seen.$1" ] && return 0 173 if [ -e "${TMP}/path.$1" ]; then 174 die "dependency cycle involving '$1'" 175 fi 176 [ -f "${PKGDIR}/$1/template" ] || die "'$1' is a makedepends of something but has no template" 177 : > "${TMP}/path.$1" 178 for d in $(field "$1" 2); do 179 visit "${d}" 180 done 181 rm -f "${TMP}/path.$1" 182 : > "${TMP}/seen.$1" 183 echo "$1" >> "${TMP}/order" 184 } 185 186 cmd_order() { 187 TMP=$(mktemp -d /tmp/unos-deps.XXXXXX) 188 : > "${TMP}/order" 189 if [ $# -gt 0 ]; then 190 for p in "$@"; do visit "${p}"; done 191 else 192 for p in $(list_pkgs); do visit "${p}"; done 193 fi 194 cat "${TMP}/order" 195 } 196 197 cmd_check() { 198 rc=0 199 TMP=$(mktemp -d /tmp/unos-deps.XXXXXX) 200 : > "${TMP}/order" 201 202 for p in $(list_pkgs); do 203 # every makedepends must name a real package 204 for d in $(field "${p}" 2); do 205 if [ ! -f "${PKGDIR}/${d}/template" ]; then 206 echo "FAIL: ${p}: makedepends '${d}' is not a package" >&2 207 rc=1 208 fi 209 if [ "${d}" = "${p}" ]; then 210 echo "FAIL: ${p}: depends on itself" >&2 211 rc=1 212 fi 213 done 214 # hostmakedepends must be resolvable on this host 215 for h in $(field "${p}" 3); do 216 command -v "${h}" >/dev/null 2>&1 || { 217 echo "FAIL: ${p}: hostmakedepends '${h}' not found on PATH" >&2 218 rc=1 219 } 220 done 221 done 222 223 # cycle detection, via the same DFS the ordering uses 224 if [ "${rc}" = "0" ]; then 225 for p in $(list_pkgs); do visit "${p}"; done >/dev/null 2>&1 || { 226 echo "FAIL: dependency graph has a cycle" >&2 227 for p in $(list_pkgs); do visit "${p}" >/dev/null; done 228 rc=1 229 } 230 fi 231 232 [ "${rc}" = "0" ] && echo "deps.sh: graph OK ($(list_pkgs | wc -l | tr -d ' ') packages)" 233 return $rc 234 } 235 236 case "${1:-}" in 237 list) list_pkgs ;; 238 vars) shift; cmd_vars "$@" ;; 239 graph) cmd_graph ;; 240 order) shift; cmd_order "$@" ;; 241 rdeps) shift; cmd_rdeps "$@" ;; 242 changed) shift; cmd_changed "$@" ;; 243 check) cmd_check ;; 244 *) sed -n '2,12p' "$0"; exit 1 ;; 245 esac