unos-repository

APK repository for unos
git clone git://git.finwo.net/misc/unos-repository
Log | Files | Refs | README

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