diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index fbd1c19..3035228 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -101,7 +101,7 @@ jobs:
# The static toolchain lives in its own repo (yeet-src/toolchain) and
# is fetched on demand into a per-machine cache, so nothing heavy
# ships in this archive. The toolchain/ subtree is dev-time source
- # only (build recipe + embed glue) — the payload under template/build/
+ # only (build recipe + embed glue) — the payload under template/.build/
# already carries its synced copies, so exclude it from the archive.
repo="${GITHUB_REPOSITORY##*/}"
tar \
diff --git a/.gitignore b/.gitignore
index 2ea1e04..cd94b3f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,8 +2,12 @@
# nothing is built here. Guard against stray artifacts if someone runs a
# build inside template/ while editing it.
template/node_modules/
-template/src/index.jsx
-template/.build/
+template/dist/
+# template/.build/ is the committed build machinery (the frontend Makefile,
+# *.mk, the shell scripts, toolchain.lock) that `scripts/new` materializes into
+# a generated project's own .build/ (which that project gitignores wholesale).
+# Only the artifacts written *inside* it are ignored here, not the machinery.
+template/.build/bpf/
template/bin/*
!template/bin/.gitkeep
template/src/bpf/include/vmlinux.h
diff --git a/Makefile b/Makefile
index 162206b..5669dd9 100644
--- a/Makefile
+++ b/Makefile
@@ -5,7 +5,7 @@
# The static build toolchain (clang, bpftool, esbuild, make, git) lives in its
# own repo, yeet-src/toolchain, vendored here under toolchain/ and pinned to a
# release tag. The template payload doesn't read toolchain/ directly — it
-# carries its own copies under template/build/ (the embed glue + a
+# carries its own copies under template/.build/ (the embed glue + a
# toolchain.lock) so a generated project stays self-contained. `sync-toolchain`
# refreshes both from a tag.
TOOLCHAIN_REPO ?= git@github.com:yeet-src/toolchain.git
@@ -37,10 +37,18 @@ sync-toolchain:
@split=$$(git rev-parse FETCH_HEAD); \
echo ">> replacing toolchain/ with $(TOOLCHAIN_TAG) ($$split)"; \
rm -rf toolchain && mkdir toolchain && git archive FETCH_HEAD | tar -x -C toolchain; \
- cp toolchain/embed/toolchain.mk template/build/toolchain.mk; \
- cp toolchain/embed/fetch-toolchain.sh template/build/fetch-toolchain.sh; \
- cp toolchain/build/versions.env template/build/toolchain.lock; \
- git add -A toolchain template/build/toolchain.mk template/build/fetch-toolchain.sh template/build/toolchain.lock; \
+ : 'The embed glue assumes the machinery dir is named build/; we vendor it'; \
+ : 'into .build/, so rewrite that path prefix as we copy (keeps the sync'; \
+ : 'robust no matter what the upstream toolchain names it). We also gate the'; \
+ : 'always-on "toolchain ready" summary behind YEET_TOOLCHAIN_QUIET so a'; \
+ : 'normal build stays quiet (pretty.mk sets it; V=1 shows it). If upstream'; \
+ : 'reworks that line the gate simply no-ops and the summary returns.'; \
+ sed 's|build/|.build/|g' toolchain/embed/toolchain.mk > template/.build/toolchain.mk; \
+ sed -e 's|build/|.build/|g' \
+ -e 's#^echo ">> toolchain ready:#[ -n "$${YEET_TOOLCHAIN_QUIET:-}" ] || echo ">> toolchain ready:#' \
+ toolchain/embed/fetch-toolchain.sh > template/.build/fetch-toolchain.sh; \
+ cp toolchain/build/versions.env template/.build/toolchain.lock; \
+ git add -A toolchain template/.build/toolchain.mk template/.build/fetch-toolchain.sh template/.build/toolchain.lock; \
if git diff --cached --quiet; then \
echo ">> already at $(TOOLCHAIN_TAG) — nothing to commit"; \
else \
diff --git a/README.md b/README.md
index 5170c6c..8316c52 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ make new DEST=
[NAME=] # or: scripts/new [name]
The static build toolchain (clang, bpftool, esbuild, make, git) lives in its own
repo, [yeet-src/toolchain](https://github.com/yeet-src/toolchain), vendored here
under `toolchain/` and pinned to a release tag. Generated projects don't read
-`toolchain/` — they carry their own copies under `template/build/` (the embed
+`toolchain/` — they carry their own copies under `template/.build/` (the embed
glue + a `toolchain.lock`). Refresh both from a toolchain release with:
```sh
diff --git a/scripts/new b/scripts/new
index f4ebb99..830d272 100755
--- a/scripts/new
+++ b/scripts/new
@@ -40,11 +40,28 @@ if [ -d "$DEST" ] && [ -n "$(ls -A "$DEST" 2>/dev/null)" ]; then
fi
mkdir -p "$DEST"
-# Copy the payload (including dotfiles) into the destination.
+# Copy the payload (including dotfiles) into the destination. This is what
+# materializes the build back end: .build/ (the frontend Makefile, *.mk, the
+# toolchain scripts + the toolchain.lock pin) is copied along with everything
+# else, so the generated project can `yeet build` immediately — even though it
+# gitignores .build/ wholesale and `yeet build` can re-materialize it later.
cp -R "$SRC/." "$DEST/"
-# The toolchain pin (build/toolchain.lock) ships in the template, so it is
-# copied along with everything else above — no stamping needed here.
+# Drop any build outputs a dirty template tree may have carried in, so a fresh
+# scaffold starts clean (all of these are regenerated by the build). These are
+# gitignored in the template, so a clean checkout won't have them anyway.
+rm -rf "$DEST/.build/bpf" "$DEST/node_modules" "$DEST/dist" \
+ "$DEST/src/bpf/include/vmlinux.h" "$DEST/.kmatrix"
+find "$DEST/bin" -type f ! -name .gitkeep -delete 2>/dev/null || true
+
+# The build back end under .build/ ships committed in the template (this repo's
+# source of truth), but a generated project treats the whole thing as
+# regenerable — `yeet build` materializes it — so ignore it wholesale. The
+# template's own .gitignore only ignores .build/bpf/ (so the machinery stays
+# tracked here); a scaffold widens that to all of .build/. Idempotent.
+if [ -f "$DEST/.gitignore" ] && ! grep -qxF '/.build/' "$DEST/.gitignore"; then
+ printf '\n# Regenerable build back end (`yeet build` materializes it).\n/.build/\n' >>"$DEST/.gitignore"
+fi
# Substitute __NAME__ in the files that carry it. Portable in-place edit
# (BSD and GNU sed disagree on -i), so go through a temp file.
diff --git a/template/.build/Makefile b/template/.build/Makefile
new file mode 100644
index 0000000..ea29d19
--- /dev/null
+++ b/template/.build/Makefile
@@ -0,0 +1,104 @@
+# Low-level build machinery for the yeet script project — the back end behind
+# `yeet build`. It and its siblings (bpf.mk, toolchain.mk, the shell scripts,
+# toolchain.lock) live under .build/, which a generated project gitignores
+# wholesale; `yeet build` (or the thin root Makefile shim, which forwards
+# `make` here from the project root) drives it. Every path below is written
+# relative to the *project root*, since that stays the working directory:
+#
+# yeet build — build everything (BPF objects + JS bundle) [= all]
+# make bpf — compile bpf/*.bpf.c into bin/* only
+# make veristat — load the built object with veristat (verifier check on this kernel)
+# make bundle — bundle the JS entry with esbuild
+# make postgen — finalize a freshly generated project (git init)
+# make clangd — write a local .clangd pointing at the resolved toolchain
+# make clean — remove build artifacts
+#
+# This is the build *frontend* logic: it orchestrates two independent
+# compilers — clang for the BPF objects, esbuild for the JS bundle.
+# Neither understands the other; the JS references compiled objects in
+# bin/ only by path, resolved at runtime. `yeet run` invokes the build
+# automatically when running this project from a trusted remote source,
+# so the default goal must leave the project runnable.
+#
+# clang, bpftool and esbuild come from the static toolchain resolved by
+# .build/toolchain.mk (a shared per-machine cache, or binaries vendored in
+# the bootstrap repo) — so the build needs no system C/BPF toolchain.
+
+.DEFAULT_GOAL := all
+
+include .build/pretty.mk
+include .build/toolchain.mk
+include .build/bpf.mk
+
+# `banner` prints first (leftmost prerequisite, built before bpf/bundle in a
+# serial build) and the recipe's summary prints last, so a build reads as a
+# titled progress list. Both are cheap, so they show even on an up-to-date
+# build — a consistent frame around whatever work actually ran.
+all: banner bpf bundle
+ @b=$(BPF_OUT); j=dist/index.jsx; \
+ bs=$$(wc -c <"$$b" 2>/dev/null || echo 0); js=$$(wc -c <"$$j" 2>/dev/null || echo 0); \
+ $(SAY) done "$$(printf '%s (%d KiB) · %s (%d KiB)' "$$b" "$$((bs/1024))" "$$j" "$$((js/1024))")"
+
+banner:
+ @$(SAY) head "building $(notdir $(CURDIR))"
+
+# Bundle the entry with the vendored esbuild. esbuild honors tsconfig `paths`
+# (so `@/` resolves at bundle time), while `yeet:*` builtins and `*.bpf.o`
+# objects stay external. The bundle is written to dist/index.jsx (a gitignored
+# build-output dir), which the entry resolver prefers over the raw src/main.jsx
+# — so once built, that is what `yeet run .` runs. The .jsx extension keeps the
+# bundle eligible for component auto-mount.
+# Compiled BPF objects in bin/ are loaded by path at runtime, never imported,
+# so they are not bundled.
+#
+# The build needs no npm/node: the starter imports only `yeet:*` builtins and
+# local `@/` modules, which esbuild resolves on its own. If you add third-party
+# packages to package.json, install them into node_modules with the package
+# manager of your choice — esbuild inlines whatever it finds there.
+# `--log-level=warning` drops esbuild's own success summary (the outfile/size
+# line and "Done in Nms") so the build shows just our one `bundle` step — real
+# warnings and errors still print.
+#
+# `--define:import.meta.main=false` compiles out the `import.meta.main`
+# self-test guards (see AGENTS.md). Bundling collapses every module into one
+# file that shares the entry's `import.meta`, so without this each guarded
+# block would see `main === true` and all fire at once in the built app;
+# defined to false they become `if (false)` and drop. A standalone
+# `yeet run src/probes/foo.js` isn't bundled, so its guard still runs.
+ESBUILD_FLAGS := --bundle --format=esm --platform=neutral \
+ --main-fields=module,main --conditions=import,module --log-level=warning \
+ --define:import.meta.main=false \
+ --outfile=dist/index.jsx --jsx=automatic --jsx-import-source=yeet:tui
+
+bundle: | toolchain
+ @$(SAY) step bundle "src/main.jsx → dist/index.jsx"
+ $(Q)$(ESBUILD) src/main.jsx $(ESBUILD_FLAGS) '--external:yeet:*' '--external:*.bpf.o'
+
+# Post-generation finalize: initialize a git repository with the vendored git
+# (fetched via `vendored-git`). Idempotent — skipped if this is already a repo.
+# The scaffolders (`yeet new`, `scripts/new`) run `make postgen` after creating
+# the project, so the CLI itself stays a thin caller of make.
+postgen: | vendored-git
+ @g="$(GIT)"; [ -x "$$g" ] || g="$$(command -v git 2>/dev/null || true)"; \
+ if [ -e .git ]; then \
+ echo "postgen: already a git repository"; \
+ elif [ -n "$$g" ]; then \
+ echo "postgen: git init"; \
+ "$$g" -c init.templateDir= init -q . || echo "warning: 'git init' failed" >&2; \
+ else \
+ echo "warning: no git available (vendored or host); skipping 'git init'" >&2; \
+ fi
+
+# Mirrors the build's titled progress list: a head banner, one step per
+# artifact group removed (BPF objects via clean-bpf, then the JS build
+# outputs), and a done summary. clean-banner is the leftmost prerequisite so
+# it prints before clean-bpf's own step, exactly as `banner` fronts `all`.
+clean: clean-banner clean-bpf
+ @$(SAY) step js "dist/ · node_modules/"
+ $(Q)rm -rf dist node_modules
+ @$(SAY) done "cleaned $(notdir $(CURDIR))"
+
+clean-banner:
+ @$(SAY) head "cleaning $(notdir $(CURDIR))"
+
+.PHONY: all banner bundle clean clean-banner postgen
diff --git a/template/build/bpf.mk b/template/.build/bpf.mk
similarity index 80%
rename from template/build/bpf.mk
rename to template/.build/bpf.mk
index 0650851..46e97ea 100644
--- a/template/build/bpf.mk
+++ b/template/.build/bpf.mk
@@ -10,7 +10,7 @@
# To add another independent object, give it its own link target alongside
# bin/probe.bpf.o below — there is intentionally no per-object magic here.
-# CLANG/BPFTOOL are normally resolved by build/toolchain.mk (vendored static
+# CLANG/BPFTOOL are normally resolved by .build/toolchain.mk (vendored static
# toolchain or shared cache), included before this file. These are the last
# resort when neither is available: whatever is on PATH. bpftool frequently
# lives in /usr/sbin, which isn't always on a non-root user's PATH; fall back
@@ -25,8 +25,9 @@ ARCH := $(ARCH:aarch64=arm64)
VMLINUX := src/bpf/include/vmlinux.h
BPF_SRCS := $(wildcard src/bpf/*.bpf.c)
-# One intermediate object per unit. They live under .build/ so they are
-# never mistaken for the loadable object in bin/.
+# One intermediate object per unit. They live under .build/bpf/ (beside the
+# build machinery, both hidden under .build/) so they are never mistaken for
+# the loadable object in bin/.
BPF_OBJS := $(patsubst src/bpf/%.bpf.c,.build/bpf/%.bpf.o,$(BPF_SRCS))
# The single linked object. Its `.bpf.o` suffix is what the JS side loads
# with `import probe from "../bin/probe.bpf.o"` (the loader's
@@ -35,7 +36,7 @@ BPF_OUT := bin/probe.bpf.o
BPF_CFLAGS ?= -g -O2 -Wall -target bpf -D__TARGET_ARCH_$(ARCH) -mcpu=v3 -I src/bpf/include
# Add the vendored libbpf program headers (, …) when a
-# vendored toolchain supplies them; resolved by build/toolchain.mk. Without
+# vendored toolchain supplies them; resolved by .build/toolchain.mk. Without
# it, the build falls back to a host libbpf-dev on the default include path.
BPF_CFLAGS += $(if $(BPF_SYSINCLUDE),-I$(BPF_SYSINCLUDE))
@@ -45,34 +46,39 @@ bpf: $(BPF_OUT)
# the cache before any rule shells out to them, without forcing rebuilds.
$(VMLINUX): | toolchain
@command -v $(BPFTOOL) >/dev/null 2>&1 || { echo "error: bpftool not found — install bpftool / linux-tools"; exit 1; }
- sh build/gen-vmlinux.sh $(BPFTOOL) $@
+ @$(SAY) step gen "vmlinux.h (kernel BTF)"
+ $(Q)sh .build/gen-vmlinux.sh $(BPFTOOL) $@ >/dev/null
# Compile each unit to an intermediate object.
.build/bpf/%.bpf.o: src/bpf/%.bpf.c $(VMLINUX) | toolchain
@command -v $(CLANG) >/dev/null 2>&1 || { echo "error: clang not found — install clang"; exit 1; }
@mkdir -p $(dir $@)
- $(CLANG) $(BPF_CFLAGS) -c $< -o $@
+ @$(SAY) step cc "$<"
+ $(Q)$(CLANG) $(BPF_CFLAGS) -c $< -o $@
# Statically link every unit into the single loadable object.
$(BPF_OUT): $(BPF_OBJS) | bin toolchain
@command -v $(BPFTOOL) >/dev/null 2>&1 || { echo "error: bpftool not found — install bpftool / linux-tools"; exit 1; }
- $(BPFTOOL) gen object $@ $(BPF_OBJS)
+ @$(SAY) step link "$@"
+ $(Q)$(BPFTOOL) gen object $@ $(BPF_OBJS)
bin:
mkdir -p bin
clean-bpf:
- rm -rf $(BPF_OUT) .build $(VMLINUX)
+ @$(SAY) step bpf "$(BPF_OUT) · .build/bpf"
+ $(Q)rm -rf $(BPF_OUT) .build/bpf $(VMLINUX)
# Load the linked object with veristat to confirm THIS kernel's verifier
# accepts every program, and to see per-program complexity (insns/states) — a
# local counterpart to the kernel-matrix CI, which runs the same check across
# many kernels. Loading BPF programs needs privileges, so run with sudo (as
-# `yeet run` does). VERISTAT is resolved by build/toolchain.mk (the vendored
+# `yeet run` does). VERISTAT is resolved by .build/toolchain.mk (the vendored
# static binary, or `veristat` on PATH).
.PHONY: veristat
veristat: $(BPF_OUT) | toolchain
- @command -v $(VERISTAT) >/dev/null 2>&1 || { echo "error: veristat not found ($(VERISTAT)) — bump build/toolchain.lock to a toolchain that ships veristat, or install veristat on PATH"; exit 1; }
+ @command -v $(VERISTAT) >/dev/null 2>&1 || { echo "error: veristat not found ($(VERISTAT)) — bump .build/toolchain.lock to a toolchain that ships veristat, or install veristat on PATH"; exit 1; }
+ @$(SAY) step veristat "$(BPF_OUT)"
$(VERISTAT) $(BPF_OUT)
# Run the same verifier check across a matrix of kernels locally (Linux + KVM),
@@ -81,7 +87,7 @@ veristat: $(BPF_OUT) | toolchain
# KERNELS="6.6-main bpf-next-main" or rely on the script's default spread.
.PHONY: veristat-matrix
veristat-matrix: $(BPF_OUT) | toolchain
- VERISTAT="$(VERISTAT)" sh build/kernel-matrix.sh $(KERNELS)
+ VERISTAT="$(VERISTAT)" sh .build/kernel-matrix.sh $(KERNELS)
# Write a local .clangd so the editor resolves vmlinux.h, the libbpf SDK
# headers and __u* types using the *resolved* toolchain include path — unlike
@@ -92,7 +98,7 @@ veristat-matrix: $(BPF_OUT) | toolchain
.PHONY: clangd
clangd:
@printf '%s\n' \
- '# Generated by `make clangd` — editor flags mirroring build/bpf.mk.' \
+ '# Generated by `make clangd` — editor flags mirroring .build/bpf.mk.' \
'# Regenerate after moving machines or bumping the toolchain version.' \
'CompileFlags:' \
' Add:' \
diff --git a/template/build/fetch-toolchain.sh b/template/.build/fetch-toolchain.sh
similarity index 95%
rename from template/build/fetch-toolchain.sh
rename to template/.build/fetch-toolchain.sh
index eff6a87..e6b852a 100755
--- a/template/build/fetch-toolchain.sh
+++ b/template/.build/fetch-toolchain.sh
@@ -4,7 +4,7 @@
# toolchain repo (releases/download/v/) and is
# checksum-verified against the pins in the lock.
#
-# build/fetch-toolchain.sh [tool...]
+# .build/fetch-toolchain.sh [tool...]
#
# With no trailing tool names, all tools are fetched (the build's `toolchain`
# target). Pass names (e.g. `git`) to fetch only those — `postgen` uses this to
@@ -95,4 +95,4 @@ if want headers && [ ! -e "$INC/bpf/bpf_helpers.h" ]; then
rm -rf "$td"
fi
-echo ">> toolchain ready: $DIR${FILTER:+ ($FILTER)}"
+[ -n "${YEET_TOOLCHAIN_QUIET:-}" ] || echo ">> toolchain ready: $DIR${FILTER:+ ($FILTER)}"
diff --git a/template/build/gen-vmlinux.sh b/template/.build/gen-vmlinux.sh
similarity index 50%
rename from template/build/gen-vmlinux.sh
rename to template/.build/gen-vmlinux.sh
index a51768b..db35594 100644
--- a/template/build/gen-vmlinux.sh
+++ b/template/.build/gen-vmlinux.sh
@@ -2,7 +2,7 @@
# Generate vmlinux.h from the running kernel's BTF — the CO-RE header
# every BPF program in this project includes.
#
-# build/gen-vmlinux.sh
+# .build/gen-vmlinux.sh
set -eu
@@ -15,5 +15,11 @@ if [ ! -r /sys/kernel/btf/vmlinux ]; then
fi
mkdir -p "$(dirname "$OUT")"
-"$BPFTOOL" btf dump file /sys/kernel/btf/vmlinux format c >"$OUT"
+# bpftool prints a benign "skipping /sys/kernel/btf/vmlinux (will be loaded as
+# base)" note to stderr even on success. Capture stderr and replay it only if
+# the dump actually fails, so a normal build stays quiet.
+if ! err="$("$BPFTOOL" btf dump file /sys/kernel/btf/vmlinux format c 2>&1 >"$OUT")"; then
+ [ -n "$err" ] && printf '%s\n' "$err" >&2
+ exit 1
+fi
echo "generated $OUT"
diff --git a/template/build/kernel-matrix.sh b/template/.build/kernel-matrix.sh
similarity index 90%
rename from template/build/kernel-matrix.sh
rename to template/.build/kernel-matrix.sh
index 3f128c7..919c7f2 100755
--- a/template/build/kernel-matrix.sh
+++ b/template/.build/kernel-matrix.sh
@@ -1,10 +1,10 @@
#!/bin/sh
-# Boot a matrix of kernels locally and run the verifier check (build/verify-
+# Boot a matrix of kernels locally and run the verifier check (.build/verify-
# kernel.sh) in each — the local counterpart to .github/workflows/kernel-
# matrix.yml. This is a TEST HARNESS, not part of the build, and needs a
# Linux host (ideally with /dev/kvm; without it QEMU falls back to slow TCG).
#
-# build/kernel-matrix.sh [kernel ...] # default: an LTS spread + bpf-next
+# .build/kernel-matrix.sh [kernel ...] # default: an LTS spread + bpf-next
# make veristat-matrix # same, via the Makefile
#
# It uses cilium's lvh + QEMU to boot quay.io/lvh-images/kind: images.
@@ -12,7 +12,7 @@
# host KVM/root, not self-contained build tools), so both are fetched on demand:
# - lvh: an `lvh` on PATH, else extracted from the quay.io/lvh-images/lvh image.
# - qemu: the vendored static qemu-.tar.gz from the toolchain release
-# (checksum-pinned in build/toolchain.lock), extracted to the toolchain
+# (checksum-pinned in .build/toolchain.lock), extracted to the toolchain
# cache and prepended to PATH; falls back to a system qemu-system.
# veristat is the vendored static binary, resolved like the build.
@@ -46,8 +46,8 @@ need ssh "install openssh-client"
# fall back to a system qemu-system on PATH. lvh finds qemu via PATH and the
# static qemu finds its firmware blobs relative to its own binary, so we just
# prepend the extracted bin dir to PATH — no -L plumbing into lvh needed.
-if [ -f build/toolchain.lock ]; then
- . ./build/toolchain.lock
+if [ -f .build/toolchain.lock ]; then
+ . ./.build/toolchain.lock
qsha="$(eval "printf '%s' \"\${QEMU_SHA256_${ARCH}:-}\"")"
QDIR="${XDG_CACHE_HOME:-$HOME/.cache}/yeet/toolchain/v${TOOLCHAIN_VERSION}/${ARCH}/qemu"
if [ ! -x "$QDIR/bin/$QEMU" ] && [ -n "$qsha" ] && [ -n "${TOOLCHAIN_BASE_URL:-}" ]; then
@@ -70,7 +70,7 @@ if [ -f build/toolchain.lock ]; then
echo ">> using vendored static qemu: $QDIR/bin/$QEMU"
fi
fi
-need "$QEMU" "no vendored qemu in build/toolchain.lock and none on PATH — bump the lock to a toolchain that ships qemu, or install qemu-system"
+need "$QEMU" "no vendored qemu in .build/toolchain.lock and none on PATH — bump the lock to a toolchain that ships qemu, or install qemu-system"
# --- resolve lvh (PATH, else extract from the OCI image with docker) ----------
LVH="$(command -v lvh || true)"
@@ -91,12 +91,12 @@ fi
echo ">> building $OBJ"
make bpf >/dev/null
VERISTAT="${VERISTAT:-}"
-if [ -z "$VERISTAT" ] && [ -f build/toolchain.lock ]; then
- . ./build/toolchain.lock
+if [ -z "$VERISTAT" ] && [ -f .build/toolchain.lock ]; then
+ . ./.build/toolchain.lock
VERISTAT="${XDG_CACHE_HOME:-$HOME/.cache}/yeet/toolchain/v${TOOLCHAIN_VERSION}/${ARCH}/veristat"
fi
[ -n "$VERISTAT" ] && [ -x "$VERISTAT" ] || VERISTAT="$(command -v veristat || true)"
-[ -n "$VERISTAT" ] && [ -x "$VERISTAT" ] || { echo "error: veristat not found — bump build/toolchain.lock to a toolchain that ships it, or install veristat" >&2; exit 1; }
+[ -n "$VERISTAT" ] && [ -x "$VERISTAT" ] || { echo "error: veristat not found — bump .build/toolchain.lock to a toolchain that ships it, or install veristat" >&2; exit 1; }
install -Dm755 "$VERISTAT" bin/veristat
# --- per-kernel: pull image, boot, verify, stop -------------------------------
@@ -129,7 +129,7 @@ for k in $KERNELS; do
# run the gate in the VM; CSV lands in the host-mounted .kmatrix via /host
if ssh -p "$SSH_PORT" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@127.0.0.1 \
- "cd /host && OUT_CSV=/host/.kmatrix/$k.csv sh build/verify-kernel.sh"; then :; else overall=1; fi
+ "cd /host && OUT_CSV=/host/.kmatrix/$k.csv sh .build/verify-kernel.sh"; then :; else overall=1; fi
stop_vm
done
trap - EXIT INT TERM
diff --git a/template/.build/pretty.mk b/template/.build/pretty.mk
new file mode 100644
index 0000000..c119856
--- /dev/null
+++ b/template/.build/pretty.mk
@@ -0,0 +1,15 @@
+# Pretty build output, shared by the other build fragments.
+#
+# SAY renders a styled status line (see .build/pretty.sh). Q hides the raw tool
+# command behind each step so a normal build shows tidy step lines instead of
+# full clang/bpftool/esbuild invocations — run `make V=1` (or `yeet build V=1`)
+# to see the exact commands. Errors always surface: only stdout on success is
+# suppressed, never a tool's stderr.
+SAY := sh .build/pretty.sh
+Q := $(if $(V),,@)
+
+# Silence the vendored toolchain fetch's "toolchain ready" summary in a normal
+# build (it prints on every build, warm cache included); `V=1` shows it, along
+# with the raw commands. Exported so the fetch script sees it. Real "fetch …"
+# download progress is never suppressed.
+export YEET_TOOLCHAIN_QUIET := $(if $(V),,1)
diff --git a/template/.build/pretty.sh b/template/.build/pretty.sh
new file mode 100644
index 0000000..8d7d2c8
--- /dev/null
+++ b/template/.build/pretty.sh
@@ -0,0 +1,53 @@
+#!/bin/sh
+# Styled status lines for the build — one tidy, colored line per step, so a
+# normal `yeet build` reads as a clean progress list instead of a dump of raw
+# compiler commands. The build fragments call this (as `$(SAY)`) for every
+# step; the raw tool commands are hidden unless `make V=1`.
+#
+# Color + glyphs engage only on a terminal with NO_COLOR unset. Piped or CI
+# logs fall back to plain ASCII markers, so nothing here corrupts a log file.
+#
+# pretty.sh head rule + title (build start)
+# pretty.sh step ▸ tag msg (a build step)
+# pretty.sh info · msg (dim note)
+# pretty.sh done ✔ msg + rule (build finished)
+set -eu
+
+kind=${1:-}
+[ $# -gt 0 ] && shift
+
+if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
+ B=$(printf '\033[1m') # bold
+ D=$(printf '\033[2m') # dim
+ R=$(printf '\033[0m') # reset
+ C=$(printf '\033[36m') # cyan — step tags
+ G=$(printf '\033[32m') # green — success
+ arrow='▸'; bullet='●'; dot='·'; check='✔'
+ rule='────────────────────────────────────────────'
+else
+ B=; D=; R=; C=; G=
+ arrow='>'; bullet='*'; dot='-'; check='✓'
+ rule='--------------------------------------------'
+fi
+
+case "$kind" in
+ head)
+ printf '\n%s%s %s%s\n%s%s%s%s\n' \
+ "$B$C" "$bullet" "$*" "$R" "$D" "$rule" "$R" ""
+ ;;
+ step)
+ tag=${1:-}
+ [ $# -gt 0 ] && shift
+ printf ' %s%s%s %s%-6s%s %s\n' "$B" "$arrow" "$R" "$C" "$tag" "$R" "$*"
+ ;;
+ info)
+ printf ' %s%s%s %s%s%s\n' "$D" "$dot" "$R" "$D" "$*" "$R"
+ ;;
+ done)
+ printf '%s%s%s %s%s%s\n%s%s%s\n\n' \
+ "$B$G" "$check" "$R" "$B" "$*" "$R" "$D" "$rule" "$R"
+ ;;
+ *)
+ printf '%s\n' "$*"
+ ;;
+esac
diff --git a/template/build/toolchain.lock b/template/.build/toolchain.lock
similarity index 100%
rename from template/build/toolchain.lock
rename to template/.build/toolchain.lock
diff --git a/template/build/toolchain.mk b/template/.build/toolchain.mk
similarity index 86%
rename from template/build/toolchain.mk
rename to template/.build/toolchain.mk
index 6cb5f18..e335efe 100644
--- a/template/build/toolchain.mk
+++ b/template/.build/toolchain.mk
@@ -1,9 +1,9 @@
# Resolve the static build toolchain (clang, bpftool, veristat, esbuild, git, gh) — included
-# by the project Makefile before build/bpf.mk, so the tools are set before any
+# by the project Makefile before .build/bpf.mk, so the tools are set before any
# rule uses them. A `make CLANG=…` CLI override beats this.
#
# Tools come from a shared, per-machine cache keyed by the project's pinned
-# toolchain version (build/toolchain.lock). `make toolchain` fills it,
+# toolchain version (.build/toolchain.lock). `make toolchain` fills it,
# downloading each missing tool once from the vendored toolchain release
# (github.com/yeet-src/toolchain). The cache key is the toolchain version,
# never the template version — so updating the template reuses an existing
@@ -24,7 +24,7 @@ UNAME_M := $(UNAME_M:arm64=aarch64)
# Only engage the vendored cache on Linux; elsewhere TOOLCHAIN_LOCK stays empty
# so the PATH fallbacks below win and the fetch targets become no-ops.
ifeq ($(UNAME_S),Linux)
-TOOLCHAIN_LOCK := $(firstword $(wildcard build/toolchain.lock))
+TOOLCHAIN_LOCK := $(firstword $(wildcard .build/toolchain.lock))
endif
ifneq ($(TOOLCHAIN_LOCK),)
include $(TOOLCHAIN_LOCK)
@@ -53,7 +53,7 @@ GH ?= gh
.PHONY: toolchain
toolchain:
ifneq ($(TOOLCHAIN_LOCK),)
- @sh build/fetch-toolchain.sh "$(TOOLCHAIN_DIR)" "$(UNAME_M)" "$(TOOLCHAIN_LOCK)"
+ @sh .build/fetch-toolchain.sh "$(TOOLCHAIN_DIR)" "$(UNAME_M)" "$(TOOLCHAIN_LOCK)"
else
@:
endif
@@ -63,7 +63,7 @@ endif
.PHONY: vendored-git
vendored-git:
ifneq ($(TOOLCHAIN_LOCK),)
- @sh build/fetch-toolchain.sh "$(TOOLCHAIN_DIR)" "$(UNAME_M)" "$(TOOLCHAIN_LOCK)" git \
+ @sh .build/fetch-toolchain.sh "$(TOOLCHAIN_DIR)" "$(UNAME_M)" "$(TOOLCHAIN_LOCK)" git \
|| echo "note: vendored git unavailable; postgen will fall back to host git" >&2
else
@:
diff --git a/template/build/verify-kernel.sh b/template/.build/verify-kernel.sh
similarity index 96%
rename from template/build/verify-kernel.sh
rename to template/.build/verify-kernel.sh
index c4ba606..2ad4c81 100755
--- a/template/build/verify-kernel.sh
+++ b/template/.build/verify-kernel.sh
@@ -5,7 +5,7 @@
# kernel with cilium's little-vm-helper and mounts the project at /host; the
# workflow stages the static veristat into bin/ before booting.
#
-# sh build/verify-kernel.sh [bpf-object] (default: bin/probe.bpf.o)
+# sh .build/verify-kernel.sh [bpf-object] (default: bin/probe.bpf.o)
#
# Set OUT_CSV= to also write a machine-readable result (file,prog,verdict,
# insns,states) — the workflow points it at the mounted workspace so the runner
diff --git a/template/.clangd b/template/.clangd
index 28192a0..e860acb 100644
--- a/template/.clangd
+++ b/template/.clangd
@@ -1,4 +1,4 @@
-# Tell clangd how the BPF units are compiled (mirrors build/bpf.mk), so the
+# Tell clangd how the BPF units are compiled (mirrors .build/bpf.mk), so the
# editor resolves vmlinux.h, the libbpf headers, and the __u* types instead of
# flagging them. vmlinux.h is generated by `make`, so run it once for full
# resolution.
diff --git a/template/.github/workflows/kernel-matrix.yml b/template/.github/workflows/kernel-matrix.yml
index daecdb8..529691f 100644
--- a/template/.github/workflows/kernel-matrix.yml
+++ b/template/.github/workflows/kernel-matrix.yml
@@ -66,18 +66,24 @@ jobs:
- name: Build BPF object + stage veristat
run: |
set -euo pipefail
+ # NOTE: the build machinery lives under .build/, which this project
+ # gitignores (it's regenerable — `yeet build` materializes it). A CI
+ # checkout therefore only has it if you commit it (`git add -f .build`)
+ # or add a step that runs `yeet build` first. `make bpf` below drives
+ # it through the root Makefile shim once .build/ is present.
+ #
# Builds bin/probe.bpf.o with the vendored static toolchain, also
# populating the per-machine toolchain cache (clang/bpftool/veristat).
make bpf
- # Resolve the vendored static veristat the same way build/toolchain.mk
+ # Resolve the vendored static veristat the same way .build/toolchain.mk
# does, and stage it into bin/ so the VM finds it under /host. It is
# fully static, so it runs in any kernel image's rootfs.
- . build/toolchain.lock
+ . .build/toolchain.lock
arch="$(uname -m)"; [ "$arch" = arm64 ] && arch=aarch64
cache="${XDG_CACHE_HOME:-$HOME/.cache}/yeet/toolchain/v${TOOLCHAIN_VERSION}/${arch}"
if [ ! -x "$cache/veristat" ]; then
- echo "::error::veristat is not in the pinned toolchain (v${TOOLCHAIN_VERSION}). Bump build/toolchain.lock to a toolchain release that ships veristat."
+ echo "::error::veristat is not in the pinned toolchain (v${TOOLCHAIN_VERSION}). Bump .build/toolchain.lock to a toolchain release that ships veristat."
exit 1
fi
install -Dm755 "$cache/veristat" bin/veristat
@@ -93,7 +99,7 @@ jobs:
install-dependencies: 'true'
cmd: |
cd /host
- OUT_CSV=/host/.kmatrix/result.csv sh build/verify-kernel.sh
+ OUT_CSV=/host/.kmatrix/result.csv sh .build/verify-kernel.sh
- name: Render kernel summary
if: always()
diff --git a/template/.gitignore b/template/.gitignore
index 733707c..bd3b455 100644
--- a/template/.gitignore
+++ b/template/.gitignore
@@ -1,12 +1,17 @@
# build artifacts
/node_modules/
-/src/index.jsx
-/.build/
+# Compiled JS bundle (the `yeet build` output the runtime prefers over src/).
+/dist/
+# Compiled BPF intermediates. The rest of .build/ is the build back end
+# (Makefile, *.mk, scripts, toolchain.lock); `yeet new` ignores that wholesale
+# too (it appends `/.build/` here — the back end is regenerable), but this
+# repo's own template keeps it committed as the source to materialize from.
+/.build/bpf/
# compiled BPF objects + generated CO-RE header
/bin/*
!/bin/.gitkeep
/src/bpf/include/vmlinux.h
-# kernel-matrix run output (build/kernel-matrix.sh)
+# kernel-matrix run output (.build/kernel-matrix.sh)
/.kmatrix/
diff --git a/template/AGENTS.md b/template/AGENTS.md
index 1bc4fa1..95b7837 100644
--- a/template/AGENTS.md
+++ b/template/AGENTS.md
@@ -5,6 +5,12 @@ isolate, fed by live kernel data (eBPF + a process/system graph). This file
is the API contract and gotcha list for editing it. For build/run mechanics,
layout, and the `@/`/`#/` aliases, see `README.md` — don't duplicate that here.
+> ⚠️ **This may not be the yeet you already know.** The runtime and its APIs
+> move quickly, and there's an unrelated older project by the same name. If
+> anything here conflicts with your priors, the live docs are authoritative —
+> **https://yeet.cx/docs**. When an API feels unfamiliar, check there before
+> relying on training-data memory.
+
## Mental model
It reads like React but it is **signals, not a vdom**. No hooks, no
@@ -40,7 +46,11 @@ blank panel could mean anything.
Before any JSX, confirm the kernel actually gives you the fields and types you
think it does. Guard a self-test with `import.meta.main` — it's `true` **only**
when this module is the run entry, so the block runs when you point `yeet run`
-at the module and stays dormant once `main.jsx` imports it.
+at the module. The build bundles with `import.meta.main` defined to `false`, so
+these guards compile out of `dist/index.jsx` entirely — they never fire in the
+built app, only when you invoke the module directly. (Don't rely on plain
+`main.jsx` imports to keep them dormant: bundling collapses every module into
+one, so without that define they'd all see the entry's `main === true`.)
Verify the **raw source**, not a `from()` signal (a `from()` producer doesn't
run until something watches it — there's no UI here):
@@ -277,6 +287,25 @@ fails silently. Map methods: `lookup/update/delete/entries/lookupBatch` (hash),
`lookup/update` (array), `read/patch` (data-sec), per-CPU lookups return an
array per CPU.
+**Prefer CO-RE and stable hooks.** Write probes Compile Once – Run Everywhere:
+build against `vmlinux.h` + BTF, read fields with `BPF_CORE_READ` and never
+hard-code struct offsets, so one object loads across kernels. Choosing an
+attach point, reach for stability first — **tracepoints** and **LSM** hooks are
+ABI-stable and should be your default; `fentry`/`fexit` on a stable exported
+function next. Drop to `kprobe`/`kretprobe` on internal kernel functions only
+when nothing stable exposes what you need: those functions get renamed,
+inlined, or removed between kernels, so a kprobe that loads today can fail to
+attach tomorrow. Pick the most stable hook that can see the event, not the
+first one that works.
+
+**No `sudo`, no root — ever.** Loading programs, attaching them, and creating
+maps are privileged operations, but *you* never perform them: the script runs
+inside **yeetd**, and the `yeet` CLI holds no privilege of its own — it hands
+the privileged work to the daemon, which carries the delegated BPF capability.
+So BPF runs with the daemon's authorization, not yours. Run `yeet run` /
+`yeet build` as your normal user; if something needs elevation, that's a daemon
+setup concern, never a reason to re-run the CLI under root.
+
## Input — global `tty`
```js
diff --git a/template/Makefile b/template/Makefile
index 59f0eac..da05c64 100644
--- a/template/Makefile
+++ b/template/Makefile
@@ -1,66 +1,22 @@
-# Build the yeet script project.
+# Thin shim → the real build machinery under .build/.
#
-# make — build everything (BPF objects + JS bundle)
-# make bpf — compile bpf/*.bpf.c into bin/* only
-# make veristat — load the built object with veristat (verifier check on this kernel)
-# make bundle — bundle the JS entry with esbuild
-# make postgen — finalize a freshly generated project (git init)
-# make clangd — write a local .clangd pointing at the resolved toolchain
-# make clean — remove build artifacts
+# `yeet build` is the frontend for this script; the low-level orchestration
+# (this project's two compilers — clang for BPF, esbuild for JS — plus the
+# toolchain resolution and the kernel test harness) lives under .build/, which
+# a generated project gitignores wholesale and `yeet build` materializes. This
+# shim just forwards a bare `make` (and the `make` that `yeet run` invokes
+# automatically) to .build/Makefile, keeping the project root as the working
+# directory so every path inside resolves as written.
#
-# This is the build *frontend*: it orchestrates two independent
-# compilers — clang for the BPF objects, esbuild for the JS bundle.
-# Neither understands the other; the JS references compiled objects in
-# bin/ only by path, resolved at runtime. `yeet run` invokes `make`
-# automatically when running this project from a trusted remote source,
-# so the default goal must leave the project runnable.
-#
-# clang, bpftool and esbuild come from the static toolchain resolved by
-# build/toolchain.mk (a shared per-machine cache, or binaries vendored in
-# the bootstrap repo) — so the build needs no system C/BPF toolchain.
-
-.DEFAULT_GOAL := all
-
-include build/toolchain.mk
-include build/bpf.mk
+# Command-line variables (e.g. `make veristat-matrix KERNELS="6.6-main"`) and
+# overrides (`make CLANG=…`) propagate to the sub-make automatically via
+# MAKEFLAGS, so they need no plumbing here.
-all: bpf bundle
+REAL := .build/Makefile
-# Bundle the entry with the vendored esbuild. esbuild honors tsconfig `paths`
-# (so `@/` resolves at bundle time), while `yeet:*` builtins and `*.bpf.o`
-# objects stay external. The bundle is written to src/index.jsx, which the
-# entry ladder prefers over src/main.jsx — so once built, that is what runs.
-# The .jsx extension keeps the bundle eligible for component auto-mount.
-# Compiled BPF objects in bin/ are loaded by path at runtime, never imported,
-# so they are not bundled.
-#
-# The build needs no npm/node: the starter imports only `yeet:*` builtins and
-# local `@/` modules, which esbuild resolves on its own. If you add third-party
-# packages to package.json, install them into node_modules with the package
-# manager of your choice — esbuild inlines whatever it finds there.
-ESBUILD_FLAGS := --bundle --format=esm --platform=neutral \
- --main-fields=module,main --conditions=import,module \
- --outfile=src/index.jsx --jsx=automatic --jsx-import-source=yeet:tui
-
-bundle: | toolchain
- $(ESBUILD) src/main.jsx $(ESBUILD_FLAGS) '--external:yeet:*' '--external:*.bpf.o'
-
-# Post-generation finalize: initialize a git repository with the vendored git
-# (fetched via `vendored-git`). Idempotent — skipped if this is already a repo.
-# The scaffolders (`yeet new`, `scripts/new`) run `make postgen` after creating
-# the project, so the CLI itself stays a thin caller of make.
-postgen: | vendored-git
- @g="$(GIT)"; [ -x "$$g" ] || g="$$(command -v git 2>/dev/null || true)"; \
- if [ -e .git ]; then \
- echo "postgen: already a git repository"; \
- elif [ -n "$$g" ]; then \
- echo "postgen: git init"; \
- "$$g" -c init.templateDir= init -q . || echo "warning: 'git init' failed" >&2; \
- else \
- echo "warning: no git available (vendored or host); skipping 'git init'" >&2; \
- fi
+.DEFAULT_GOAL := all
-clean: clean-bpf
- rm -rf node_modules dist src/index.jsx
+all bpf bundle veristat veristat-matrix postgen clangd clean:
+ @$(MAKE) --no-print-directory -f $(REAL) $@
-.PHONY: all bundle clean postgen
+.PHONY: all bpf bundle veristat veristat-matrix postgen clangd clean
diff --git a/template/README.md b/template/README.md
index ddc8b2c..024d6fb 100644
--- a/template/README.md
+++ b/template/README.md
@@ -1,7 +1,7 @@
# __NAME__
A yeet script: a reactive JSX TUI bundled with esbuild, an `@/` source
-alias, and a BPF program — all driven by one `make`.
+alias, and a BPF program — all driven by `yeet build`.
The starter is **cpusched**: a live scheduler dashboard. The top is a
cores × time heatmap of context-switch rate (one row per CPU, newest column
@@ -36,9 +36,11 @@ shares its `control`; each probe module attaches its own maps.
## Layout
```
-Makefile build frontend — orchestrates the two compilers
-build/bpf.mk clang + bpftool rules: src/bpf/*.bpf.c -> bin/probe.bpf.o
-build/gen-vmlinux.sh generates src/bpf/include/vmlinux.h from kernel BTF
+Makefile thin shim → .build/Makefile (keeps `make`/`yeet run` working)
+.build/ build back end — gitignored, materialized by `yeet build`:
+.build/Makefile orchestrates the two compilers
+.build/bpf.mk clang + bpftool rules: src/bpf/*.bpf.c -> bin/probe.bpf.o
+.build/gen-vmlinux.sh generates src/bpf/include/vmlinux.h from kernel BTF
package.json project manifest + optional npm/jsr deps
tsconfig.json `#/` -> project root, `@/` -> ./src path aliases
src/main.jsx entry — composition root: input + mount
@@ -51,7 +53,7 @@ src/bpf/cpusched.bpf.c sched_switch program + the runtime knob
src/bpf/runqlat.bpf.c wakeup→on-CPU latency histogram
bin/ the linked BPF object lands here
.github/workflows/kernel-matrix.yml CI: verify the object across kernels
-build/verify-kernel.sh the in-VM gate that workflow runs
+.build/verify-kernel.sh the in-VM gate that workflow runs
```
The JS is layered: `probes/` is the only BPF-aware code (it owns the object
@@ -62,16 +64,23 @@ the `@/` alias; `main.jsx` wires them together and owns input.
## Build & run
```sh
-make # compile BPF (clang + bpftool) + bundle JS (esbuild)
-yeet run . # runs the bundled src/index.jsx (needs root for BPF)
+yeet build # compile BPF (clang + bpftool) + bundle JS (esbuild)
+yeet run . # runs the bundled dist/index.jsx (needs root for BPF)
```
-`make` runs two independent compilers: **clang + bpftool** compile
+`yeet build` is the frontend; the low-level orchestration lives under `.build/`
+(gitignored and materialized on demand). Bare `make` still works — the root
+`Makefile` is a thin shim that forwards to `.build/Makefile` — which is also how
+`yeet run` auto-builds before running. The build prints a styled step list;
+pass `V=1` (`yeet build V=1` / `make V=1`) to see the raw compiler commands.
+
+It drives two independent compilers: **clang + bpftool** compile
`src/bpf/*.bpf.c` and link them into one loadable object `bin/probe.bpf.o`;
-**esbuild** bundles `src/main.jsx` into `src/index.jsx`, resolving the `@/`
+**esbuild** bundles `src/main.jsx` into `dist/index.jsx`, resolving the `@/`
alias (and inlining any npm/jsr deps you add) and leaving `yeet:*` builtins
external. esbuild is vendored by the toolchain, so the build needs no
-node/npm.
+node/npm. `yeet run .` prefers that built `dist/index.jsx` over the raw
+`src/main.jsx`, so once built the bundle is what runs.
The data layer loads the object at runtime:
@@ -99,7 +108,7 @@ kernel in its matrix it builds `bin/probe.bpf.o`, boots that kernel in a VM
([cilium's little-vm-helper](https://github.com/cilium/little-vm-helper), images
from `quay.io/lvh-images`), and runs the vendored static **veristat** against the
object — failing the job if the verifier rejects any program. The check is
-`build/verify-kernel.sh`.
+`.build/verify-kernel.sh`.
Edit `matrix.kernel` to the kernels you care about (tags at
[quay.io/lvh-images/kind](https://quay.io/repository/lvh-images/kind?tab=tags);
@@ -113,9 +122,9 @@ boots those kernel images with [`lvh`](https://github.com/cilium/little-vm-helpe
`make veristat-matrix KERNELS="6.6-main bpf-next-main"`. Both `lvh` and a static
`qemu` are fetched on demand (VM infra, not part of the build toolchain) — the
vendored static qemu comes from the toolchain release, checksum-pinned in
-`build/toolchain.lock`, falling back to a system `qemu-system` if absent.
+`.build/toolchain.lock`, falling back to a system `qemu-system` if absent.
-> Requires a toolchain pin (`build/toolchain.lock`) that ships `veristat`; the
+> Requires a toolchain pin (`.build/toolchain.lock`) that ships `veristat`; the
> workflow says so explicitly if the pinned version predates it.
`#/` (project root) and `@/` (source root) are **bundle-time aliases** that
diff --git a/template/src/bpf/runqlat.bpf.c b/template/src/bpf/runqlat.bpf.c
index 0745afd..21f692f 100644
--- a/template/src/bpf/runqlat.bpf.c
+++ b/template/src/bpf/runqlat.bpf.c
@@ -60,10 +60,12 @@ int on_switch_lat(struct trace_event_raw_sched_switch *ctx)
__u64 delta = bpf_ktime_get_ns() - *tsp;
bpf_map_delete_elem(&runq_start, &pid);
- // slot = floor(log2(delta_ns))
+ // slot = floor(log2(delta_ns)). A bounded loop (≤ MAX_SLOTS iterations):
+ // modern verifiers accept these directly, so it needs no `#pragma unroll`
+ // — which couldn't unroll it anyway (the data-dependent break makes the
+ // trip count non-constant) and only produced a warning.
__u32 slot = 0;
__u64 d = delta;
-#pragma unroll
for (int i = 0; i < MAX_SLOTS; i++) {
if (d <= 1) {
break;