From 583e92045b768646b3de20b17a4f80a4b7ee2542 Mon Sep 17 00:00:00 2001 From: jrg Date: Wed, 28 Oct 2026 21:43:42 -0500 Subject: [PATCH] template: dist/ is the whole output; #/ is the one import root; pkg/--.tar.gz The linked BPF object now lands at dist/bin/probe.bpf.o beside the bundle, and bin/ at the project root is gone. A copied dist/ (index.js + bin/) runs on its own, and a final `tarball` step packs it into pkg/--.tar.gz (name and version from package.json, arch from the toolchain), listed with the artifacts; pkg/ is gitignored and removed by `make clean`, and holds one package per version and arch. `#/` replaces `@/` as the one import root and means src/: tsconfig maps it there for esbuild, package.json `imports` maps it there for the runtime, so every module also runs standalone with `yeet run` (the old `@/` was bundle-time only). The one exception is the `#/bin/` prefix, the built-object dir, mapped to dist/bin/ in the tree. Nothing needs the bundle to run: `make bpf` then `yeet run .` runs the sources directly. The object is imported, not located: `import probe from "#/bin/probe.bpf.o"` evaluates to a ready BpfObject (the loader's BpfObjectRule). In the bundle it stays external and one esbuild alias rewrites the `#/bin` prefix to ./bin, so it points beside the bundle and any further object is just another import. This replaces the old `exe: "../bin/probe.bpf.o", base: import.meta.dirname` path, which only resolved from the bundle's location. Also: the kernel matrix stages veristat in .kmatrix/ instead of bin/, locally and in CI; `clean` prints one row per path; the step macro treats an empty exit status as failure; README and AGENTS.md follow. --- template/.github/workflows/kernel-matrix.yml | 6 +-- template/.gitignore | 7 +-- template/AGENTS.md | 48 +++++++++-------- template/Makefile | 48 +++++++++++++---- template/README.md | 54 +++++++++++--------- template/build/bpf.mk | 29 ++++++----- template/build/kernel-matrix.sh | 2 +- template/build/ui.mk | 10 ++-- template/build/verify-kernel.sh | 4 +- template/package.json | 4 ++ template/src/components/detail.jsx | 2 +- template/src/components/heatmap.jsx | 2 +- template/src/components/histogram.jsx | 2 +- template/src/components/procs.jsx | 2 +- template/src/components/titlebar.jsx | 2 +- template/src/lib/format.js | 2 +- template/src/main.jsx | 24 ++++----- template/src/probes/cpusched.js | 2 +- template/src/probes/probe.js | 12 +++-- template/src/probes/runqlat.js | 2 +- template/tsconfig.json | 3 +- 21 files changed, 158 insertions(+), 109 deletions(-) diff --git a/template/.github/workflows/kernel-matrix.yml b/template/.github/workflows/kernel-matrix.yml index 0ac1c37..86f70ff 100644 --- a/template/.github/workflows/kernel-matrix.yml +++ b/template/.github/workflows/kernel-matrix.yml @@ -1,7 +1,7 @@ name: kernel-matrix # Build the BPF object once per job, then boot a range of kernels and confirm -# each one's verifier accepts every program in bin/probe.bpf.o. The check is +# each one's verifier accepts every program in dist/bin/probe.bpf.o. The check is # the vendored static `veristat` (it loads each program and reports a verdict); # kernels come from cilium's little-vm-helper (quay.io/lvh-images), booted under # QEMU/KVM on the runner. Each job writes a detail table to its step summary and @@ -66,7 +66,7 @@ jobs: - name: Build BPF object + stage veristat run: | set -euo pipefail - # Builds bin/probe.bpf.o with the vendored static toolchain, also + # Builds dist/bin/probe.bpf.o with the vendored static toolchain, also # populating the per-machine toolchain cache (clang/bpftool/veristat). make bpf @@ -82,7 +82,7 @@ jobs: exit 1 fi install -Dm755 "$cache/veristat" .kmatrix/veristat - file .kmatrix/veristat bin/probe.bpf.o + file .kmatrix/veristat dist/bin/probe.bpf.o - name: Verify on kernel ${{ matrix.kernel }} uses: cilium/little-vm-helper@v0.0.30 diff --git a/template/.gitignore b/template/.gitignore index faba519..1cc0117 100644 --- a/template/.gitignore +++ b/template/.gitignore @@ -1,10 +1,11 @@ -# build artifacts (the esbuild bundle lands in dist/index.js) +# build artifacts: dist/ holds the esbuild bundle (index.js) and the linked +# BPF object (bin/probe.bpf.o); .build/ the per-unit objects /node_modules/ /dist/ /.build/ +/pkg/ -# compiled BPF objects (bin/ exists only once built) + generated CO-RE header -/bin/ +# generated CO-RE header /src/bpf/include/vmlinux.h # kernel-matrix run output (build/kernel-matrix.sh) diff --git a/template/AGENTS.md b/template/AGENTS.md index 6864c26..941cb7b 100644 --- a/template/AGENTS.md +++ b/template/AGENTS.md @@ -3,7 +3,7 @@ This is a **yeet** script: a reactive JSX TUI that runs in the daemon's V8 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. +layout, and the `#/` import root, see `README.md` — don't duplicate that here. ## Mental model @@ -47,10 +47,11 @@ run until something watches it — there's no UI here): ```js // probes/conns.js -import { BpfObject, RingBuf } from "yeet:bpf"; +import { RingBuf } from "yeet:bpf"; import { from } from "yeet:tui"; +import probe from "#/bin/probe.bpf.o"; // a BpfObject for the built object -const ctl = await new BpfObject({ exe: "../bin/probe.bpf.o", base: import.meta.dirname }) +const ctl = await probe .bind("events", { kind: "ringbuf", btf_struct: "conn_event" }) .start(); const events = new RingBuf(ctl, "events"); @@ -75,9 +76,9 @@ if (import.meta.main) { } ``` -Run it directly — `yeet run src/probes/conns.js`. **Caveat:** `@/` and `#/` are -bundle-time aliases, so a standalone module must reach its siblings by relative -path (`./probe.js`), or be bundled as its own entry. Switching `JSON.stringify` +Run it directly — `yeet run src/probes/conns.js`. `#/` imports work here +too: the runtime resolves them through `imports` in package.json, the same +way the bundle does at build time. Switching `JSON.stringify` to flag BigInt up front saves you the "why does math give NaN" detour — wrap 64-bit values with `Number(...)` once you've seen them. @@ -89,7 +90,7 @@ hand-fed signal before any real data exists. Mount just the one: ```jsx // scratch entry while developing components/gauge.jsx import { mount, signal } from "yeet:tui"; -import Gauge from "@/components/gauge.jsx"; +import Gauge from "#/components/gauge.jsx"; const fake = signal(0.3); setInterval(() => fake.set(Math.random()), 700); // exercise the reactive path @@ -252,9 +253,10 @@ restarted. **BPF** — bind maps on the shared object, then read them (`yeet:bpf`): ```js -import { BpfObject, RingBuf, ArrayMap, HashMap, DataSec } from "yeet:bpf"; +import { RingBuf, ArrayMap, HashMap, DataSec } from "yeet:bpf"; +import probe from "#/bin/probe.bpf.o"; // a BpfObject: the loader builds it from the file -const ctl = await new BpfObject({ exe: "../bin/probe.bpf.o", base: import.meta.dirname }) +const ctl = await probe .bind("events", { kind: "ringbuf", btf_struct: "sched_event" }) .bind("probe.data", { kind: "data" }) // .data/.rodata/.bss section .bind("runq_hist", { kind: "array" }) @@ -329,8 +331,11 @@ writes atomically. `yeet.exit()` tears the script down. combinators (use `face(patch)` when the style is computed at runtime). 6. **Ring-buffer events are wrapped** under the `btf_struct` name — unwrap with `w?. ?? w`. -7. **`@/` and `#/` are bundle-time only** — the runtime resolver doesn't know - them, which is why the BPF object is located with `import.meta.dirname`. +7. **`#/` is the only import root: `src/`** — resolved by tsconfig at bundle + time and by package.json `imports` at runtime, so every module also runs + standalone. `#/bin/` is the one exception, the built-object dir, mapped to + `dist/bin/`; write `import probe from "#/bin/probe.bpf.o"`, and the bundle + rewrites the prefix to `./bin/` beside itself. 8. **`console.log` goes to the daemon log, not the screen** (and strips ANSI) — render in-pane via the JSX tree or `tty.write`. 9. **No Node builtins** (`fs`, `net`, …) — only packages that run in bare V8 @@ -421,8 +426,8 @@ export const cpuPct = computed(() => Math.round(cpu.get() * 100)); ```jsx // main.jsx import { Box, Text, mount } from "yeet:tui"; -import { cpu } from "@/probes/sysload.js"; -import Gauge from "@/components/gauge.jsx"; +import { cpu } from "#/probes/sysload.js"; +import Gauge from "#/components/gauge.jsx"; tty.on("keydown", (e) => { if (e.code === "Escape" || (e.key ?? "").toLowerCase() === "q") yeet.exit(); @@ -449,14 +454,15 @@ renders. ```js // probes/conns.js -import { BpfObject, RingBuf } from "yeet:bpf"; +import { RingBuf } from "yeet:bpf"; import { from } from "yeet:tui"; +import probe from "#/bin/probe.bpf.o"; const MAX = 50; export const conns = from((state) => { const rows = []; - const ctl = new BpfObject({ exe: "../bin/probe.bpf.o", base: import.meta.dirname }) + const ctl = probe .bind("events", { kind: "ringbuf", btf_struct: "conn_event" }); const sub = ctl.start().then((c) => new RingBuf(c, "events").subscribe((w) => { @@ -496,7 +502,7 @@ a signal it reads changes, so it also re-targets cleanly. // components/detail.jsx — subscribe only while this panel is visible import { Box, Effect, Text, signal } from "yeet:tui"; import { RingBuf } from "yeet:bpf"; -import { control } from "@/probes/probe.js"; +import { control } from "#/probes/probe.js"; export default function Detail({ pid }) { const lines = signal([]); @@ -533,7 +539,7 @@ patching a global in its `.data` section. 64-bit fields want a `BigInt`. // probes/knob.js import { DataSec } from "yeet:bpf"; import { signal } from "yeet:tui"; -import { control } from "@/probes/probe.js"; +import { control } from "#/probes/probe.js"; const knobs = new DataSec(control, "probe.data"); @@ -562,7 +568,7 @@ transparently; iteration order is unstable under churn, so collect-then-act. // probes/syscount.js — map keyed by comm[16], value is a __u64 counter import { HashMap } from "yeet:bpf"; import { from } from "yeet:tui"; -import { control } from "@/probes/probe.js"; +import { control } from "#/probes/probe.js"; const counts = new HashMap(control, "counts"); const comm = (u8) => { let s = ""; for (const b of u8) { if (!b) break; s += String.fromCharCode(b); } return s; }; @@ -630,7 +636,7 @@ export const status = signal("starting…"); export const start = async () => { try { - const ctl = await new BpfObject({ exe: "../bin/probe.bpf.o", base: import.meta.dirname }) + const ctl = await probe // import probe from "#/bin/probe.bpf.o" .bind("events", { kind: "ringbuf", btf_struct: "conn_event" }) .start(); /* … wire maps … */ @@ -648,8 +654,8 @@ keeps that lifecycle clean: ```jsx // main.jsx import { mount } from "yeet:tui"; -import App from "@/components/app.jsx"; -import Bsod from "@/components/bsod.jsx"; +import App from "#/components/app.jsx"; +import Bsod from "#/components/bsod.jsx"; tty.on("keydown", (e) => { if (e.code === "Escape" || (e.key ?? "").toLowerCase() === "q") yeet.exit(); diff --git a/template/Makefile b/template/Makefile index 78c3e1b..e9a7615 100644 --- a/template/Makefile +++ b/template/Makefile @@ -1,9 +1,10 @@ # Build the yeet script project. # # make — build everything (BPF objects + JS bundle) -# make bpf — compile bpf/*.bpf.c into bin/* only +# make bpf — compile bpf/*.bpf.c into dist/bin/probe.bpf.o only # make veristat — load the built object with veristat (verifier check on this kernel) # make bundle — bundle the JS entry with esbuild +# make tarball — pack dist/ into pkg/--.tar.gz, the shippable artifact # 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 @@ -12,7 +13,7 @@ # 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` +# dist/ 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. # @@ -29,33 +30,57 @@ include build/bpf.mk BUILD_START := $(shell date +%s%N 2>/dev/null) -all: bpf bundle - $(call done,$(notdir $(CURDIR))) +# The project name and version, from package.json (the scaffold sets the +# name); the directory name and 0.0.0 as fallbacks. +NAME := $(or $(shell sed -n 's/^ "name": "\(.*\)",$$/\1/p' package.json),$(notdir $(CURDIR))) +VERSION := $(or $(shell sed -n 's/^ "version": "\(.*\)",$$/\1/p' package.json),0.0.0) +# The BPF object is compiled for one target arch, so the package is named by +# it too (UNAME_M from build/toolchain.mk: x86_64 or aarch64). +TARBALL := pkg/$(NAME)-$(VERSION)-$(UNAME_M).tar.gz + +all: bpf bundle tarball + $(call done,$(NAME)) $(call artifact,$(BPF_OUT)) $(call artifact,dist/index.js) + $(call artifact,$(TARBALL)) # Bundle the entry with the vendored esbuild. esbuild honors tsconfig `paths` -# (so `@/` resolves at bundle time), while `yeet:*` builtins and `*.bpf.o` +# (so `#/` resolves at bundle time), while `yeet:*` builtins and `*.bpf.o` # objects stay external. The bundle is written to dist/index.js: the entry # ladder tries dist/ before src/, so once built, that is what runs — and the # generated file stays out of the source tree (dist/ is gitignored). Plain # .js is fine: JSX is compiled away by esbuild, and src/main.jsx mounts its # root component explicitly, so the bundle needs no auto-mount. -# Compiled BPF objects in bin/ are loaded by path at runtime, never imported, -# so they are not bundled. +# Compiled BPF objects are imported as "#/bin/.bpf.o" (see +# src/probes/probe.js) and stay external: the alias rewrites the "#/bin" +# prefix to ./bin, relative to the bundle, so every object import points +# beside the bundle and a copied dist/ (index.js + bin/) runs on its own +# with no manifest. (`\#`: a bare # would start a make comment.) # # 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 +# 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 \ --define:import.meta.main=false \ - --outfile=dist/index.js --jsx=automatic --jsx-import-source=yeet:tui + --outfile=dist/index.js --jsx=automatic --jsx-import-source=yeet:tui \ + '--alias:\#/bin=./bin' bundle: | toolchain $(call step,bundle,dist/index.js,$(ESBUILD) src/main.jsx $(ESBUILD_FLAGS) --log-level=warning '--external:yeet:*' '--external:*.bpf.o') +# The shippable artifact: all of dist/ (index.js + bin/) as +# pkg/--.tar.gz. Entries are relative (./index.js, +# ./bin/probe.bpf.o), so it unpacks into any directory you choose, and +# `yeet run .` there runs it. pkg/ holds one package per version and arch +# built; bump "version" in package.json to cut a new one. +tarball: bpf bundle | pkg + $(call step,tarball,$(TARBALL),tar -czf $(TARBALL) -C dist .) + +pkg: + $(Q)mkdir -p pkg + # 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 @@ -74,6 +99,7 @@ postgen: | vendored-git clean: clean-bpf $(call status,clean,node_modules) $(call status,clean,dist) - $(Q)rm -rf node_modules dist + $(call status,clean,pkg) + $(Q)rm -rf node_modules dist pkg -.PHONY: all bundle clean postgen +.PHONY: all bundle tarball clean postgen diff --git a/template/README.md b/template/README.md index b71275f..9a6ff7b 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`. +A yeet script: a reactive JSX TUI bundled with esbuild, a `#/` import root, +and a BPF program — all driven by one `make`. 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 @@ -27,7 +27,7 @@ complete tour of the layout and of yeet's reactive BPF. ## Two BPF programs, one object `src/bpf/*.bpf.c` are independent units that `bpftool gen object` links into -one `bin/probe.bpf.o`: `cpusched.bpf.c` (the sched_switch stream + the knob) +one `dist/bin/probe.bpf.o`: `cpusched.bpf.c` (the sched_switch stream + the knob) and `runqlat.bpf.c` (wakeup→on-CPU latency). Add another `.bpf.c` and it's linked in automatically — keep `char LICENSE[]` in exactly one unit, and give each program a unique name. `probes/probe.js` loads the object once and @@ -37,10 +37,10 @@ shares its `control`; each probe module attaches its own maps. ``` Makefile build frontend — orchestrates the two compilers -build/bpf.mk clang + bpftool rules: src/bpf/*.bpf.c -> bin/probe.bpf.o +build/bpf.mk clang + bpftool rules: src/bpf/*.bpf.c -> dist/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 +tsconfig.json `#/` -> ./src path alias (the bundle-time half of `#/`) src/main.jsx entry — composition root: input + mount src/probes/probe.js loads the shared BPF object (binds maps, start()) src/probes/cpusched.js sched_switch stream → signals + the DataSec knob @@ -49,8 +49,9 @@ src/components/*.jsx pure UI: titlebar, heatmap, detail, histogram, footer src/lib/format.js pure render helpers (rate, duration, heat color) 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 (created by the build) -dist/ the esbuild JS bundle lands here (gitignored) +dist/ build output: index.js + bin/probe.bpf.o — runs on its own (gitignored) +pkg/ dist/ packed for shipping as --.tar.gz + (from package.json); unpack anywhere, `yeet run .` (gitignored) .github/workflows/kernel-matrix.yml CI: verify the object across kernels build/verify-kernel.sh the in-VM gate that workflow runs ``` @@ -58,46 +59,48 @@ 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 and exposes plain signals), `components/` is pure presentation that reads those signals, and `lib/` is pure helpers. They reference each other through -the `@/` alias; `main.jsx` wires them together and owns input. +the `#/` root; `main.jsx` wires them together and owns input. ## Build & run ```sh -make # compile BPF (clang + bpftool) + bundle JS (esbuild) +make # compile BPF + bundle JS + pack pkg/--.tar.gz make V=1 # same, showing every command behind the status lines yeet run . # runs the bundled dist/index.js (needs root for BPF) ``` `make` runs 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 `dist/index.js`, resolving the `@/` -alias (and inlining any npm/jsr deps you add) and leaving `yeet:*` builtins +`src/bpf/*.bpf.c` and link them into one loadable object `dist/bin/probe.bpf.o`; +**esbuild** bundles `src/main.jsx` into `dist/index.js`, resolving `#/` +imports (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. The data layer loads the object at runtime: ```js -const probe = new BpfObject({ exe: "../bin/probe.bpf.o", base: import.meta.dirname }); +import probe from "#/bin/probe.bpf.o"; // a BpfObject for that file const control = await probe .bind("events", { kind: "ringbuf", btf_struct: "sched_event" }) .bind("probe.data", { kind: "data" }) // the .data section that holds the knob .start(); // the tracepoint auto-attaches ``` -`base: import.meta.dirname` resolves the path against the running bundle. -`probe.data` is libbpf's name for this object's `.data` section (confirm with -`bpftool btf dump file bin/probe.bpf.o` if you rename things). +Importing a `.bpf.o` yields a `BpfObject` for it. `#/bin/` is the built-object +dir: `dist/bin/` in the tree, `./bin/` beside the bundle — see *the `#/` root* +below. `probe.data` is libbpf's +name for this object's `.data` section (confirm with +`bpftool btf dump file dist/bin/probe.bpf.o` if you rename things). ## Testing across kernels -Run `make veristat` to load `bin/probe.bpf.o` with veristat on **your** kernel — +Run `make veristat` to load `dist/bin/probe.bpf.o` with veristat on **your** kernel — a quick check that every program passes the verifier, plus per-program complexity (insns/states). It needs privileges to load programs, so use `sudo`. A BPF program that loads on your laptop can be rejected by an older kernel's verifier. `.github/workflows/kernel-matrix.yml` guards against that: for each -kernel in its matrix it builds `bin/probe.bpf.o`, boots that kernel in a VM +kernel in its matrix it builds `dist/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 @@ -120,14 +123,19 @@ vendored static qemu comes from the toolchain release, checksum-pinned in > 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 -esbuild resolves via tsconfig `paths`; the runtime resolver doesn't know them, -which is why the BPF object is located with `import.meta.dirname`. +`#/` is the one import root: `src/`. esbuild maps it there via tsconfig +`paths` at bundle time, and the runtime maps it there via the `imports` field +of `package.json` (Node-style subpath imports), so `#/lib/format.js` works +both bundled and standalone (`yeet run src/probes/probe.js`). `#/bin/` is the +one exception, the built-object dir, mapped to `dist/bin/`. The bundle doesn't +use `#/` at all: sources are inlined, and the external `#/bin/…` object imports +are rewritten by an esbuild alias to `./bin/…` — so a copied `dist/` runs by +itself with no manifest, and another object is just another import. ## npm / jsr packages The starter needs no third-party packages — it imports only `yeet:*` builtins -and local `@/` modules, so `make` builds with no npm/node and no `node_modules`. +and local `#/` modules, so `make` builds with no npm/node and no `node_modules`. To pull in a dependency, add it to `package.json` and install it into `node_modules` with whatever package manager you like (`npm`, `pnpm`, `bun`, …) @@ -137,7 +145,7 @@ run in bare V8 work: no Node builtins (`fs`, `net`, …), and no `Intl` / ## Pure-JS scripts -Don't need BPF? Delete `src/bpf/`, `bin/`, and `src/probes/cpusched.js`, then +Don't need BPF? Delete `src/bpf/`, `src/probes/probe.js`, and `src/probes/cpusched.js`, then feed the components from any source that exposes the same signals. ## Prerequisites diff --git a/template/build/bpf.mk b/template/build/bpf.mk index 7e8a2f5..466ef51 100644 --- a/template/build/bpf.mk +++ b/template/build/bpf.mk @@ -2,13 +2,13 @@ # # The BPF program is modular: each src/bpf/*.bpf.c is a unit, compiled on # its own, and all units are statically linked into ONE loadable object, -# bin/probe.bpf.o, with `bpftool gen object` — the same linker libbpf uses +# dist/bin/probe.bpf.o, with `bpftool gen object` — the same linker libbpf uses # internally. Split the program across as many .bpf.c files as you like; # share structs, maps and helpers through headers in src/bpf/include/ and # the linker merges the duplicates. vmlinux.h (CO-RE) is generated there. # # 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. +# dist/bin/probe.bpf.o below — there is intentionally no per-object magic here. # CLANG/BPFTOOL are normally resolved by build/toolchain.mk (the yeet CLI's # toolchain via `yeet toolchain exec`, or the lock-pinned shared cache), @@ -28,12 +28,14 @@ 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/. +# never mistaken for the loadable object in dist/. 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 -# BpfObjectRule matches on that suffix). -BPF_OUT := bin/probe.bpf.o +# The single linked object, written under dist/ so dist/ is the whole +# deployable. Its `.bpf.o` suffix is what the loader's BpfObjectRule matches +# on: src/probes/probe.js does `import probe from "#/bin/probe.bpf.o"`. +# `#/bin/` is the built-object dir — package.json maps it to dist/bin/ in the +# tree, and esbuild rewrites it to ./bin/ in the bundle (see the Makefile). +BPF_OUT := dist/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 @@ -56,20 +58,19 @@ $(VMLINUX): | toolchain $(call step,$(basename $(basename $(notdir $<))),$@,$(CLANG) $(BPF_CFLAGS) -c $< -o $@) # Statically link every unit into the single loadable object. -$(BPF_OUT): $(BPF_OBJS) | bin toolchain +$(BPF_OUT): $(BPF_OBJS) | dist/bin toolchain @command -v $(firstword $(BPFTOOL)) >/dev/null 2>&1 || { $(call fail,bpftool not found — install bpftool / linux-tools); } $(call step,link,$@,$(BPFTOOL) gen object $@ $(BPF_OBJS)) -bin: - $(Q)mkdir -p bin +dist/bin: + $(Q)mkdir -p dist/bin -# bin/ is a build product: it holds the linked object (and whatever the kernel -# matrix stages beside it), is not tracked, and the `bin` rule recreates it. +# Only the BPF side of dist/: the JS bundle beside it belongs to `make clean`. clean-bpf: - $(call status,clean,bin) + $(call status,clean,$(BPF_OUT)) $(call status,clean,.build) $(call status,clean,$(VMLINUX)) - $(Q)rm -rf bin .build $(VMLINUX) + $(Q)rm -rf $(BPF_OUT) .build $(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 diff --git a/template/build/kernel-matrix.sh b/template/build/kernel-matrix.sh index 5dcd5af..102f855 100755 --- a/template/build/kernel-matrix.sh +++ b/template/build/kernel-matrix.sh @@ -22,7 +22,7 @@ KERNELS=${*:-"5.10-main 5.15-main 6.1-main 6.6-main 6.12-main bpf-next-main"} LVH_VERSION="${LVH_VERSION:-v0.0.30}" SSH_PORT="${SSH_PORT:-2222}" MON_PORT="${MON_PORT:-45454}" -OBJ="${OBJ:-bin/probe.bpf.o}" +OBJ="${OBJ:-dist/bin/probe.bpf.o}" case "$(uname -s)" in Linux) ;; diff --git a/template/build/ui.mk b/template/build/ui.mk index 2992568..f3d96bc 100644 --- a/template/build/ui.mk +++ b/template/build/ui.mk @@ -5,12 +5,14 @@ # ✔ vmlinux.h src/bpf/include/vmlinux.h # ✔ cpusched .build/bpf/cpusched.bpf.o # ✔ runqlat .build/bpf/runqlat.bpf.o -# ✔ link bin/probe.bpf.o +# ✔ link dist/bin/probe.bpf.o # ✔ bundle dist/index.js +# ✔ tarball pkg/myproject-0.1.0-x86_64.tar.gz # ✔ done myproject (0.66s) # -# ==> bin/probe.bpf.o (8.1kb) +# ==> dist/bin/probe.bpf.o (8.1kb) # ==> dist/index.js (15.5kb) +# ==> pkg/myproject-0.1.0-x86_64.tar.gz (9.2kb) # # Rows go to stderr, so the build's stdout stays clean for piping. While a # step runs, its row shows a spinner, then settles into the check. The @@ -99,8 +101,8 @@ $(if $(filter 1,$(V)),$(info $(3)))@if [ -n "$(SPIN)" ]; then \ done; \ wait $$pid; rc=$$?; printf '\r\033[2K' >&2; cat "$$out" >&2; rm -f "$$out"; \ else ( $(3) ); rc=$$?; fi; \ -if [ $$rc -eq 0 ]; then $(call row,$(STEP_FMT),$(1),$(2)); \ -else $(call row,$(FAIL_FMT),$(1),$(2),failed); exit $$rc; fi +if [ "$${rc:-1}" -eq 0 ]; then $(call row,$(STEP_FMT),$(1),$(2)); \ +else $(call row,$(FAIL_FMT),$(1),$(2),failed); exit "$${rc:-1}"; fi endef # $(call done,artifact) — the summary row, timed from BUILD_START (set by diff --git a/template/build/verify-kernel.sh b/template/build/verify-kernel.sh index 7927ddc..db79740 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 .kmatrix/ before booting. # -# sh build/verify-kernel.sh [bpf-object] (default: bin/probe.bpf.o) +# sh build/verify-kernel.sh [bpf-object] (default: dist/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 @@ -18,7 +18,7 @@ set -eu -OBJ="${1:-bin/probe.bpf.o}" +OBJ="${1:-dist/bin/probe.bpf.o}" VERISTAT="${VERISTAT:-./.kmatrix/veristat}" # verdict LAST so the gate below can match it at end-of-line. veristat's CSV # header uses each stat's canonical name, so the columns come out as diff --git a/template/package.json b/template/package.json index d53d19c..bdcd585 100644 --- a/template/package.json +++ b/template/package.json @@ -3,6 +3,10 @@ "version": "0.1.0", "private": true, "type": "module", + "imports": { + "#/bin/*": "./dist/bin/*", + "#/*": "./src/*" + }, "description": "__NAME__ — a yeet script.", "dependencies": {} } diff --git a/template/src/components/detail.jsx b/template/src/components/detail.jsx index aac49b2..77dbb8a 100644 --- a/template/src/components/detail.jsx +++ b/template/src/components/detail.jsx @@ -1,7 +1,7 @@ // The selected CPU's recent context switches, streamed live: prev -> next // with the outgoing task's on-CPU slice. Reads `cpus` + `selected`. import { Box, Text, idx } from "yeet:tui"; -import { fmtDuration, lpad, pad } from "@/lib/format.js"; +import { fmtDuration, lpad, pad } from "#/lib/format.js"; export default ({ cpus, selected, rows }) => ( diff --git a/template/src/components/heatmap.jsx b/template/src/components/heatmap.jsx index b7a95db..845ae82 100644 --- a/template/src/components/heatmap.jsx +++ b/template/src/components/heatmap.jsx @@ -5,7 +5,7 @@ // layout: it renders the most recent `gridCols` of history and, when there // are more cores than rows, windows the list to keep the selection in view. import { Box, Text, idx } from "yeet:tui"; -import { heat } from "@/lib/format.js"; +import { heat } from "#/lib/format.js"; const HEADER = " cores × time (newest → right) brightness = switch rate"; diff --git a/template/src/components/histogram.jsx b/template/src/components/histogram.jsx index b54bbd2..e620fe4 100644 --- a/template/src/components/histogram.jsx +++ b/template/src/components/histogram.jsx @@ -2,7 +2,7 @@ // (cumulative counts per bucket) and draws a bar per bucket, the label // being the bucket's lower bound. import { Box, Text, idx } from "yeet:tui"; -import { fmtDuration, lpad } from "@/lib/format.js"; +import { fmtDuration, lpad } from "#/lib/format.js"; const LO = 8; // 2^8 ns = 256ns const HI = 23; // 2^23 ns ≈ 8.4ms diff --git a/template/src/components/procs.jsx b/template/src/components/procs.jsx index 26e1a60..118bc63 100644 --- a/template/src/components/procs.jsx +++ b/template/src/components/procs.jsx @@ -6,7 +6,7 @@ // time. Only switches the kernel emitted are counted, so sub-`min-slice` tasks // don't show — it tracks CPU hogs, not every wakeup. import { Box, Text, idx } from "yeet:tui"; -import { heat, lpad, pad } from "@/lib/format.js"; +import { heat, lpad, pad } from "#/lib/format.js"; const NAMEW = 15; // command column width const PCTW = 7; // "100.0%" + a space diff --git a/template/src/components/titlebar.jsx b/template/src/components/titlebar.jsx index bc72f74..68af807 100644 --- a/template/src/components/titlebar.jsx +++ b/template/src/components/titlebar.jsx @@ -3,7 +3,7 @@ // tinted as the rail via the container's own bg — full width, no fragile // space-fill. import { Box, Text, idx } from "yeet:tui"; -import { fmtDuration, fmtRate } from "@/lib/format.js"; +import { fmtDuration, fmtRate } from "#/lib/format.js"; const RAIL = idx(235); const sep = () => {" ▏ "}; diff --git a/template/src/lib/format.js b/template/src/lib/format.js index a2bbc5c..d71f3d4 100644 --- a/template/src/lib/format.js +++ b/template/src/lib/format.js @@ -1,5 +1,5 @@ // Pure presentation helpers — strings and color, no signals or BPF. -// Imported by the components through the `@/` alias (resolved at bundle time). +// Imported by the components through the `#/` root (resolved at bundle time). import { idx } from "yeet:tui"; export const pad = (s, n) => (s + " ".repeat(n)).slice(0, n); diff --git a/template/src/main.jsx b/template/src/main.jsx index edfe702..6ec51ef 100644 --- a/template/src/main.jsx +++ b/template/src/main.jsx @@ -12,21 +12,21 @@ * re-filters which switches it emits — live. * * Layout: probes/ (BPF-aware) → components/ (pure UI) → lib/ (pure helpers), - * imported through the `@/` source alias and composed here. + * imported through the `#/` root and composed here. * - * To ship a pure-JS script, delete src/bpf/, bin/, and probes/cpusched.js. + * To ship a pure-JS script, delete src/bpf/, probes/probe.js, and probes/cpusched.js. */ import { Box, Text, idx, mount, signal } from "yeet:tui"; -import { numCpus } from "@/probes/probe.js"; -import { cpus, minSlice, procs, setMinSlice } from "@/probes/cpusched.js"; -import { latency } from "@/probes/runqlat.js"; -import { layoutFor } from "@/lib/layout.js"; -import TitleBar from "@/components/titlebar.jsx"; -import Heatmap from "@/components/heatmap.jsx"; -import Procs from "@/components/procs.jsx"; -import Detail from "@/components/detail.jsx"; -import Histogram from "@/components/histogram.jsx"; -import Footer from "@/components/footer.jsx"; +import { numCpus } from "#/probes/probe.js"; +import { cpus, minSlice, procs, setMinSlice } from "#/probes/cpusched.js"; +import { latency } from "#/probes/runqlat.js"; +import { layoutFor } from "#/lib/layout.js"; +import TitleBar from "#/components/titlebar.jsx"; +import Heatmap from "#/components/heatmap.jsx"; +import Procs from "#/components/procs.jsx"; +import Detail from "#/components/detail.jsx"; +import Histogram from "#/components/histogram.jsx"; +import Footer from "#/components/footer.jsx"; const SLICE_STEP = 100; // µs per +/- press const HEAT_TOP = 2; // screen row of the first CPU row (title + heatmap header) diff --git a/template/src/probes/cpusched.js b/template/src/probes/cpusched.js index dcb252c..a5679ea 100644 --- a/template/src/probes/cpusched.js +++ b/template/src/probes/cpusched.js @@ -12,7 +12,7 @@ import { DataSec, RingBuf } from "yeet:bpf"; import { from, signal } from "yeet:tui"; import { _ } from "yeet:helpers"; -import { control, numCpus } from "@/probes/probe.js"; +import { control, numCpus } from "#/probes/probe.js"; const WINDOW_MS = 500; // rate window const FEED = 64; // recent switches kept per CPU diff --git a/template/src/probes/probe.js b/template/src/probes/probe.js index 8fad94b..c28b5dd 100644 --- a/template/src/probes/probe.js +++ b/template/src/probes/probe.js @@ -1,11 +1,13 @@ -// Shared BPF object. Both .bpf.c units are linked into one bin/probe.bpf.o +// Shared BPF object. Both .bpf.c units are linked into one dist/bin/probe.bpf.o // and loaded once here; the per-feature probe modules (cpusched.js, // runqlat.js) import this `control` and attach their own maps to it. All // binds must happen before the single start(), so they live together here. -import { BpfObject } from "yeet:bpf"; - -// `base: import.meta.dirname` resolves against the running bundle. -const probe = new BpfObject({ exe: "../bin/probe.bpf.o", base: import.meta.dirname }); +// Importing a `.bpf.o` yields a BpfObject for that file (the loader's +// BpfObjectRule). `#/` is the src/ root, except `#/bin/`, the built-object +// dir: package.json maps it to dist/bin/, so this module also runs +// standalone, and in the bundle esbuild rewrites the prefix to ./bin/, so a +// copied dist/ runs on its own. +import probe from "#/bin/probe.bpf.o"; export const control = await probe .bind("events", { kind: "ringbuf", btf_struct: "sched_event" }) // cpusched stream diff --git a/template/src/probes/runqlat.js b/template/src/probes/runqlat.js index 371d880..3bb57b9 100644 --- a/template/src/probes/runqlat.js +++ b/template/src/probes/runqlat.js @@ -4,7 +4,7 @@ // in contrast to cpusched's ring-buffer stream. import { ArrayMap } from "yeet:bpf"; import { from } from "yeet:tui"; -import { control } from "@/probes/probe.js"; +import { control } from "#/probes/probe.js"; export const SLOTS = 27; // must match MAX_SLOTS in runqlat.bpf.c const POLL_MS = 500; diff --git a/template/tsconfig.json b/template/tsconfig.json index 6782b63..accb1c7 100644 --- a/template/tsconfig.json +++ b/template/tsconfig.json @@ -7,8 +7,7 @@ "jsxImportSource": "yeet:tui", "baseUrl": ".", "paths": { - "#/*": ["./*"], - "@/*": ["./src/*"] + "#/*": ["./src/*"] }, "types": [], "strict": true,