Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
8 changes: 6 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 13 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 \
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ make new DEST=<dir> [NAME=<name>] # or: scripts/new <dir> [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
Expand Down
23 changes: 20 additions & 3 deletions scripts/new
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
104 changes: 104 additions & 0 deletions template/.build/Makefile
Original file line number Diff line number Diff line change
@@ -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
30 changes: 18 additions & 12 deletions template/build/bpf.mk → template/.build/bpf.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 (<bpf/bpf_helpers.h>, …) 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))

Expand All @@ -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),
Expand All @@ -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
Expand All @@ -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:' \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# toolchain repo (releases/download/v<TOOLCHAIN_VERSION>/) and is
# checksum-verified against the pins in the lock.
#
# build/fetch-toolchain.sh <dest-dir> <uname-arch> <lock> [tool...]
# .build/fetch-toolchain.sh <dest-dir> <uname-arch> <lock> [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
Expand Down Expand Up @@ -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)}"
10 changes: 8 additions & 2 deletions template/build/gen-vmlinux.sh → template/.build/gen-vmlinux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <bpftool> <output-path>
# .build/gen-vmlinux.sh <bpftool> <output-path>

set -eu

Expand All @@ -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"
Loading
Loading