diff --git a/Makefile b/Makefile index ee47bc8e..d02cdd32 100644 --- a/Makefile +++ b/Makefile @@ -293,6 +293,15 @@ test-vod: dev-setup: $(MAKE) -j16 app-cached dev-setup-meson +# Fast alternative to dev-setup for fresh worktrees: seed the build dir from an +# already-built checkout (defaults to the main checkout this worktree came +# from) instead of doing a cold meson/GStreamer build. Only reads the donor. +# SEED_FROM=/path/to/built/checkout to pick a different donor. +SEED_FROM?= +.PHONY: dev-setup-fast +dev-setup-fast: + bash hack/seed-build-dir.sh $(SEED_FROM) + .PHONY: dev dev: app-cached $(LEXICON_STAMP) if [ ! -d $(BUILDDIR) ]; then $(MAKE) dev-setup; fi diff --git a/hack/seed-build-dir.sh b/hack/seed-build-dir.sh new file mode 100755 index 00000000..f05803d6 --- /dev/null +++ b/hack/seed-build-dir.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash +# seed-build-dir.sh — make a fresh worktree Go-test-ready in seconds instead of +# a ~1h cold `make dev-setup`. +# +# The dev (shared-library) build installs everything into the build dir itself +# (meson prefix == build dir). The cgo deps that `go test` links against — +# GStreamer, FFmpeg, glib, x264, libiroh_streamplace — are a pure function of +# the meson inputs (meson.build, subprojects/*.wrap, rust/, Cargo.lock), NOT of +# the branch's Go code. So a new worktree can copy the installed tree from an +# already-built checkout, rewrite the absolute prefix paths in the .pc files, +# and immediately run `go build` / `go test` against it. +# +# Usage: +# hack/seed-build-dir.sh [DONOR_CHECKOUT] [--force] +# +# DONOR_CHECKOUT defaults to the main checkout this worktree was created from +# (derived from `git rev-parse --git-common-dir`). The donor is only ever read. +# +# After seeding, run tests with hack/test-env.sh, e.g.: +# hack/test-env.sh go test -run TestStreamTranscoder -v ./pkg/media +# +# What this does NOT cover: +# - Changes to the C/rust dependency stack itself (meson.build, subprojects, +# rust/, Cargo.lock). The manifest check below catches those and tells you to +# run a real `make dev-setup`. +# - Changes to the meson option blocks in the Makefile (BASE_OPTS/SHARED_OPTS). +# Those are rare and not covered by the manifest; if you're editing them, +# you're doing a real build anyway. +# - The real frontend. js/app/dist gets a placeholder index.html so that +# packages embedding js/app compile; run `make app` if you need the actual UI. +set -euo pipefail + +FORCE=0 +DONOR_ROOT="" +for arg in "$@"; do + case "$arg" in + --force) FORCE=1 ;; + -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//' | head -30; exit 0 ;; + *) DONOR_ROOT="$arg" ;; + esac +done + +WT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +BUILDOS="${BUILDOS:-$(uname -s | tr '[:upper:]' '[:lower:]')}" +BUILDARCH="${BUILDARCH:-$(uname -m | tr '[:upper:]' '[:lower:]')}" +case "$BUILDARCH" in + aarch64) BUILDARCH=arm64 ;; + x86_64) BUILDARCH=amd64 ;; +esac +BUILDDIR="${BUILDDIR:-build-$BUILDOS-$BUILDARCH}" + +if [ -z "$DONOR_ROOT" ]; then + COMMON_DIR="$(cd "$WT_ROOT" && git rev-parse --path-format=absolute --git-common-dir)" + DONOR_ROOT="$(dirname "$COMMON_DIR")" +fi +DONOR_ROOT="$(cd "$DONOR_ROOT" && pwd)" + +if [ "$DONOR_ROOT" = "$WT_ROOT" ]; then + echo "error: donor checkout resolves to this checkout ($WT_ROOT)." >&2 + echo "Pass the path to an already-built checkout explicitly:" >&2 + echo " hack/seed-build-dir.sh /path/to/built/streamplace" >&2 + exit 1 +fi + +DONOR_BUILD="$DONOR_ROOT/$BUILDDIR" +DEST_BUILD="$WT_ROOT/$BUILDDIR" + +if [ ! -f "$DONOR_BUILD/lib/pkgconfig/streamplacedeps.pc" ]; then + echo "error: $DONOR_BUILD doesn't look like a completed dev build" >&2 + echo "(missing lib/pkgconfig/streamplacedeps.pc). Run 'make dev-setup' in the" >&2 + echo "donor checkout first, or pass a different donor." >&2 + exit 1 +fi + +if [ -e "$DEST_BUILD" ]; then + if [ "$FORCE" = 1 ]; then + echo "--force: removing existing $DEST_BUILD" + rm -rf "$DEST_BUILD" + else + echo "error: $DEST_BUILD already exists; rm -rf it or pass --force." >&2 + exit 1 + fi +fi + +# Hash the inputs that determine the native dependency build. If these differ +# between donor and worktree, the donor's libs may not match what this branch +# expects and you need a real build. +# Only tracked files count: builds promote extra gitignored .wrap files into +# subprojects/, which must not make two otherwise-identical checkouts mismatch. +manifest() { + local root="$1" out + out="$( + cd "$root" + git ls-files -z -- meson.build 'subprojects/*.wrap' Cargo.toml Cargo.lock rust | + LC_ALL=C sort -z | xargs -0 -r sha256sum + )" + if [ -z "$out" ]; then + echo "error: git ls-files found no native build inputs in $root" >&2 + echo "(not a streamplace checkout, or git can't read it here?)" >&2 + return 1 + fi + printf '%s' "$out" | sha256sum | cut -d' ' -f1 +} + +DONOR_MANIFEST="$(manifest "$DONOR_ROOT")" +WT_MANIFEST="$(manifest "$WT_ROOT")" +if [ "$DONOR_MANIFEST" != "$WT_MANIFEST" ]; then + echo "error: native build inputs differ between this checkout and the donor:" >&2 + echo " donor ($DONOR_ROOT): $DONOR_MANIFEST" >&2 + echo " worktree ($WT_ROOT): $WT_MANIFEST" >&2 + echo "meson.build / subprojects/*.wrap / rust/ / Cargo.lock changed, so the" >&2 + echo "donor's libraries may not match this branch. Run 'make dev-setup' for a" >&2 + echo "real build, or re-run with --force if you know the change is irrelevant." >&2 + if [ "$FORCE" != 1 ]; then + exit 1 + fi + echo "--force: continuing anyway." >&2 +fi + +echo "Seeding $DEST_BUILD from $DONOR_BUILD ..." +mkdir -p "$DEST_BUILD" +# --reflink=auto: instant CoW copy on filesystems that support it, plain copy +# otherwise. Deliberately NOT hardlinks: a later `meson install` in the donor +# overwrites files in place, which would corrupt hardlinked copies. +for d in lib include libexec bin; do + if [ -e "$DONOR_BUILD/$d" ]; then + cp -a --reflink=auto "$DONOR_BUILD/$d" "$DEST_BUILD/$d" + fi +done + +# The .pc files carry the donor's absolute prefix; retarget them here. +grep -rlF --include='*.pc' "$DONOR_ROOT" "$DEST_BUILD" | while read -r pc; do + sed -i "s|$DONOR_ROOT|$WT_ROOT|g" "$pc" +done + +# Placeholder frontend + lexicon stamp so `go build` (go:embed js/app/dist) and +# `make dev` work without a pnpm build. Run `make app` for the real UI. +mkdir -p "$WT_ROOT/js/app/dist" "$WT_ROOT/.build" +[ -f "$WT_ROOT/js/app/dist/index.html" ] || touch "$WT_ROOT/js/app/dist/index.html" +touch "$WT_ROOT/.build/lexicon-stamp" + +# Canary: the donor's libs must be loadable in THIS environment. A build dir +# made on the host doesn't load inside the builder container (newer glibc), and +# vice versa can't be assumed either — catch that now, not at `go test` time. +if [ -x "$DEST_BUILD/bin/gio" ]; then + if ! LD_LIBRARY_PATH="$DEST_BUILD/lib" "$DEST_BUILD/bin/gio" version >/dev/null 2>&1; then + echo "error: the seeded libraries don't load in this environment:" >&2 + LD_LIBRARY_PATH="$DEST_BUILD/lib" "$DEST_BUILD/bin/gio" version 2>&1 | head -3 >&2 || true + echo "The donor ($DONOR_BUILD) was probably built against a different libc" >&2 + echo "(e.g. built on the host, but you're seeding inside the builder" >&2 + echo "container). Pick a donor checkout whose build ran in the same" >&2 + echo "environment you'll run tests in." >&2 + rm -rf "$DEST_BUILD" + exit 1 + fi +fi + +{ + echo "seeded-from: $DONOR_BUILD" + echo "date: $(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "manifest: $WT_MANIFEST" +} > "$DEST_BUILD/.seeded-from" + +echo "Done. Run Go tests with:" +echo " hack/test-env.sh go test -run -v ./pkg/" diff --git a/hack/test-env.sh b/hack/test-env.sh new file mode 100755 index 00000000..7dc0024a --- /dev/null +++ b/hack/test-env.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# test-env.sh — run a command with the env cgo Go tests need against this +# checkout's dev (shared) build in build--/, e.g.: +# +# hack/test-env.sh go test -run TestStreamTranscoder -v ./pkg/media +# +# With no arguments, prints export lines for eval: +# +# eval "$(hack/test-env.sh)" +# +# Works with a build dir produced by `make dev-setup` or hack/seed-build-dir.sh. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +BUILDOS="${BUILDOS:-$(uname -s | tr '[:upper:]' '[:lower:]')}" +BUILDARCH="${BUILDARCH:-$(uname -m | tr '[:upper:]' '[:lower:]')}" +case "$BUILDARCH" in + aarch64) BUILDARCH=arm64 ;; + x86_64) BUILDARCH=amd64 ;; +esac +BUILDDIR="${BUILDDIR:-build-$BUILDOS-$BUILDARCH}" +B="$ROOT/$BUILDDIR" + +if [ ! -f "$B/lib/pkgconfig/streamplacedeps.pc" ]; then + echo "error: $B has no dev build; run 'make dev-setup' or 'hack/seed-build-dir.sh'." >&2 + exit 1 +fi + +PKG_CONFIG_PATH="$B/lib/pkgconfig:$B/lib/gstreamer-1.0/pkgconfig:$B/meson-uninstalled" +LD_LIBRARY_PATH="$B/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +CGO_LDFLAGS="${CGO_LDFLAGS:--lm}" +# A seeded build dir contains libs whose compiled-in plugin path points at the +# donor checkout; point GStreamer at this checkout's copies explicitly. +GST_PLUGIN_PATH="$B/lib/gstreamer-1.0" + +if [ "$#" -eq 0 ]; then + echo "export PKG_CONFIG_PATH=\"$PKG_CONFIG_PATH\"" + echo "export LD_LIBRARY_PATH=\"$LD_LIBRARY_PATH\"" + echo "export CGO_LDFLAGS=\"$CGO_LDFLAGS\"" + echo "export GST_PLUGIN_PATH=\"$GST_PLUGIN_PATH\"" + exit 0 +fi + +export PKG_CONFIG_PATH LD_LIBRARY_PATH CGO_LDFLAGS GST_PLUGIN_PATH +exec "$@"