Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions template/.github/workflows/kernel-matrix.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
7 changes: 4 additions & 3 deletions template/.gitignore
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
48 changes: 27 additions & 21 deletions template/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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");
Expand All @@ -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.

Expand All @@ -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
Expand Down Expand Up @@ -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" })
Expand Down Expand Up @@ -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?.<struct> ?? 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
Expand Down Expand Up @@ -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();
Expand All @@ -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) => {
Expand Down Expand Up @@ -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([]);
Expand Down Expand Up @@ -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");

Expand Down Expand Up @@ -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; };
Expand Down Expand Up @@ -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 … */
Expand All @@ -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();
Expand Down
48 changes: 37 additions & 11 deletions template/Makefile
Original file line number Diff line number Diff line change
@@ -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/<name>-<version>-<arch>.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
Expand All @@ -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.
#
Expand All @@ -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/<name>.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/<name>-<version>-<arch>.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
Expand All @@ -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
Loading
Loading