|
| 1 | +# lib/progress.sh — pipe a long `make` through buildmeter (third_party/buildmeter, |
| 2 | +# the standalone progress-meter package — formerly scripts/forge/progress.py) when |
| 3 | +# interactive, so a 72-minute build shows a live progress bar instead of an |
| 4 | +# endless scroll of CC/LD lines. |
| 5 | +# |
| 6 | +# Falls through to plain `make` (no pipe) when ANY of these holds: |
| 7 | +# - stdout isn't a TTY (CI, log redirect, forge stage capture) — plain output |
| 8 | +# - FORGE_PROGRESS=0 is set in the environment — plain output |
| 9 | +# - progress.py or python3 is missing — plain output |
| 10 | +# - the `make -n` pre-scan fails — indeterminate bar |
| 11 | +# |
| 12 | +# Usage (from a build script that has _SCRIPT_DIR set): |
| 13 | +# source "${_SCRIPT_DIR}/lib/progress.sh" |
| 14 | +# forge_progress_run kernel make ARCH=$ARCH CROSS_COMPILE=$CC -j$(nproc) zImage dtbs |
| 15 | +# forge_progress_run buildroot make BR2_TOOLCHAIN_EXTERNAL_PATH=$TC |
| 16 | +# |
| 17 | +# Pre-scan: runs `<make-cmd> -n` once first to count the build units (the |
| 18 | +# denominator for % + ETA). On a clean build this enumerates every step; on an |
| 19 | +# incremental build, only the steps about to run — either way it matches what |
| 20 | +# the real build will do. Disable with FORGE_PROGRESS_PRESCAN=0 (then the bar |
| 21 | +# runs indeterminate: count + rate + elapsed, no %). |
| 22 | +# |
| 23 | +# Exit code: returns the make command's exit (PIPESTATUS[0]), so `set -e` / |
| 24 | +# `pipefail` in the caller still catch real build failures. The progress.py |
| 25 | +# consumer is best-effort and never changes the build's success/failure. |
| 26 | + |
| 27 | +# Self-locate so this lib doesn't depend on the caller having set _SCRIPT_DIR |
| 28 | +# (matches lib/toolchain.sh's BASH_SOURCE pattern; safe under `set -u`). |
| 29 | +_PROGRESS_LIB_DIR=$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd) |
| 30 | +# Points at buildmeter's script-style CLI entry (third_party/buildmeter |
| 31 | +# submodule). cli.py self-fixes sys.path so `python3 "$FORGE_PROGRESS_PY"` works |
| 32 | +# without PYTHONPATH. Same flag contract as the old progress.py (kind / --total / |
| 33 | +# --count-only / --log / --ignore-errors), so callers (build-uboot.sh's custom |
| 34 | +# block) need no business-logic change. |
| 35 | +FORGE_PROGRESS_PY="${_PROGRESS_LIB_DIR}/../../third_party/buildmeter/src/buildmeter/cli.py" |
| 36 | + |
| 37 | +forge_progress_run() { |
| 38 | + local kind="$1"; shift |
| 39 | + local progress_py="$FORGE_PROGRESS_PY" |
| 40 | + |
| 41 | + # Non-interactive or disabled → plain make, preserve exit. |
| 42 | + if [[ "${FORGE_PROGRESS:-1}" != "1" ]] || [[ ! -t 1 ]] \ |
| 43 | + || [[ ! -f "$progress_py" ]] || ! command -v python3 >/dev/null 2>&1; then |
| 44 | + "$@" |
| 45 | + return $? |
| 46 | + fi |
| 47 | + |
| 48 | + # stdbuf -oL makes `make` line-buffer its stdout. make block-buffers when |
| 49 | + # stdout is a pipe (which it is here), and would starve the bar until EOF — |
| 50 | + # the bar would only appear at the very end. Fall back to plain if stdbuf |
| 51 | + # isn't available (coreutils; virtually always present on Linux). |
| 52 | + local buf="" |
| 53 | + if command -v stdbuf >/dev/null 2>&1; then buf="stdbuf -oL"; fi |
| 54 | + |
| 55 | + # Pre-scan: dry-run for the denominator. Emit a status line first so the |
| 56 | + # silent dry-run (can take tens of seconds on a big tree) doesn't look like a hang. |
| 57 | + # `{ make -k -n || true; }` — two dry-run hazards swallowed: |
| 58 | + # - `-k` (keep-going): the dry-run aborts early on errors that only happen in |
| 59 | + # -n (e.g. kernel tools/objtool sub-make fails because libsubcmd isn't built |
| 60 | + # in a dry-run), truncating the CC enumeration → undercount → bar overflows |
| 61 | + # past 100%. -k makes make enumerate the rest despite the error. |
| 62 | + # - `|| true`: that same sub-make error makes `make -n` exit non-zero; swallow |
| 63 | + # it so the caller's `set -o pipefail` doesn't fail the pipe + zero the total. |
| 64 | + local total=0 |
| 65 | + if [[ "${FORGE_PROGRESS_PRESCAN:-1}" == "1" ]]; then |
| 66 | + printf '[INFO] counting build units (make -n)…\n' >&2 |
| 67 | + # NOTE: buildmeter --count-only stderr is NOT silenced here — on a TTY it |
| 68 | + # renders the pre-scan spinner (`⠙ scanning dry-run (make -n)…`), giving the |
| 69 | + # 15-30s dry-run a heartbeat. It only writes stderr on a TTY, so CI / pipe |
| 70 | + # / non-interactive is unaffected. The make-side 2>/dev/null above still |
| 71 | + # swallows make -n's own noise. |
| 72 | + total=$({ "$@" -k -n 2>/dev/null || true; } \ |
| 73 | + | python3 "$progress_py" --count-only "$kind") || total=0 |
| 74 | + if [[ "$total" -le 0 ]]; then |
| 75 | + printf '[INFO] pre-scan returned 0 — running indeterminate (no %% bar)\n' >&2 |
| 76 | + fi |
| 77 | + fi |
| 78 | + |
| 79 | + # Tee the full make output to a per-build log under /tmp (preserved for |
| 80 | + # reference / debugging), pipe through progress.py for the live bar. |
| 81 | + local logf="/tmp/forge-${kind}-${BASHPID:-$$}.log" |
| 82 | + printf '[INFO] full build log → %s\n' >&2 "$logf" |
| 83 | + if [[ "$total" -gt 0 ]] 2>/dev/null; then |
| 84 | + $buf "$@" 2>&1 | tee "$logf" | python3 "$progress_py" "$kind" --total "$total" --log "$logf" |
| 85 | + else |
| 86 | + $buf "$@" 2>&1 | tee "$logf" | python3 "$progress_py" "$kind" --log "$logf" |
| 87 | + fi |
| 88 | + return "${PIPESTATUS[0]}" |
| 89 | +} |
0 commit comments