Page MenuHomeFreeBSD

D59425.diff
No OneTemporary

D59425.diff

diff --git a/packages/bsdpv/bsdpv.ucl b/packages/bsdpv/bsdpv.ucl
--- a/packages/bsdpv/bsdpv.ucl
+++ b/packages/bsdpv/bsdpv.ucl
@@ -12,5 +12,7 @@
terminal, or as a full-screen gauge via bsddialog(3).
Also provided is bsdpv(3) (libprivatebsdpv), a private library used by
-bsdpv(1) and by other base utilities that need a progress view.
+bsdpv(1) and by other base utilities that need a progress view, and
+srcpv(1), which shows buildworld and buildkernel progress from the
+output of make(1), read from a file or snooped from its terminal.
EOD
diff --git a/targets/pseudo/userland/Makefile.depend b/targets/pseudo/userland/Makefile.depend
--- a/targets/pseudo/userland/Makefile.depend
+++ b/targets/pseudo/userland/Makefile.depend
@@ -325,6 +325,7 @@
usr.bin/soelim \
usr.bin/sort \
usr.bin/split \
+ usr.bin/srcpv \
usr.bin/ssh-copy-id \
usr.bin/stat \
usr.bin/stdbuf \
diff --git a/usr.bin/Makefile b/usr.bin/Makefile
--- a/usr.bin/Makefile
+++ b/usr.bin/Makefile
@@ -139,6 +139,7 @@
soelim \
sort \
split \
+ srcpv \
stat \
stdbuf \
strings \
diff --git a/usr.bin/srcpv/Makefile b/usr.bin/srcpv/Makefile
new file mode 100644
--- /dev/null
+++ b/usr.bin/srcpv/Makefile
@@ -0,0 +1,6 @@
+PACKAGE= bsdpv
+
+SCRIPTS= srcpv
+MAN= srcpv.1
+
+.include <bsd.prog.mk>
diff --git a/usr.bin/srcpv/Makefile.depend b/usr.bin/srcpv/Makefile.depend
new file mode 100644
--- /dev/null
+++ b/usr.bin/srcpv/Makefile.depend
@@ -0,0 +1,10 @@
+# Autogenerated - do NOT edit!
+
+DIRDEPS = \
+
+
+.include <dirdeps.mk>
+
+.if ${DEP_RELDIR} == ${_DEP_RELDIR}
+# local dependencies - needed for -jN in clean tree
+.endif
diff --git a/usr.bin/srcpv/srcpv b/usr.bin/srcpv/srcpv
new file mode 100755
--- /dev/null
+++ b/usr.bin/srcpv/srcpv
@@ -0,0 +1,1667 @@
+#!/bin/sh
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2026 Devin Teske <dteske@FreeBSD.org>
+#
+############################################################ IDENT(1)
+#
+# $Title: Progress bar for make(1) targets in the FreeBSD source tree $
+#
+############################################################ GLOBALS
+
+VERSION='$Version: 1.0 $'
+
+pgm="${0##*/}" # Program basename
+
+#
+# Global exit status
+#
+SUCCESS=0
+FAILURE=1
+
+#
+# Command-line arguments
+# NB: universe and release are planned
+#
+MODE= # buildworld or buildkernel
+
+#
+# Command-line options
+#
+INPUT= # -f file
+REFRESH_CACHE= # -R
+SRCROOT=/usr/src # -s dir
+WATCH_TTY= # -w tty
+
+#
+# Environment
+#
+CACHE_DIR=${SRCPV_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/srcpv}
+HIGHWATER_MAXAGE=${SRCPV_HIGHWATER_MAXAGE:-21600} # seconds
+STAGE_WEIGHT=${SRCPV_STAGE_WEIGHT:-32} # ticks per non-library stage
+: ${TARGET=} ${TARGET_ARCH=} # cross-build target; default host
+: ${KERNCONF=} # kernel configuration(s); default GENERIC
+
+#
+# Miscellaneous
+#
+SCHEDULE_FORMAT=1 # cache file format
+SUDO= # sudo(8) when watch(8) needs it
+export SUDO_PROMPT="[sudo] Password for watch(8):"
+WORKDIR= # mktemp -d
+
+############################################################ FUNCTIONS
+
+die() { exec >&2; [ "$*" ] && echo "$pgm:" "$@"; exit $FAILURE; }
+
+usage()
+{
+ local modefmt="\t%-11s %s\n" optfmt="\t%-7s %s\n"
+ exec >&2
+ [ "$*" ] && printf "%s: %s\n" "$pgm" "$*"
+ printf "Usage: %s mode [-Rv] [-s dir] -f file\n" "$pgm"
+ printf " %s mode [-Rv] [-s dir] -w tty\n" "$pgm"
+ printf " %s -v\n" "$pgm"
+ printf "Modes:\n"
+ printf "$modefmt" "buildworld" "Follow make(1) buildworld."
+ printf "$modefmt" "buildkernel" "Follow make(1) buildkernel."
+ printf "Options:\n"
+ printf "$optfmt" "-f file" \
+ "Read make(1) output from file, FIFO, or \`-' (stdin)."
+ printf "$optfmt" "-h" "Print this usage statement and exit."
+ printf "$optfmt" "-R" "Refresh the schedule cache."
+ printf "$optfmt" "-s dir" "Source tree (default \`/usr/src')."
+ printf "$optfmt" "-v" "Print version information and exit."
+ printf "$optfmt" "-w tty" "Terminal running make(1); e.g., \`pts/1'."
+ exit $FAILURE
+}
+
+# list_objects $dir
+#
+# Print the object names (.o, .pico, and .nossppico) expected from the
+# compilable sources under $dir, excluding tests and documentation.
+#
+list_objects()
+{
+ find "$1" \
+ ! -path '*/.git/*' \
+ ! -path '*/.svn/*' \
+ ! -path '*/CVS/*' \
+ ! -path '*/tests/*' \
+ ! -path '*/test/*' \
+ ! -path '*/unittests/*' \
+ ! -path '*/UnitTests/*' \
+ ! -path '*/examples/*' \
+ ! -path '*/Documentation/*' \
+ ! -path '*/docs/*' \
+ ! -path '*/doc/*' \
+ \( \
+ -name '*.c' -o \
+ -name '*.cc' -o \
+ -name '*.cpp' -o \
+ -name '*.cxx' -o \
+ -name '*.C' -o \
+ -name '*.s' -o \
+ -name '*.S' -o \
+ -name '*.m' -o \
+ -name '*.mm' \
+ \) -print 2> /dev/null |
+ awk '
+ {
+ sub(/\.[^.\/]+$/, "")
+ print $0 ".o"
+ print $0 ".pico"
+ print $0 ".nossppico"
+ }
+ ' # END-QUOTE
+}
+
+# list_archives $dir
+#
+# Print the archive names (lib*.a and its PIC/PIE variants) expected from
+# the Makefiles under $dir that set LIB.
+#
+list_archives()
+{
+ find "$1" \
+ ! -path '*/.git/*' \
+ ! -path '*/.svn/*' \
+ ! -path '*/CVS/*' \
+ ! -path '*/tests/*' \
+ ! -path '*/test/*' \
+ ! -path '*/sys/*' \
+ ! -path '*/stand/*' \
+ -name Makefile -exec awk '
+ FNR == 1 { private = 0 }
+ /^PRIVATELIB([[:space:]]|=)/ { private = 1 }
+ /^LIB[[:space:]]*=/ {
+ lib = $0
+ sub(/^[^=]*=[[:space:]]*/, "", lib)
+ sub(/[[:space:]]*#.*$/, "", lib)
+ sub(/[[:space:]].*$/, "", lib)
+ if (lib == "" || lib ~ /\$/)
+ next
+ prefix = private ? "libprivate" : "lib"
+ print prefix lib ".a"
+ print prefix lib "_pic.a"
+ print prefix lib "_nossp_pic.a"
+ print prefix lib "_pie.a"
+ }
+ ' {} + 2> /dev/null
+}
+
+# unit_weight $srcdir $unit
+#
+# Print the number of distinct objects and archives expected from one
+# schedule unit (minimum 1).
+#
+unit_weight()
+{
+ local srcdir="$1" unit="$2"
+ local dir="$srcdir/$unit"
+
+ case "$unit" in
+ lib/clang|usr.bin/clang)
+ #
+ # Clang/LLVM objects are compiled from contrib/llvm-project,
+ # not from the thin lib/clang and usr.bin/clang wrappers.
+ #
+ [ -d "$srcdir/contrib/llvm-project" ] &&
+ dir=$srcdir/contrib/llvm-project
+ ;;
+ esac
+ {
+ list_objects "$dir"
+ list_archives "$dir"
+ } | awk '! seen[$0]++ { n++ } END { print (n > 0 ? n : 1) }'
+}
+
+# schedule_cache_file $mode $srcdir $var_to_set
+#
+# Set $var_to_set to the schedule cache path for $mode and $srcdir.
+#
+schedule_cache_file()
+{
+ local __mode="$1" __srcdir="$2" __var_to_set="$3"
+ local __id
+
+ __id=$( printf '%s' "$__srcdir" | tr -c 'A-Za-z0-9._-' '_' )
+ eval $__var_to_set=\"\$CACHE_DIR/\$__mode.\$__id.schedule\"
+}
+
+# schedule_markers $srcdir [$file ...]
+#
+# Print path:mtime pairs for the makefiles that shape every schedule, the
+# system-wide make configuration, and any extra $file; a cache is stale
+# when one of them changes.
+#
+schedule_markers()
+{
+ local srcdir="$1"
+ local file mtime
+
+ shift 1 # srcdir
+ for file in \
+ "$srcdir/Makefile" \
+ "$srcdir/Makefile.inc1" \
+ "$srcdir/Makefile.libcompat" \
+ "$srcdir/share/mk/src.opts.mk" \
+ /etc/make.conf \
+ /etc/src.conf \
+ /etc/src-env.conf \
+ "$@"
+ do
+ mtime=$( stat -f %m "$file" 2> /dev/null ) || mtime=-
+ printf '%s:%s ' "$file" "$mtime"
+ done
+ printf '\n'
+}
+
+# schedule_markers_ok $markers
+#
+# Succeed when every path:mtime pair in $markers still holds.
+#
+schedule_markers_ok()
+{
+ local pair mtime
+
+ for pair in $1; do
+ mtime=$( stat -f %m "${pair%:*}" 2> /dev/null ) || mtime=-
+ [ "$mtime" = "${pair##*:}" ] || return $FAILURE
+ done
+ return $SUCCESS
+}
+
+# resolve_target
+#
+# Fill in TARGET and TARGET_ARCH the way the top-level Makefile does when
+# only one, or neither, is given.
+#
+resolve_target()
+{
+ if [ ! "$TARGET" ] && [ ! "$TARGET_ARCH" ]; then
+ TARGET=$( uname -m )
+ TARGET_ARCH=$( uname -p )
+ elif [ ! "$TARGET_ARCH" ]; then
+ case "$TARGET" in
+ arm) TARGET_ARCH=armv7 ;;
+ arm64) TARGET_ARCH=aarch64 ;;
+ riscv) TARGET_ARCH=riscv64 ;;
+ *) TARGET_ARCH=$TARGET
+ esac
+ elif [ ! "$TARGET" ]; then
+ case "$TARGET_ARCH" in
+ aarch64) TARGET=arm64 ;;
+ arm*) TARGET=arm ;;
+ powerpc*) TARGET=powerpc ;;
+ riscv*) TARGET=riscv ;;
+ *) TARGET=$TARGET_ARCH
+ esac
+ fi
+}
+
+# schedule_load $mode $srcdir $outfile $var_to_set
+#
+# Copy a valid cached schedule to $outfile and set $var_to_set to its total
+# weight. Fails when there is no cache or it is stale.
+#
+schedule_load()
+{
+ local __mode="$1" __srcdir="$2" __outfile="$3" __var_to_set="$4"
+ local __cache __line
+ local __format= __cmode= __csrcroot= __ctarget= __ckernconf=
+ local __markers= __total=
+
+ [ "$REFRESH_CACHE" ] && return $FAILURE
+ schedule_cache_file "$__mode" "$__srcdir" __cache
+ [ -f "$__cache" ] || return $FAILURE
+
+ while IFS= read -r __line; do
+ case "$__line" in
+ "# format="*) __format=${__line#*=} ;;
+ "# mode="*) __cmode=${__line#*=} ;;
+ "# srcroot="*) __csrcroot=${__line#*=} ;;
+ "# target="*) __ctarget=${__line#*=} ;;
+ "# kernconf="*) __ckernconf=${__line#*=} ;;
+ "# markers="*) __markers=${__line#*=} ;;
+ "# total="*) __total=${__line#*=} ;;
+ "#"*) : ok ;;
+ *) break
+ esac
+ done < "$__cache"
+
+ [ "$__format" = "$SCHEDULE_FORMAT" ] || return $FAILURE
+ [ "$__cmode" = "$__mode" ] || return $FAILURE
+ [ "$__csrcroot" = "$__srcdir" ] || return $FAILURE
+ [ "$__ctarget" = "$TARGET/$TARGET_ARCH" ] || return $FAILURE
+ [ "$__ckernconf" = "$KERNCONF" ] || return $FAILURE
+ schedule_markers_ok "$__markers" || return $FAILURE
+ case "$__total" in
+ ""|*[!0-9]*|0) return $FAILURE ;;
+ esac
+
+ printf '%s: loading %s\n' "$pgm" "$__cache" >&2
+ awk '!/^#/ && NF' "$__cache" > "$__outfile"
+ eval $__var_to_set=\$__total
+}
+
+# self_path $var_to_set
+#
+# Set $var_to_set to a path by which xargs(1) workers can re-enter this
+# script.
+#
+self_path()
+{
+ local __path
+
+ case "$0" in
+ */*) __path=$0 ;;
+ *) __path=$( command -v "$0" ) || __path=./$0
+ esac
+ eval $1=\$__path
+}
+
+# schedule_units_world $srcdir $outfile
+#
+# Write the buildworld units to $outfile, one per line as pass, stage,
+# kind, path, and weight (tab-separated), in make order.
+#
+schedule_units_world()
+{
+ local __srcdir="$1" __outfile="$2"
+ local __tgts __prereq __startup __prebuild __generic __subdir
+ local __units __paths __weights __self __ncpu __n __stage __pass __unit
+
+ printf '%s: querying make schedule (TARGET=%s TARGET_ARCH=%s) ...\n' \
+ "$pgm" "$TARGET" "$TARGET_ARCH" >&2
+ make -C "$__srcdir" -f Makefile.inc1 \
+ TARGET="$TARGET" TARGET_ARCH="$TARGET_ARCH" \
+ -V WMAKE_TGTS -V _prereq_libs -V _startup_libs \
+ -V _prebuild_libs -V _generic_libs -V SUBDIR \
+ > "$WORKDIR/vars" || return $FAILURE
+ {
+ read -r __tgts
+ read -r __prereq
+ read -r __startup
+ read -r __prebuild
+ read -r __generic
+ read -r __subdir
+ } < "$WORKDIR/vars"
+ [ "$__tgts" ] || return $FAILURE
+
+ #
+ # One unit per line: pass, stage, kind, and path (tab-separated).
+ # Kind is STAGE for a WMAKE_TGTS step with no directory of its own,
+ # PL or L for a library ordered by Makefile.inc1 (prereq versus
+ # startup/prebuild/generic), and SUBDIR for an everything subtree.
+ #
+ __units=$WORKDIR/units
+ : > "$__units"
+ for __stage in $__tgts; do
+ case "$__stage" in
+ _libraries) __pass=native ;;
+ build?*) __pass=${__stage#build} ;;
+ everything)
+ __n=0
+ for __unit in $__subdir; do
+ case "$__unit" in
+ .WAIT) continue ;;
+ sys) continue ;; # kernel, not world
+ esac
+ printf 'native\teverything\tSUBDIR\t%s\n' \
+ "$__unit" >> "$__units"
+ __n=$(( $__n + 1 ))
+ #
+ # Clang/LLVM is a nested SUBDIR whose objects
+ # outnumber the rest of lib and usr.bin.
+ #
+ case "$__unit" in
+ lib|usr.bin) : ok ;;
+ *) continue
+ esac
+ [ -d "$__srcdir/$__unit/clang" ] || continue
+ printf 'native\teverything\tSUBDIR\t%s\n' \
+ "$__unit/clang" >> "$__units"
+ __n=$(( $__n + 1 ))
+ done
+ [ $__n -gt 0 ] ||
+ printf '%s: everything SUBDIR is empty\n' \
+ "$pgm" >&2
+ continue
+ ;;
+ *)
+ printf 'native\t%s\tSTAGE\t%s\n' \
+ "$__stage" "$__stage" >> "$__units"
+ continue
+ esac
+ for __unit in $__prereq; do
+ printf '%s\t%s\tPL\t%s\n' \
+ "$__pass" "$__stage" "$__unit" >> "$__units"
+ done
+ for __unit in $__startup $__prebuild $__generic; do
+ printf '%s\t%s\tL\t%s\n' \
+ "$__pass" "$__stage" "$__unit" >> "$__units"
+ done
+ done
+
+ #
+ # Weigh each unique path once (compat passes reuse native weights),
+ # in parallel, one weight file per path; STAGE rows need no find(1).
+ # Workers re-enter this script through its _weigh mode.
+ #
+ __paths=$WORKDIR/paths
+ __weights=$WORKDIR/weights
+ awk -F '\t' '$3 != "STAGE" && ! seen[$4]++ { print $4 }' "$__units" \
+ > "$__paths"
+ __n=$( awk 'END { print NR }' "$__paths" )
+ __ncpu=$( sysctl -n hw.ncpu 2> /dev/null ) || __ncpu=1
+ self_path __self
+ mkdir -p "$__weights" || return $FAILURE
+ printf '%s: weighing %s path(s) with %s worker(s) ...\n' \
+ "$pgm" "$__n" "$__ncpu" >&2
+ [ $__n -gt 0 ] &&
+ xargs -P "$__ncpu" -n 1 -J % \
+ sh "$__self" _weigh "$__srcdir" % "$__weights" \
+ < "$__paths" |
+ bsdpv -li "%'10lli paths weighed @ %'9.1f paths/sec." \
+ "$__n:$__srcdir schedule"
+
+ awk -F '\t' -v stage_weight="$STAGE_WEIGHT" -v weights="$__weights" '
+ {
+ if ($3 == "STAGE")
+ w = stage_weight
+ else {
+ file = $4
+ gsub("/", "%", file)
+ file = weights "/" file
+ w = 1
+ getline w < file
+ close(file)
+ }
+ printf "%s\t%s\t%s\t%s\t%d\n", $1, $2, $3, $4, w
+ }
+ ' "$__units" > "$__outfile"
+}
+
+# kernel_modules $srcdir $outdir
+#
+# Describe every directory that sys/modules reaches through SUBDIR, one
+# file per directory under $outdir holding its KMOD, SUBDIR, and objects
+# (one per line), enumerating each level of nesting in parallel. The
+# caller exports the kernel's MKMODULESENV so make(1) sees the same
+# module selection as the build. Workers re-enter this script through its
+# _kmod mode.
+#
+kernel_modules()
+{
+ local srcdir="$1" outdir="$2"
+ local top queue next self ncpu n dir kmod subdir sub probe
+
+ mkdir -p "$outdir" || return $FAILURE
+ top=$outdir/.top
+ queue=$outdir/.queue
+ next=$outdir/.next
+ self_path self
+ ncpu=$( sysctl -n hw.ncpu 2> /dev/null ) || ncpu=1
+
+ #
+ # Probe the compiler and linker once for all workers; bsd.compiler.mk
+ # and bsd.linker.mk skip cc(1) and ld(1) when these are already set.
+ #
+ probe='COMPILER_TYPE=${COMPILER_TYPE}'
+ probe="$probe"' COMPILER_VERSION=${COMPILER_VERSION}'
+ probe="$probe"' COMPILER_FREEBSD_VERSION=${COMPILER_FREEBSD_VERSION}'
+ probe="$probe"' COMPILER_RESOURCE_DIR=${COMPILER_RESOURCE_DIR}'
+ probe="$probe"' LINKER_TYPE=${LINKER_TYPE}'
+ probe="$probe"' LINKER_VERSION=${LINKER_VERSION}'
+ probe="$probe"' LINKER_FREEBSD_VERSION=${LINKER_FREEBSD_VERSION}'
+ probe="$probe"' LINKER_FEATURES="${LINKER_FEATURES}"'
+ make -C "$srcdir/sys/modules" -V SUBDIR -V "$probe" \
+ > "$outdir/.vars" || return $FAILURE
+ {
+ read -r subdir
+ read -r probe
+ } < "$outdir/.vars"
+ eval export $probe
+
+ : > "$top"
+ for sub in $subdir; do
+ [ "$sub" = .WAIT ] && continue
+ printf '%s\n' "$sub" >> "$top"
+ done
+ cp "$top" "$queue" || return $FAILURE
+ while [ -s "$queue" ]; do
+ n=$( awk 'END { print NR }' "$queue" )
+ printf '%s: weighing %s module dir(s) with %s worker(s) ...\n' \
+ "$pgm" "$n" "$ncpu" >&2
+ xargs -P "$ncpu" -n 1 -J % \
+ sh "$self" _kmod "$srcdir" % "$outdir" < "$queue" |
+ bsdpv -li "%'10lli dirs weighed @ %'9.1f dirs/sec." \
+ "$n:$srcdir/sys/modules"
+
+ #
+ # Next level: the SUBDIR lists of the directories just done
+ #
+ : > "$next"
+ while read -r dir; do
+ kmod= subdir=
+ {
+ read -r kmod
+ read -r subdir
+ } < "$outdir/$( printf '%s' "$dir" | tr / % )"
+ for sub in $subdir; do
+ [ "$sub" = .WAIT ] && continue
+ printf '%s/%s\n' "$dir" "$sub" >> "$next"
+ done
+ done < "$queue"
+ mv -f "$next" "$queue"
+ done
+}
+
+# kernel_module_units $moddir $kernel $kobjs $outfile
+#
+# Append one weighted MODULE unit per module described under $moddir (see
+# kernel_modules) to $outfile, walking sys/modules in make order. A module
+# weighs its .ko plus each object not also named in $kobjs, the kernel's
+# own objects, which the seeker cannot tell apart from a module's.
+#
+kernel_module_units()
+{
+ local moddir="$1" kernel="$2" kobjs="$3" outfile="$4"
+
+ awk -v moddir="$moddir" -v kernel="$kernel" -v kobjs="$kobjs" \
+ '######################################## BEGIN
+
+ BEGIN {
+ n = split(kobjs, a, " ")
+ for (i = 1; i <= n; i++)
+ kobj[a[i]] = 1
+ }
+
+ ######################################## FUNCTIONS
+
+ function walk(dir, file, kmod, subdir, objs, obj, sd,
+ n, i, w)
+ {
+ file = dir
+ gsub("/", "%", file)
+ file = moddir "/" file
+ kmod = subdir = objs = ""
+ getline kmod < file
+ getline subdir < file
+ getline objs < file
+ close(file)
+
+ if (kmod != "") {
+ w = 1 # the .ko
+ delete seen
+ n = split(objs, obj, " ")
+ for (i = 1; i <= n; i++)
+ if (!(obj[i] in kobj) && !seen[obj[i]]++)
+ w++
+ printf "%s\teverything\tMODULE\t%s\t%d\n",
+ kernel, dir, w
+ }
+ n = split(subdir, sd, " ")
+ for (i = 1; i <= n; i++)
+ if (sd[i] != ".WAIT")
+ walk(dir "/" sd[i])
+ }
+
+ ######################################## MAIN
+
+ { walk($1) }
+ ' "$moddir/.top" >> "$outfile" # END-QUOTE
+}
+
+# schedule_units_kernel $srcdir $outfile
+#
+# Write the buildkernel units to $outfile, one per line as pass, stage,
+# kind, path, and weight (tab-separated), in make order: for each kernel
+# in BUILDKERNELS, the numbered stages, the kernel proper (KERNEL, weighed
+# by its objects), and one MODULE per module. Weightless KOBJ lines name
+# the kernel's objects for the seeker.
+#
+schedule_units_kernel()
+{
+ local __srcdir="$1" __outfile="$2"
+ local __kernels __confdir __kconfdir __configargs __kernel __kdir
+ local __objs __menv __moddir __file __stage __obj __n
+
+ printf '%s: querying make schedule (TARGET=%s TARGET_ARCH=%s) ...\n' \
+ "$pgm" "$TARGET" "$TARGET_ARCH" >&2
+ make -C "$__srcdir" -f Makefile.inc1 \
+ TARGET="$TARGET" TARGET_ARCH="$TARGET_ARCH" \
+ -V BUILDKERNELS -V KRNLCONFDIR -V KERNCONFDIR -V CONFIGARGS \
+ > "$WORKDIR/vars" || return $FAILURE
+ {
+ read -r __kernels
+ read -r __confdir
+ read -r __kconfdir
+ read -r __configargs
+ } < "$WORKDIR/vars"
+ [ "$__kernels" ] || return $FAILURE
+
+ for __file in sys/conf/files sys/conf/kmod.mk sys/conf/kern.opts.mk \
+ sys/modules/Makefile
+ do
+ MARKER_FILES="$MARKER_FILES $__srcdir/$__file"
+ done
+ : > "$__outfile"
+ for __kernel in $__kernels; do
+ MARKER_FILES="$MARKER_FILES $__kconfdir/$__kernel"
+ for __stage in _config _cleanobj _obj _build-tools; do
+ printf '%s\t%s\tSTAGE\t%s\t%s\n' "$__kernel" \
+ "$__stage" "$__stage" "$STAGE_WEIGHT" \
+ >> "$__outfile"
+ done
+
+ #
+ # The kernel proper, weighed by the objects config(8) and
+ # the kernel makefiles agree on
+ #
+ printf '%s: configuring %s ...\n' "$pgm" "$__kernel" >&2
+ __kdir=$WORKDIR/kernel/$__kernel
+ mkdir -p "$__kdir" || return $FAILURE
+ ( cd "$__confdir" && config $__configargs -d "$__kdir" \
+ -I "$__kconfdir" -I "$__confdir" \
+ "$__kconfdir/$__kernel" ) > /dev/null || return $FAILURE
+ make -C "$__kdir" -V SYSTEM_OBJS -V MKMODULESENV \
+ > "$WORKDIR/vars" || return $FAILURE
+ {
+ read -r __objs
+ read -r __menv
+ } < "$WORKDIR/vars"
+ __n=0
+ for __obj in $__objs; do
+ printf '%s\teverything\tKOBJ\t%s\t0\n' \
+ "$__kernel" "$__obj" >> "$__outfile"
+ __n=$(( $__n + 1 ))
+ done
+ [ $__n -gt 0 ] || return $FAILURE
+ printf '%s\teverything\tKERNEL\t%s\t%s\n' \
+ "$__kernel" "$__kernel" "$__n" >> "$__outfile"
+
+ #
+ # Modules, selected and weighed under the kernel's own
+ # MKMODULESENV (MODULES_OVERRIDE, WITHOUT_MODULES, ...)
+ #
+ __moddir=$__kdir.modules
+ (
+ eval export $__menv
+ export WITHOUT_AUTO_OBJ=yes # query only; no objdirs
+ kernel_modules "$__srcdir" "$__moddir" &&
+ kernel_module_units "$__moddir" "$__kernel" \
+ "$__objs" "$__outfile"
+ ) || return $FAILURE
+ done
+}
+
+# schedule_build $mode $srcdir $outfile $var_to_set
+#
+# Write the make-order schedule to $outfile, one unit per line as pass,
+# stage, kind, path, weight, and cumulative weight (tab-separated), set
+# $var_to_set to the total weight, and save the result to the cache.
+#
+schedule_build()
+{
+ local __mode="$1" __srcdir="$2" __outfile="$3" __var_to_set="$4"
+ local __units __total __cache __tmp
+
+ __units=$WORKDIR/weighted
+ MARKER_FILES=
+ case "$__mode" in
+ buildworld) schedule_units_world "$__srcdir" "$__units" ;;
+ buildkernel) schedule_units_kernel "$__srcdir" "$__units" ;;
+ *) return $FAILURE
+ esac || return $FAILURE
+
+ awk -F '\t' '
+ {
+ w = int($5)
+ if (w < 1 && $3 != "KOBJ")
+ w = 1
+ cum += w
+ printf "%s\t%s\t%s\t%s\t%d\t%d\n",
+ $1, $2, $3, $4, w, cum
+ }
+ ' "$__units" > "$__outfile"
+ __total=$( awk -F '\t' 'END { print $6 + 0 }' "$__outfile" )
+ [ "$__total" -gt 0 ] 2> /dev/null || return $FAILURE
+ eval $__var_to_set=\$__total
+
+ schedule_cache_file "$__mode" "$__srcdir" __cache
+ mkdir -p "$CACHE_DIR" || return $FAILURE
+ __tmp=$__cache.$$
+ {
+ printf '%s\n' \
+ "# format=$SCHEDULE_FORMAT" \
+ "# mode=$__mode" \
+ "# srcroot=$__srcdir" \
+ "# target=$TARGET/$TARGET_ARCH" \
+ "# kernconf=$KERNCONF" \
+ "# markers=$( schedule_markers "$__srcdir" $MARKER_FILES )" \
+ "# total=$__total"
+ cat "$__outfile"
+ } > "$__tmp" && mv -f "$__tmp" "$__cache"
+ printf '%s: saved %s (%s ticks)\n' "$pgm" "$__cache" "$__total" >&2
+}
+
+# highwater_load $file $total $var_to_set
+#
+# Set $var_to_set to the tick reached by a previous run, saved in $file,
+# clamped to $total; or to 0 when there is none or it is older than
+# HIGHWATER_MAXAGE.
+#
+highwater_load()
+{
+ local __file="$1" __total="$2" __var_to_set="$3"
+ local __tick __time __age
+
+ eval $__var_to_set=0
+ [ -f "$__file" ] || return $FAILURE
+ read -r __tick __time < "$__file" || return $FAILURE
+ case "$__tick" in
+ ""|*[!0-9]*|0) return $FAILURE ;;
+ esac
+ case "$__time" in
+ ""|*[!0-9]*) __time=$( stat -f %m "$__file" ) ;;
+ esac
+ __age=$(( $( date +%s ) - $__time ))
+ if [ $__age -lt 0 ] || [ $__age -gt "$HIGHWATER_MAXAGE" ]; then
+ printf '%s: ignoring high-water mark (%s s old; max %s s)\n' \
+ "$pgm" "$__age" "$HIGHWATER_MAXAGE" >&2
+ return $FAILURE
+ fi
+ [ $__tick -gt "$__total" ] && __tick=$__total
+ printf '%s: high-water mark %s/%s (applied once banners confirm)\n' \
+ "$pgm" "$__tick" "$__total" >&2
+ eval $__var_to_set=\$__tick
+}
+
+# ensure_snp_privs
+#
+# watch(8) opens snp(4), which is root-only by default. Validate sudo(8)
+# before the pipeline starts so its password prompt is not hidden behind
+# the progress view.
+#
+ensure_snp_privs()
+{
+ [ "$( id -u )" -eq 0 ] || [ -w /dev/snp ] && return $SUCCESS
+ SUDO=sudo
+ sudo -v
+}
+
+# input_source
+#
+# Copy make(1) output to standard output, from the -f file or by snooping
+# the -w tty. A regular file is followed as it grows; a FIFO, or `-' for
+# standard input, is read to end of file.
+#
+input_source()
+{
+ case "$INPUT" in
+ "") exec $SUDO watch -c "$WATCH_TTY" ;;
+ -) exec cat ;;
+ *)
+ [ -f "$INPUT" ] && exec tail -n +1 -f "$INPUT"
+ exec cat "$INPUT"
+ esac
+}
+
+# banner_filter
+#
+# Reduce raw terminal output from make(1) to one token per line:
+# STAGE <text> >>> banners and --- _target --- from Makefile.inc1
+# SUBDIR <path> ===> path and --- all_subdir_path ---
+# LIB <path> --- path__L --- and --- path__PL ---
+# OBJ <name> --- name.o ---, archive/PIC/PIE variants, .ko, .fwo
+#
+banner_filter()
+{
+ stdbuf -oL tr '\r' '\n' | awk '
+ {
+ line = $0
+ gsub(/\033\[[0-9;]*[A-Za-z]/, "", line) # CSI sequences
+ }
+ line ~ />>> / {
+ sub(/^.*>>> /, ">>> ", line)
+ print "STAGE\t" line
+ fflush()
+ next
+ }
+ line ~ /===> / {
+ if (match(line, /===> [^[(]+/)) {
+ s = substr(line, RSTART + 4, RLENGTH - 4)
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", s)
+ if (s != "") {
+ print "SUBDIR\t" s
+ fflush()
+ }
+ }
+ next
+ }
+ match(line, /--- [^[:space:]]+__(L|PL) ---/) {
+ s = substr(line, RSTART, RLENGTH)
+ sub(/^--- /, "", s)
+ sub(/__(L|PL) ---$/, "", s)
+ print "LIB\t" s
+ fflush()
+ next
+ }
+ match(line, /--- all_subdir_[^[:space:]]+ ---/) {
+ s = substr(line, RSTART, RLENGTH)
+ sub(/^--- all_subdir_/, "", s)
+ sub(/ ---$/, "", s)
+ print "SUBDIR\t" s
+ fflush()
+ next
+ }
+ match(line,
+ /--- [^[:space:]]+\.(pieo|(nossp)?pico|o|a|ko|fwo) ---/) {
+ s = substr(line, RSTART, RLENGTH)
+ sub(/^--- /, "", s)
+ sub(/ ---$/, "", s)
+ print "OBJ\t" s
+ fflush()
+ next
+ }
+ match(line, /--- _[A-Za-z0-9._-]+ ---/) {
+ s = substr(line, RSTART, RLENGTH)
+ sub(/^--- /, "", s)
+ sub(/ ---$/, "", s)
+ print "STAGE\t" s
+ fflush()
+ next
+ }
+ ' # END-QUOTE
+}
+
+# schedule_seek $mode $schedule $floor $hwfile
+#
+# Turn banner_filter tokens into one line per tick for bsdpv(1), walking
+# the weighted schedule in make order and recording the high-water tick
+# in $hwfile as it goes.
+#
+# The gauge cannot rewind, so a high-water $floor from a previous run is
+# applied only once live banners show the build is at or past it (a
+# reattach), and dropped when they show an earlier stage or a new build.
+#
+# A kernel builds its modules in parallel with the kernel proper, so in
+# buildkernel mode module banners only widen the parallel window and the
+# objects themselves carry the gauge.
+#
+schedule_seek()
+{
+ local mode="$1" sched="$2" floor="$3" hwfile="$4"
+
+ case "$floor" in
+ ""|*[!0-9]*) floor=0 ;;
+ esac
+
+ awk -v floor="$floor" \
+ -v hwfile="$hwfile" \
+ -v mode="$mode" \
+ -v pgm="$pgm" \
+ -v sched="$sched" \
+ '######################################## BEGIN
+
+ BEGIN {
+ #
+ # Load the schedule: pass, stage, kind, path, weight,
+ # and cumulative weight per unit, in make order. KOBJ
+ # lines are not units; they name the kernel objects.
+ #
+ while ((getline line < sched) > 0) {
+ n = split(line, a, "\t")
+ if (n < 6)
+ continue
+ if (a[3] == "KOBJ") {
+ kobj[a[4]] = 1
+ continue
+ }
+ nunit++
+ passarr[nunit] = a[1]
+ stagearr[nunit] = a[2]
+ kindarr[nunit] = a[3]
+ patharr[nunit] = a[4]
+ w[nunit] = a[5] + 0
+ cum[nunit] = a[6] + 0
+ if (!(a[2] in stage_first))
+ stage_first[a[2]] = nunit
+ if (!((a[1], a[2]) in pass_stage_first))
+ pass_stage_first[a[1], a[2]] = nunit
+ if (!(a[1] in pass_first))
+ pass_first[a[1]] = nunit
+ pass_last[a[1]] = nunit
+ if (a[3] == "MODULE")
+ module_index(a[1], a[4], nunit)
+ }
+ close(sched)
+
+ total = cum[nunit] + 0
+ cur = 0 # ticks emitted so far
+ unit = 1 # current schedule unit
+ prefer_pass = "native" # native, compat, or kernel pass
+ parallel_end = 0 # furthest cumulative weight seen
+ objctx = 0 # module unit owning recent object banners
+
+ #
+ # The high-water floor waits for confirmation from
+ # live banners; floor_unit is the unit containing it.
+ #
+ floor_applied = (floor > 0) ? 0 : 1
+ floor_unit = 1
+ if (floor > 0) {
+ for (i = 1; i <= nunit; i++) {
+ if (cum[i] > floor) {
+ floor_unit = i
+ break
+ }
+ floor_unit = i + 1
+ }
+ }
+ }
+
+ ######################################## FUNCTIONS
+
+ function write_hw(pos)
+ {
+ print pos, systime() > hwfile
+ close(hwfile)
+ }
+
+ function save_hw(pos, line, old, a)
+ {
+ old = 0
+ if ((getline line < hwfile) > 0) {
+ split(line, a)
+ old = a[1] + 0
+ }
+ close(hwfile)
+ if (pos > old)
+ write_hw(pos)
+ }
+
+ function emit_seek(count, j, n)
+ {
+ #
+ # One line per tick; flush in batches so the gauge
+ # moves while a large seek is in flight.
+ #
+ while (count > 0) {
+ n = (count < 512) ? count : 512
+ for (j = 0; j < n; j++)
+ print "--- seek ---"
+ fflush()
+ cur += n
+ save_hw(cur)
+ count -= n
+ }
+ }
+
+ function advance_to(pos)
+ {
+ pos = int(pos)
+ if (pos > total)
+ pos = total
+ if (pos > cur)
+ emit_seek(pos - cur)
+ }
+
+ function unit_start(u) { return cum[u] - w[u] }
+
+ function enter_unit(u, start)
+ {
+ if (u < unit || u > nunit)
+ return
+ start = unit_start(u)
+ if (start > cur)
+ advance_to(start)
+ unit = u
+ delete seenobj
+ }
+
+ function find_unit_by_path(path, pass, from,
+ i, best, st)
+ {
+ #
+ # Exact path within the current WMAKE stage, so
+ # ===> tools/build during legacy cannot jump ahead
+ # to everything.
+ #
+ if (from < 1)
+ from = unit
+ if (from < 1 || from > nunit)
+ return 0
+
+ st = stagearr[from]
+ best = 0
+ for (i = from; i <= nunit && stagearr[i] == st; i++) {
+ if (patharr[i] != path)
+ continue
+ if (pass != "" && passarr[i] == pass)
+ return i
+ if (best == 0)
+ best = i
+ }
+ return best
+ }
+
+ function find_unit_prefix(path, pass, from,
+ i, best, plen, p, st)
+ {
+ #
+ # Longest unit path that is a prefix of path, within
+ # the current WMAKE stage.
+ #
+ if (from < 1)
+ from = unit
+ if (from < 1 || from > nunit)
+ return 0
+
+ st = stagearr[from]
+ best = 0
+ plen = 0
+ for (i = from; i <= nunit && stagearr[i] == st; i++) {
+ p = patharr[i]
+ if (p != path && index(path, p "/") != 1)
+ continue
+ if (pass != "" && passarr[i] != pass)
+ continue
+ if (length(p) >= plen) {
+ plen = length(p)
+ best = i
+ }
+ }
+ return best
+ }
+
+ function find_unit_by_path_any(path, pass, from,
+ i, best)
+ {
+ #
+ # Exact path in this or any later stage, for a
+ # reattach during everything after a compat floor.
+ #
+ if (from < 1)
+ from = unit
+
+ best = 0
+ for (i = from; i <= nunit; i++) {
+ if (patharr[i] != path)
+ continue
+ if (pass != "" && passarr[i] == pass)
+ return i
+ if (best == 0)
+ best = i
+ }
+ return best
+ }
+
+ function path_unit(path, from, u)
+ {
+ u = find_unit_by_path(path, prefer_pass, from)
+ if (!u)
+ u = find_unit_prefix(path, prefer_pass, from)
+ return u
+ }
+
+ function path_unit_everything(path, i, u)
+ {
+ if (!("everything" in stage_first))
+ return 0
+ i = stage_first["everything"]
+ u = find_unit_by_path(path, "native", i)
+ if (!u)
+ u = find_unit_prefix(path, "native", i)
+ return u
+ }
+
+ function everything_anchor(u, p, v)
+ {
+ #
+ # usr.bin, usr.sbin, stand, tests, and etc build in
+ # parallel; anchor a mid-join at usr.bin so its
+ # clang unit is not skipped.
+ #
+ p = patharr[u]
+ if (p == "usr.sbin" || p == "stand" ||
+ p == "tests" || p == "etc") {
+ v = find_unit_by_path_any("usr.bin", "", 1)
+ if (v && v <= u)
+ return v
+ }
+ return u
+ }
+
+ function parallel_peer(u, v, pu, pv)
+ {
+ pu = patharr[u]
+ pv = patharr[v]
+ if (pu == "usr.bin/clang")
+ pu = "usr.bin"
+ if (pv == "usr.bin/clang")
+ pv = "usr.bin"
+ if (pu == "lib/clang")
+ pu = "lib"
+ if (pv == "lib/clang")
+ pv = "lib"
+
+ if (pu == pv)
+ return 1
+ if ((pu == "usr.bin" || pu == "usr.sbin" ||
+ pu == "stand" || pu == "tests" || pu == "etc") &&
+ (pv == "usr.bin" || pv == "usr.sbin" ||
+ pv == "stand" || pv == "tests" || pv == "etc"))
+ return 1
+ return 0
+ }
+
+ function consider_subdir(u, p)
+ {
+ #
+ # A parallel all_subdir banner must not complete the
+ # current unit, but a stale floor must still catch up
+ # when a later everything unit is already active.
+ #
+ if (u < 1 || u > nunit)
+ return
+ if (cum[u] > parallel_end)
+ parallel_end = cum[u]
+ if (u < unit)
+ return
+
+ if (cur == 0 || kindarr[unit] == "STAGE") {
+ enter_unit(everything_anchor(u))
+ return
+ }
+ if (u == unit)
+ return
+
+ p = patharr[unit]
+ if (p == "lib/clang" || p == "usr.bin/clang")
+ return
+ if (parallel_peer(unit, u))
+ return
+ enter_unit(everything_anchor(u))
+ }
+
+ function llvm_obj(obj, n, a)
+ {
+ #
+ # LLVM/Clang object banners are relative to the
+ # object directory (clang/, lld/, or a PascalCase
+ # component), not to contrib/llvm-project.
+ #
+ n = split(obj, a, "/")
+ if (n < 2)
+ return 0
+ if (a[1] ~ /^(clang|lld|lldb|llvm)$/)
+ return 1
+ if (a[1] ~ /^[A-Z]/)
+ return 1
+ return 0
+ }
+
+ function enter_clang_unit( u, p)
+ {
+ p = patharr[unit]
+ if (p == "lib/clang" || p == "usr.bin/clang")
+ return
+
+ #
+ # Parallel lib/* PIC objects belong to the lib/clang
+ # pass, not usr.bin/clang.
+ #
+ if (p == "lib" || index(p, "lib/") == 1) {
+ u = find_unit_by_path_any("lib/clang", "", 1)
+ if (u)
+ enter_unit(u)
+ return
+ }
+
+ u = find_unit_by_path_any("lib/clang", "", unit)
+ if (!u)
+ u = find_unit_by_path_any("usr.bin/clang",
+ "", unit)
+ if (!u)
+ u = find_unit_by_path_any("lib/clang", "", 1)
+ if (!u)
+ u = find_unit_by_path_any("usr.bin/clang",
+ "", 1)
+ if (u)
+ enter_unit(u)
+ }
+
+ function stage_unit(st)
+ {
+ #
+ # First unit of stage st, in the preferred pass when
+ # it has one (each kernel repeats every stage).
+ #
+ if ((prefer_pass, st) in pass_stage_first)
+ return pass_stage_first[prefer_pass, st]
+ if (st in stage_first)
+ return stage_first[st]
+ return 0
+ }
+
+ function stage_from_text(text, p, st, u)
+ {
+ u = stage_unit(text)
+ if (u)
+ return u
+
+ if (text ~ /Deleting stale files/ ||
+ text ~ /Cleaning up the temporary/)
+ return stage_unit("_cleanworldtmp")
+ if (text ~ /Rebuilding the temporary build tree/)
+ return stage_unit("_worldtmp")
+ if (text ~ /legacy release compatibility/)
+ return stage_unit("_legacy")
+ if (text ~ /bootstrap tools/)
+ return stage_unit("_bootstrap-tools")
+ if (text ~ /configuring the kernel/)
+ return stage_unit("_config")
+ if (text ~ /cleaning up the object tree/ ||
+ text ~ /^_cleankernobj/)
+ return stage_unit("_cleanobj")
+ if (text ~ /rebuilding the object tree/)
+ return stage_unit("_obj")
+ if (text ~ /build tools/)
+ return stage_unit("_build-tools")
+ if (text ~ /cross tools/)
+ return stage_unit("_cross-tools")
+ if (text ~ /recording build metadata/)
+ return stage_unit("_build-metadata")
+ if (text ~ /building includes/)
+ return stage_unit("_includes")
+ if (text ~ /building libraries/)
+ return stage_unit("_libraries")
+
+ if (match(text, /building lib([a-z0-9]+) /)) {
+ p = substr(text, RSTART, RLENGTH)
+ sub(/^building lib/, "", p)
+ sub(/ $/, "", p)
+ st = "build" p
+ u = stage_unit(st)
+ if (u)
+ return u
+ }
+
+ if (text ~ /building everything/)
+ return stage_unit("everything")
+ return 0
+ }
+
+ function pass_from_text(text, p)
+ {
+ #
+ # Kernel name from ">>> Kernel build for NAME ..."
+ #
+ p = text
+ sub(/^.*Kernel build for /, "", p)
+ sub(/ .*$/, "", p)
+ return p
+ }
+
+ function module_index(pass, path, u, tail)
+ {
+ #
+ # MODULE unit by pass and path, and by the last path
+ # component alone for all_subdir_ banners of nested
+ # module directories; first unit wins, and pass ""
+ # spans all passes.
+ #
+ if (!((pass, path) in modidx))
+ modidx[pass, path] = u
+ if (!(("", path) in modidx))
+ modidx["", path] = u
+ tail = path
+ sub(/^.*\//, "", tail)
+ if (tail == path)
+ return
+ if (!((pass, tail) in modtail))
+ modtail[pass, tail] = u
+ if (!(("", tail) in modtail))
+ modtail["", tail] = u
+ }
+
+ function module_unit(path, p)
+ {
+ p = (prefer_pass in pass_first) ? prefer_pass : ""
+ if ((p, path) in modidx)
+ return modidx[p, path]
+ if ((p, path) in modtail)
+ return modtail[p, path]
+ return 0
+ }
+
+ function kernel_unit(u)
+ {
+ #
+ # KERNEL unit of the pass containing unit u
+ #
+ if ((passarr[u], "everything") in pass_stage_first)
+ return pass_stage_first[passarr[u], "everything"]
+ return u
+ }
+
+ function apply_floor()
+ {
+ if (floor_applied || floor <= 0)
+ return
+ if (floor > cur)
+ advance_to(floor)
+ unit = floor_unit
+ if (unit < 1)
+ unit = 1
+ prefer_pass = passarr[unit]
+ floor_applied = 1
+ delete seenobj
+ }
+
+ function drop_floor()
+ {
+ if (floor > 0 && cur == 0) {
+ printf "%s: new %s; %s %s\n", pgm, mode,
+ "ignoring high-water mark",
+ floor > "/dev/stderr"
+ write_hw(0)
+ }
+ floor = 0
+ floor_applied = 1
+ unit = 1
+ prefer_pass = "native"
+ delete seenobj
+ }
+
+ function on_stage(u)
+ {
+ if (u < 1 || u > nunit)
+ return
+
+ if (!floor_applied && floor > 0) {
+ #
+ # An earlier unit in the same WMAKE stage is a
+ # mid-join, not a new build.
+ #
+ if (unit_start(u) < floor &&
+ stagearr[u] != stagearr[floor_unit])
+ drop_floor()
+ else
+ apply_floor()
+ }
+ enter_unit(u)
+ }
+
+ ######################################## MAIN
+
+ $1 == "STAGE" {
+ text = $0
+ sub(/^STAGE\t/, "", text)
+
+ if (text ~ /World build started/) {
+ drop_floor()
+ if ("_cleanworldtmp" in stage_first) {
+ u = stage_first["_cleanworldtmp"]
+ enter_unit(u)
+ } else
+ enter_unit(1)
+ next
+ }
+ if (text ~ /World build completed/) {
+ advance_to(total)
+ next
+ }
+ if (mode == "buildkernel" &&
+ text ~ /Kernel build for .* started/) {
+ p = pass_from_text(text)
+ if (!(p in pass_first))
+ next
+ u = pass_first[p]
+ if (u == 1)
+ drop_floor()
+ prefer_pass = p
+ on_stage(u)
+ next
+ }
+ if (mode == "buildkernel" &&
+ text ~ /Kernel build for .* completed/) {
+ p = pass_from_text(text)
+ if (p in pass_last)
+ advance_to(cum[pass_last[p]])
+ next
+ }
+ if (mode == "buildkernel" &&
+ text ~ /Kernel\(s\) .* built in/) {
+ advance_to(total)
+ next
+ }
+
+ u = stage_from_text(text)
+ if (!u)
+ next
+
+ if (text ~ /building libraries/ ||
+ text ~ /building everything/)
+ prefer_pass = "native"
+ if (match(text, /building lib([a-z0-9]+) /)) {
+ p = substr(text, RSTART, RLENGTH)
+ sub(/^building lib/, "", p)
+ sub(/ $/, "", p)
+ prefer_pass = p
+ }
+ on_stage(u)
+ next
+ }
+
+ $1 == "SUBDIR" && mode == "buildkernel" {
+ path = $0
+ sub(/^SUBDIR\t/, "", path)
+
+ #
+ # ===> KERNCONF opens a pass; a module banner widens
+ # the parallel window and owns the objects that follow
+ # until the next one.
+ #
+ if (path in pass_first) {
+ prefer_pass = path
+ on_stage(pass_first[path])
+ next
+ }
+ u = module_unit(path)
+ if (!u)
+ next
+ if (!floor_applied && floor > 0)
+ apply_floor()
+ objctx = u
+ if (cum[u] > parallel_end)
+ parallel_end = cum[u]
+ if (cur == 0 || kindarr[unit] == "STAGE")
+ enter_unit(kernel_unit(u))
+ next
+ }
+
+ $1 == "SUBDIR" {
+ path = $0
+ sub(/^SUBDIR\t/, "", path)
+
+ u = path_unit_everything(path)
+ if (!u)
+ u = path_unit(path, unit)
+ if (!u)
+ u = find_unit_by_path_any(path, "", unit)
+
+ #
+ # A banner already past the floor makes the floor moot;
+ # one behind it is a parallel sibling, not a new build.
+ #
+ if (!floor_applied && floor > 0) {
+ if (u && unit_start(u) > floor)
+ floor_applied = 1
+ else
+ apply_floor()
+ }
+
+ if (!u && path ~ /(^|\/)clang(\/|$)/) {
+ enter_clang_unit()
+ next
+ }
+ if (u)
+ consider_subdir(u)
+ next
+ }
+
+ $1 == "LIB" {
+ path = $0
+ sub(/^LIB\t/, "", path)
+
+ u = path_unit_everything(path)
+ if (!u)
+ u = path_unit(path, unit)
+
+ if (!floor_applied && floor > 0) {
+ if (u && unit_start(u) > floor)
+ floor_applied = 1
+ else
+ apply_floor()
+ }
+
+ if (u)
+ consider_subdir(u)
+ next
+ }
+
+ $1 == "OBJ" {
+ obj = $0
+ sub(/^OBJ\t/, "", obj)
+
+ if (!floor_applied && floor > 0)
+ apply_floor()
+ if (llvm_obj(obj))
+ enter_clang_unit()
+ if (unit < 1 || unit > nunit)
+ next
+
+ #
+ # Path and suffix together: Foo.o and Foo.pico are
+ # distinct objects, as is Foo.pico in another directory.
+ # Kernel object names are bare, so a module object is
+ # told apart by the module banner it followed, and a
+ # kernel object by the KOBJ list (a module object of
+ # the same name was left out of the module weight).
+ #
+ key = obj
+ if (mode == "buildkernel") {
+ if (obj in kobj)
+ key = "kernel" SUBSEP obj
+ else if (obj !~ /\.ko$/)
+ key = objctx SUBSEP obj
+ }
+ if (key in seenobj)
+ next
+ seenobj[key] = 1
+
+ #
+ # One tick per new object, stopping one short of the
+ # unit end (or of the furthest parallel unit) so the
+ # last tick marks completion.
+ #
+ limit = cum[unit] - 1
+ if (parallel_end - 1 > limit)
+ limit = parallel_end - 1
+ if (limit < unit_start(unit))
+ limit = cum[unit]
+ if (cur < limit)
+ advance_to(cur + 1)
+ next
+ }
+ ' # END-QUOTE
+}
+
+############################################################ MAIN
+
+VERSION="${VERSION#*: }"
+VERSION="${VERSION% $}"
+
+#
+# Process command-line arguments
+#
+case "$1" in
+-v)
+ echo "$VERSION"
+ exit $SUCCESS
+ ;;
+_weigh)
+ #
+ # Worker for schedule_build: $0 _weigh $srcdir $unit $weightsdir
+ # writes the weight of $unit to $weightsdir and echoes $unit so
+ # bsdpv(1) can count progress.
+ #
+ shift # _weigh
+ unit_weight "$1" "$2" > "$3/$( printf '%s' "$2" | tr '/' '%' )"
+ echo "$2"
+ exit
+ ;;
+_kmod)
+ #
+ # Worker for kernel_modules: $0 _kmod $srcdir $dir $outdir writes
+ # the KMOD, SUBDIR, and objects of sys/modules/$dir to $outdir (one
+ # per line) and echoes $dir so bsdpv(1) can count progress. Objects
+ # are OBJS plus the genoffset.o and genassym.o that offset.inc and
+ # assym.inc build (see sys/conf/kmod.mk).
+ #
+ shift # _kmod
+ objs='${OBJS}'
+ objs="$objs"' ${DPSRCS:Moffset.inc:S/offset.inc/genoffset.o/}'
+ objs="$objs"' ${SRCS:Massym.inc:S/assym.inc/genassym.o/}'
+ objs="$objs"' ${DPSRCS:Massym.inc:S/assym.inc/genassym.o/}'
+ make -C "$1/sys/modules/$2" -V KMOD -V SUBDIR -V "$objs" \
+ 2> /dev/null > "$3/$( printf '%s' "$2" | tr '/' '%' )"
+ echo "$2"
+ exit
+ ;;
+buildworld|buildkernel) MODE=$1 ;;
+-h|help|"") usage
+ # NOTREACHED
+ ;;
+*) usage "unknown mode \`$1'"
+ # NOTREACHED
+esac
+shift # MODE
+
+#
+# Process command-line options
+#
+while getopts f:hRs:vw: flag; do
+ case "$flag" in
+ f) INPUT=$OPTARG ;;
+ R) REFRESH_CACHE=1 ;;
+ s) SRCROOT=$OPTARG ;;
+ v) echo "$VERSION"
+ exit $SUCCESS ;;
+ w) WATCH_TTY=$OPTARG ;;
+ *) usage
+ # NOTREACHED
+ esac
+done
+shift $(( $OPTIND - 1 ))
+
+[ "$INPUT" ] || [ "$WATCH_TTY" ] ||
+ usage "missing -f file or -w tty" # NOTREACHED
+[ "$INPUT" ] && [ "$WATCH_TTY" ] &&
+ usage "-f file and -w tty are exclusive" # NOTREACHED
+[ $# -eq 0 ] || usage "extra arguments: $*" # NOTREACHED
+
+case "$SRCROOT" in
+/*) : ok ;;
+*) SRCROOT=$PWD/$SRCROOT
+esac
+[ -d "$SRCROOT" ] || die "source tree does not exist: $SRCROOT"
+[ ! "$INPUT" ] || [ "$INPUT" = - ] || [ -r "$INPUT" ] ||
+ die "cannot read $INPUT"
+
+resolve_target
+
+WORKDIR=$( mktemp -d -t "$pgm" ) || exit
+trap 'rm -rf "$WORKDIR"' EXIT
+trap 'exit 130' INT
+trap 'exit 143' TERM
+
+#
+# Load or build the weighted schedule
+#
+SCHEDULE=$WORKDIR/schedule
+if ! schedule_load "$MODE" "$SRCROOT" "$SCHEDULE" TOTAL; then
+ schedule_build "$MODE" "$SRCROOT" "$SCHEDULE" TOTAL ||
+ die "failed to build make schedule for $SRCROOT"
+fi
+printf '%s: expecting %s weighted ticks for %s\n' "$pgm" "$TOTAL" "$MODE" >&2
+
+#
+# High-water mark from a previous run, for reattaching to a build in progress
+#
+schedule_cache_file "$MODE" "$SRCROOT" HW_FILE
+HW_FILE=${HW_FILE%.schedule}.highwater
+highwater_load "$HW_FILE" "$TOTAL" HW_FLOOR || : no floor
+
+if [ "$INPUT" ]; then
+ printf '%s: reading %s; waiting for make progress banners ...\n' \
+ "$pgm" "$INPUT" >&2
+else
+ ensure_snp_privs ||
+ die "sudo(8) authentication failed; needed for watch(8)"
+ printf '%s: snooping %s; waiting for make progress banners ...\n' \
+ "$pgm" "$WATCH_TTY" >&2
+fi
+input_source |
+ banner_filter |
+ schedule_seek "$MODE" "$SCHEDULE" "$HW_FLOOR" "$HW_FILE" |
+ bsdpv -li "%'10lli targets reached" "$TOTAL:$SRCROOT $MODE"
+
+################################################################################
+# END
+################################################################################
diff --git a/usr.bin/srcpv/srcpv.1 b/usr.bin/srcpv/srcpv.1
new file mode 100644
--- /dev/null
+++ b/usr.bin/srcpv/srcpv.1
@@ -0,0 +1,267 @@
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.\" Copyright (c) 2026 Devin Teske <dteske@FreeBSD.org>
+.\"
+.Dd September 4, 2026
+.Dt SRCPV 1
+.Os
+.Sh NAME
+.Nm srcpv
+.Nd progress bar for make targets in the FreeBSD source tree
+.Sh SYNOPSIS
+.Nm
+.Ar mode
+.Op Fl \&Rv
+.Op Fl s Ar dir
+.Fl f Ar file
+.Nm
+.Ar mode
+.Op Fl \&Rv
+.Op Fl s Ar dir
+.Fl w Ar tty
+.Nm
+.Fl v
+.Sh DESCRIPTION
+.Nm
+displays a
+.Xr bsdpv 1
+progress bar for a
+.Cm buildworld
+or
+.Cm buildkernel
+running elsewhere, as selected by
+.Ar mode .
+It reads the output of
+.Xr make 1
+from a file, FIFO, or standard input
+.Pq Fl f ,
+or snoops the terminal running the build with
+.Xr watch 8
+.Pq Fl w ;
+recognizes the stage and directory banners in that output;
+and advances the bar through a weighted schedule of the build.
+The build itself is not modified and need not be started in any special way.
+.Pp
+For
+.Cm buildworld ,
+the schedule is derived from
+.Pa Makefile.inc1
+in the source tree:
+the
+.Va WMAKE_TGTS
+stages, the ordered library lists
+.Pq Va _prereq_libs , _startup_libs , _prebuild_libs , _generic_libs ,
+each compat pass
+.Pq Cm build32 and friends ,
+and the
+.Va SUBDIR
+list built by the
+.Cm everything
+stage.
+Each library or subtree is weighted by the number of objects and archives
+its sources are expected to produce, so the bar moves in proportion to the
+work remaining rather than the number of directories.
+.Pp
+For
+.Cm buildkernel ,
+the schedule covers each kernel in
+.Va KERNCONF :
+its numbered stages, the kernel proper, weighted by the objects
+.Xr config 8
+and the kernel makefiles agree on, and each module under
+.Pa sys/modules
+that the kernel builds, weighted by its own objects.
+Because a kernel and its modules build in parallel, module banners only
+widen the window the bar may advance within; the objects themselves move it.
+.Pp
+Weighing the tree takes a few seconds the first time and is cached
+.Pq see Sx FILES ;
+the cache is rebuilt when the makefiles that shape the schedule change.
+.Pp
+The bar may be started before, during, or after the build begins.
+When
+.Nm
+is stopped and restarted against a build already in progress, the position
+reached by the previous run is restored once the live banners confirm the
+build is at or past it.
+.Pp
+Reading with
+.Fl f
+needs no privilege.
+Snooping with
+.Fl w
+opens
+.Xr snp 4 ,
+which is root-only by default; when
+.Nm
+is not root and cannot write
+.Pa /dev/snp ,
+it validates
+.Xr sudo 8 Pq Pa ports/security/sudo
+before starting
+.Pq so the password prompt is not hidden behind the progress view
+and runs
+.Xr watch 8
+through it.
+.Pp
+The following modes are available:
+.Bl -tag -width ".Cm buildkernel"
+.It Cm buildworld
+Follow
+.Xr make 1
+.Cm buildworld .
+.It Cm buildkernel
+Follow
+.Xr make 1
+.Cm buildkernel .
+.El
+.Pp
+The following options are available:
+.Bl -tag -width "-s dir"
+.It Fl f Ar file
+Read
+.Xr make 1
+output from
+.Ar file .
+A regular file is followed as it grows, as with
+.Xr tail 1
+.Fl f ;
+a FIFO, or
+.Ql -
+for standard input, is read to end of file.
+.It Fl R
+Refresh the schedule cache even if it appears current.
+.It Fl s Ar dir
+Source tree to schedule.
+Default is
+.Pa /usr/src .
+.It Fl v
+Print version information and exit.
+.It Fl w Ar tty
+Terminal on which the build is running, as accepted by
+.Xr watch 8 ;
+for example
+.Ql pts/1 .
+Use
+.Xr tty 1
+on that terminal to find its name.
+.El
+.Sh ENVIRONMENT
+.Bl -tag -width ".Ev SRCPV_HIGHWATER_MAXAGE"
+.It Ev TARGET , TARGET_ARCH
+Target platform and architecture of the build, as for
+.Xr make 1 .
+Default is the host; when only one is set, the other is derived the same way
+the top-level
+.Pa Makefile
+derives it.
+.It Ev KERNCONF
+Kernel configuration(s) scheduled by
+.Cm buildkernel ,
+as for
+.Xr make 1 .
+Default is that of the top-level
+.Pa Makefile ,
+normally
+.Cm GENERIC .
+.It Ev SRCPV_CACHE_DIR
+Directory for cached schedules.
+Default is
+.Pa $XDG_CACHE_HOME/srcpv ,
+or
+.Pa $HOME/.cache/srcpv .
+.It Ev SRCPV_HIGHWATER_MAXAGE
+Age in seconds beyond which the position saved by a previous run is
+ignored.
+Default is 21600
+.Pq six hours .
+.It Ev SRCPV_STAGE_WEIGHT
+Weight, in ticks, of a build stage that has no directory of its own
+.Pq for example _worldtmp , _includes , or configuring the kernel .
+Default is 32.
+.El
+.Sh FILES
+.Bl -tag -width "*.highwater" -compact
+.It Pa *.schedule
+Cached schedule for one mode, source tree, and target.
+.It Pa *.highwater
+Position reached by the most recent run.
+.El
+.Pp
+Both live in the cache directory
+.Pq see Sx ENVIRONMENT
+and may be removed at any time.
+.Sh EXIT STATUS
+.Ex -std
+.Sh EXAMPLES
+Log the build on one terminal and follow the log from another:
+.Bd -literal -offset indent
+$ cd /usr/src && make -j16 buildworld 2>&1 | tee /var/tmp/build.log
+.Ed
+.Bd -literal -offset indent
+$ srcpv buildworld -f /var/tmp/build.log
+.Ed
+.Pp
+Show only the progress bar:
+.Bd -literal -offset indent
+$ cd /usr/src && make -j16 buildworld 2>&1 | srcpv buildworld -f -
+.Ed
+.Pp
+Snoop a build already running on another terminal
+.Pq whose name Xr tty 1 reports there :
+.Bd -literal -offset indent
+$ srcpv buildworld -w pts/1
+.Ed
+.Pp
+Follow a cross build of
+.Pa ~/src
+for arm64:
+.Bd -literal -offset indent
+$ TARGET=arm64 srcpv buildworld -s ~/src -w pts/1
+.Ed
+.Pp
+Follow a kernel build of two configurations:
+.Bd -literal -offset indent
+$ KERNCONF="GENERIC MINIMAL" srcpv buildkernel -w pts/1
+.Ed
+.Sh SEE ALSO
+.Xr bsdpv 1 ,
+.Xr make 1 ,
+.Xr tail 1 ,
+.Xr tty 1 ,
+.Xr snp 4 ,
+.Xr build 7 ,
+.Xr config 8 ,
+.Xr sudo 8 ,
+.Xr watch 8
+.Sh HISTORY
+The
+.Nm
+utility first appeared in
+.Fx 16.0 .
+.Sh AUTHORS
+.An Devin Teske Aq Mt dteske@FreeBSD.org
+.Sh CAVEATS
+Progress is estimated from the order and expected output of the build, not
+measured.
+Parallel
+.Pq Fl j
+builds interleave banners from several directories, and a directory's
+weight is a count of its sources, not the time each takes to compile, so
+the bar is an approximation that tends toward accuracy as the build
+proceeds.
+.Pp
+A kernel object and a module object of the same name cannot be told apart
+in the output, so such module objects are left out of the schedule and
+not counted.
+.Pp
+Only the
+.Cm buildworld
+and
+.Cm buildkernel
+targets are supported;
+.Cm universe
+and
+.Cm release
+are planned.

File Metadata

Mime Type
text/plain
Expires
Tue, Sep 8, 1:06 PM (20 h, 48 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
38489060
Default Alt Text
D59425.diff (50 KB)

Event Timeline