unos-repository

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

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