Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F170912759
D59425.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
50 KB
Referenced Files
None
Subscribers
None
D59425.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D59425: srcpv: Add a progress bar for buildworld and buildkernel
Attached
Detach File
Event Timeline
Log In to Comment