deps.sh (11614B)
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 37 # shellcheck disable=SC1091 38 . "${HERE}/channels.inc" 39 40 CHANNEL="${UNOS_CHANNEL:-lts}" 41 case "${1:-}" in 42 --channel) CHANNEL="${2:?--channel needs a value}"; shift 2 ;; 43 --channel=*) CHANNEL="${1#--channel=}"; shift ;; 44 esac 45 PKGDIR=$(chan_pkgdir "${CHANNEL}") 46 47 die() { printf 'deps.sh: error: %s\n' "$*" >&2; exit 1; } 48 49 case "${1:-}" in 50 channels|layers) ;; 51 *) [ -d "${PKGDIR}" ] \ 52 || die "channel '${CHANNEL}' not assembled; run ./mk/channel-assemble.sh ${CHANNEL}" ;; 53 esac 54 55 # Same PATH that mk/build.sh gives templates, so hostmakedepends like `muon` 56 # resolve from build/host/bin rather than appearing absent. 57 PATH="${ROOT}/build/host/bin:${PATH}" 58 export PATH 59 60 list_pkgs() { 61 for d in "${PKGDIR}"/*/; do 62 [ -f "${d}template" ] || continue 63 basename "${d}" 64 done | sort 65 } 66 67 # Print "depends|makedepends|hostmakedepends" for one package. 68 # Runs in a subshell: template assignments cannot leak into the caller. 69 read_vars() { 70 pkg="$1" 71 t="${PKGDIR}/${pkg}/template" 72 [ -f "${t}" ] || die "no such package: ${pkg}" 73 ( 74 depends= 75 makedepends= 76 hostmakedepends= 77 subpackages= 78 version= 79 revision=0 80 # Templates reference these; define them so sourcing cannot fail on an 81 # unset variable under `set -u`. 82 SYSROOT=; DESTDIR=; WRKSRC=; FILESDIR=; WORK=; SRCDEST= 83 # Real path: `.` is a special builtin, so a missing kernel.inc kills 84 # the subshell outright and every field comes back empty. 85 UNOS_MKDIR="${HERE}" 86 die() { :; } 87 msg() { :; } 88 vinstall() { :; } 89 vmkdir() { :; } 90 # shellcheck disable=SC1090 91 . "${t}" >/dev/null 2>&1 || true 92 printf '%s|%s|%s|%s|%s|%s\n' "${depends}" "${makedepends}" "${hostmakedepends}" \ 93 "${subpackages}" "${version}" "${revision}" 94 ) 95 } 96 97 field() { read_vars "$1" | cut -d'|' -f"$2"; } 98 99 # A makedepends may name a subpackage (linux-headers) rather than a 100 # template. The build graph needs the template that produces it. 101 owner_of() { 102 if [ -f "${PKGDIR}/$1/template" ]; then echo "$1"; return 0; fi 103 found= 104 for _o in $(list_pkgs); do 105 for _s in $(field "${_o}" 4); do 106 [ "${_s}" = "$1" ] || continue 107 [ -z "${found}" ] || die "'$1' is provided by both '${found}' and '${_o}'" 108 found="${_o}" 109 done 110 done 111 [ -n "${found}" ] || return 1 112 echo "${found}" 113 } 114 115 # makedepends of <pkg>, each resolved to its owning template. 116 makedeps_of() { 117 for _d in $(field "$1" 2); do 118 owner_of "${_d}" || die "$1: makedepends '${_d}' is not a package or subpackage" 119 done 120 } 121 122 cmd_vars() { 123 pkg="${1:?usage: deps.sh vars <pkg>}" 124 v=$(read_vars "${pkg}") 125 printf 'package: %s\n' "${pkg}" 126 printf 'depends: %s\n' "$(printf '%s' "${v}" | cut -d'|' -f1)" 127 printf 'makedepends: %s\n' "$(printf '%s' "${v}" | cut -d'|' -f2)" 128 printf 'hostmakedepends: %s\n' "$(printf '%s' "${v}" | cut -d'|' -f3)" 129 } 130 131 # Runtime depends of a package or subpackage, version pins intact. A 132 # subpackage's depends live in depends_<name with - as _>, not in `depends`. 133 cmd_rtdeps() { 134 name="${1:?usage: deps.sh rtdeps <name>}" 135 o=$(owner_of "${name}") || die "no such package: ${name}" 136 if [ "${o}" = "${name}" ]; then 137 field "${o}" 1 138 return 0 139 fi 140 u=$(printf '%s' "${name}" | tr '-' '_') 141 ( 142 depends=; makedepends=; hostmakedepends=; subpackages=; version=; revision=0 143 SYSROOT=; DESTDIR=; WRKSRC=; FILESDIR=; WORK=; SRCDEST= 144 UNOS_MKDIR="${HERE}" 145 die() { :; } ; msg() { :; } ; vinstall() { :; } ; vmkdir() { :; } 146 # shellcheck disable=SC1090 147 . "${PKGDIR}/${o}/template" >/dev/null 2>&1 || true 148 eval "printf '%s\n' \"\${depends_${u}:-}\"" 149 ) 150 } 151 152 cmd_graph() { 153 for p in $(list_pkgs); do 154 for d in $(makedeps_of "${p}"); do 155 printf '%s\t%s\n' "${p}" "${d}" 156 done 157 done 158 } 159 160 cmd_rdeps() { 161 # reverse dependencies: which packages have makedepends on <pkg> 162 [ $# -ge 1 ] || die "usage: deps.sh rdeps <pkg>" 163 target="$1" 164 for p in $(list_pkgs); do 165 for d in $(makedeps_of "${p}"); do 166 if [ "${d}" = "${target}" ]; then 167 echo "${p}" 168 break 169 fi 170 done 171 done 172 } 173 174 cmd_changed() { 175 # changed packages between two git refs: maps git diff paths to package names. 176 # ./mk/deps.sh changed <base> <head> 177 # 178 # Only channels/ and mk/ are considered, because that is all this repo 179 # holds. First-party source (linkd) now arrives as a tag-pinned tarball, so 180 # editing it produces no diff here at all: bumping the template's version 181 # is what marks it changed, and that is a channels/ path like any other. 182 # 183 # A layer the current channel does not compose is ignored: editing 184 # channels/edge/ does not dirty lts. Whether the edited layer actually wins 185 # is not decided here -- it is enough that it is in the chain. 186 # 187 # Compares <base>...<head> (three-dot, like GitHub PR diff). Falls back to 188 # two-dot if base is not an ancestor. 189 base="${1:-}" 190 head_ref="${2:-}" 191 [ -n "${base}" ] || die "usage: deps.sh changed <base> <head>" 192 [ -n "${head_ref}" ] || die "usage: deps.sh changed <base> <head>" 193 194 # git diff --name-only base...head, or base..head if ... yields nothing due 195 # to non-ancestor base. 196 changed_files=$(git -C "${ROOT}" diff --name-only "${base}...${head_ref}" 2>/dev/null || true) 197 if [ -z "${changed_files}" ]; then 198 changed_files=$(git -C "${ROOT}" diff --name-only "${base}..${head_ref}" 2>/dev/null || true) 199 fi 200 201 pkgs="" 202 mk_changed=0 203 layers=" $(chan_layers "${CHANNEL}" | tr '\n' ' ')" 204 205 for f in ${changed_files}; do 206 case "${f}" in 207 channels/*/packages/*) 208 # channels/<layer>/packages/<name>/{template,patches,files} 209 layer=$(printf '%s' "${f}" | cut -d/ -f2) 210 case "${layers}" in *" ${layer} "*) ;; *) continue ;; esac 211 pkg=$(printf '%s' "${f}" | cut -d/ -f4) 212 if [ -f "${PKGDIR}/${pkg}/template" ]; then 213 case " ${pkgs} " in *" ${pkg} "*) ;; *) pkgs="${pkgs} ${pkg}" ;; esac 214 fi 215 ;; 216 channels/*/channel.conf) 217 mk_changed=1 218 ;; 219 mk/*) 220 mk_changed=1 221 ;; 222 esac 223 done 224 225 for p in ${pkgs}; do echo "${p}"; done 226 227 # mk/ changes do not automatically trigger a full rebuild; they signal the 228 # conductor to warn. Emit a sentinel on stderr so the caller can decide. 229 if [ "${mk_changed}" = "1" ]; then 230 echo "note: mk/ changed - full rebuild not triggered automatically" >&2 231 fi 232 } 233 234 # Depth-first topological sort. State lives in a temp dir because POSIX sh has 235 # no associative arrays: <tmp>/seen.<pkg> marks done, <tmp>/path.<pkg> marks 236 # "currently on the recursion stack", which is how cycles are detected. 237 TMP= 238 cleanup() { [ -n "${TMP}" ] && rm -rf "${TMP}"; return 0; } 239 trap cleanup EXIT 240 241 # NOTE: everything here uses "$1" rather than a named variable. POSIX sh has no 242 # function-local variables, so a recursive function that assigns `pkg="$1"` 243 # has that assignment clobbered by its own recursive calls -- on return it 244 # would mark and emit the last child instead of itself, producing duplicates 245 # and silently dropping every package that has dependencies. Positional 246 # parameters, by contrast, are saved and restored per invocation. 247 visit() { 248 [ -e "${TMP}/seen.$1" ] && return 0 249 if [ -e "${TMP}/path.$1" ]; then 250 die "dependency cycle involving '$1'" 251 fi 252 [ -f "${PKGDIR}/$1/template" ] || die "'$1' is a makedepends of something but has no template in channel '${CHANNEL}'" 253 : > "${TMP}/path.$1" 254 for d in $(makedeps_of "$1"); do 255 visit "${d}" 256 done 257 rm -f "${TMP}/path.$1" 258 : > "${TMP}/seen.$1" 259 echo "$1" >> "${TMP}/order" 260 } 261 262 cmd_order() { 263 TMP=$(mktemp -d /tmp/unos-deps.XXXXXX) 264 : > "${TMP}/order" 265 if [ $# -gt 0 ]; then 266 for p in "$@"; do visit "${p}"; done 267 else 268 for p in $(list_pkgs); do visit "${p}"; done 269 fi 270 cat "${TMP}/order" 271 } 272 273 # Like visit(), but emits the names as written (subpackages stay subpackages) 274 # because these are apk package names to install, not templates to build. 275 seed_visit() { 276 [ -e "${TMP}/seen.$1" ] && return 0 277 : > "${TMP}/seen.$1" 278 for d in $(field "$(owner_of "$1")" 2); do 279 seed_visit "${d}" 280 done 281 echo "$1" >> "${TMP}/order" 282 } 283 284 cmd_seed() { 285 TMP=$(mktemp -d /tmp/unos-deps.XXXXXX) 286 : > "${TMP}/order" 287 if [ $# -gt 0 ]; then 288 for p in "$@"; do seed_visit "${p}"; done 289 else 290 for p in $(list_pkgs); do seed_visit "${p}"; done 291 fi 292 cat "${TMP}/order" 293 } 294 295 cmd_check() { 296 rc=0 297 TMP=$(mktemp -d /tmp/unos-deps.XXXXXX) 298 : > "${TMP}/order" 299 300 for p in $(list_pkgs); do 301 # every makedepends must name a real package 302 for d in $(field "${p}" 2); do 303 if ! owner_of "${d}" >/dev/null 2>&1; then 304 echo "FAIL: ${p}: makedepends '${d}' is not a package or subpackage" >&2 305 rc=1 306 fi 307 if [ "${d}" = "${p}" ]; then 308 echo "FAIL: ${p}: depends on itself" >&2 309 rc=1 310 fi 311 done 312 # hostmakedepends must be resolvable on this host 313 for h in $(field "${p}" 3); do 314 command -v "${h}" >/dev/null 2>&1 || { 315 echo "FAIL: ${p}: hostmakedepends '${h}' not found on PATH" >&2 316 rc=1 317 } 318 done 319 done 320 321 # cycle detection, via the same DFS the ordering uses 322 if [ "${rc}" = "0" ]; then 323 for p in $(list_pkgs); do visit "${p}"; done >/dev/null 2>&1 || { 324 echo "FAIL: dependency graph has a cycle" >&2 325 for p in $(list_pkgs); do visit "${p}" >/dev/null; done 326 rc=1 327 } 328 fi 329 330 [ "${rc}" = "0" ] && echo "deps.sh: graph OK ($(list_pkgs | wc -l | tr -d ' ') packages)" 331 return $rc 332 } 333 334 case "${1:-}" in 335 list) list_pkgs ;; 336 vars) shift; cmd_vars "$@" ;; 337 graph) cmd_graph ;; 338 order) shift; cmd_order "$@" ;; 339 seed) shift; cmd_seed "$@" ;; 340 owner) shift; owner_of "${1:?usage: deps.sh owner <name>}" ;; 341 subpkgs) shift; field "${1:?usage: deps.sh subpkgs <pkg>}" 4 ;; 342 rtdeps) shift; cmd_rtdeps "$@" ;; 343 channels) chan_targets ;; 344 layers) shift; chan_layers "${1:-${CHANNEL}}" | tr ' ' '\n' ;; 345 # A subpackage carries its owning template's version. 346 pkgver) shift 347 _o=$(owner_of "${1:?usage: deps.sh pkgver <name>}") || die "no such package: $1" 348 _v=$(field "${_o}" 5) 349 [ -n "${_v}" ] || die "${_o}: template sets no version" 350 printf '%s-r%s\n' "${_v}" "$(field "${_o}" 6)" 351 ;; 352 rdeps) shift; cmd_rdeps "$@" ;; 353 changed) shift; cmd_changed "$@" ;; 354 check) cmd_check ;; 355 *) sed -n '2,12p' "$0"; exit 1 ;; 356 esac