Skip to content

feat(buck2): distributed CI building + testing the whole tree on free runners#1862

Draft
gilescope wants to merge 32 commits into
mainfrom
giles-buck2-build
Draft

feat(buck2): distributed CI building + testing the whole tree on free runners#1862
gilescope wants to merge 32 commits into
mainfrom
giles-buck2-build

Conversation

@gilescope

Copy link
Copy Markdown
Contributor

Overview

Adds an experimental buck2 build of midnight-node (parallel to the existing
cargo and bazel builds) that culminates in a distributed CI workflow: instead
of paying for 16-core machines, one driver job owns the buck2 invocation and
N free ubuntu-latest worker jobs lend their cores over a
gilescope/rebuck2
iroh P2P mesh. Compile/link/test actions dispatch to whichever runner is free;
blobs move peer-to-peer; rendezvous is keyless via github.run_id.

Full design write-up: docs/buck2-notes.md.

Build system

  • reindeer-generated third-party graph + first-party BUCK (scripts/buck2/*).
  • Real on-chain WASM runtime blob, built end-to-end in buck2: a wasm32 no_std
    crate universe (wasm-deps/) + first-party wasm variants via select() on
    cpu:wasm32, cdylib → wasm-opt → zstd (tools/wasm-compress) →
    include_bytes! inject. 84 substrate exports; the node builds a dev chainspec
    from it.
  • buck2 test root//... --exclude ci-infraPass 1460 / Fail 0 locally.

Distributed CI (.github/workflows/buck2-distributed.yml)

  • rebuck2 driver + 3 workers, re-exec platform (platforms/defs.bzl).
  • Portable wasm toolchain: toolchains/BUCK reads [wasm] cc/cxx/ar from config
    (macOS Homebrew LLVM defaults; CI overrides with distro clang), wasm-lld.sh
    locates rust-lld via the sysroot. .github/actions/buck2-linux-toolchain
    installs rustc + wasm32v1-none, clang/lld, wasm-opt.
  • The 5 Docker/devnet/Cardano tests carry a ci-infra label and are excluded.

🗹 TODO before merging

  • Add DCO sign-off — commit is intentionally unsigned; needs git rebase --signoff.
  • First CI run validates the Linux/distributed assumptions (couldn't run locally).
  • Consider pinning buck2 (currently install-buck2 latest) and wiring store persistence.

📌 Submission Checklist

  • All commits are signed off (git commit -s) for the DCO
  • Changes are backward-compatible (additive: new build system + CI only; cargo/bazel untouched)
  • Pull request description explains why the change is needed
  • Self-reviewed the diff
  • I have included a change file, or skipped for this reason: alternative build system + CI only, does not affect the shipped node/toolkit/runtime products
  • If the changes introduce a new feature, I have bumped the node minor version: N/A (build tooling, no runtime change)
  • Update documentation (if relevant): docs/buck2-notes.md
  • Updated AGENTS.md if build commands, architecture, or workflows changed: N/A (buck2 is documented separately in docs/buck2-notes.md)
  • No new todos introduced

🧪 Testing Evidence

Locally (macOS arm64):

  • buck2 build root//node:midnight-node-bin → builds, links, --versionmidnight-node 2.0.0.
  • buck2 build root//:runtime-wasm → 1.1 MB blob, substrate sp-maybe-compressed-blob format (magic prefix + zstd), 84 exports.
  • buck2 test root//... --exclude ci-infraPass 1460, Fail 0.
  • Workflow: actionlint clean; rebuck2 action inputs verified against their action.yml.

Not yet run: the distributed CI itself (needs this push). The local Docker daemon
and the x86 test box were unavailable, so the Linux build is validated on the
first CI run; the toolchain action fail-fasts if a runner's clang lacks the wasm
backend.

  • Additional tests are provided (if possible)

🔱 Fork Strategy

  • N/A — build tooling + CI only; no runtime or client change.

Links

Distributed builder: gilescope/rebuck#2

… runners

Adds a buck2 build of midnight-node (parallel to cargo/bazel) that culminates
in a distributed CI workflow: no paid 16-core box — a driver job owns buck2 and
N ubuntu-latest worker jobs lend cores over a rebuck2 iroh P2P mesh.

Build system
- reindeer-generated third-party graph + first-party BUCK (scripts/buck2/*).
- Real on-chain WASM runtime blob built end-to-end: wasm32 no_std crate universe
  (wasm-deps/) + first-party wasm variants via select() on cpu:wasm32, cdylib ->
  wasm-opt -> zstd (tools/wasm-compress) -> include_bytes! inject. 84 substrate
  exports; node builds a dev chainspec from it.
- buck2 test root//... --exclude ci-infra: Pass 1460 / Fail 0 locally.

Distributed CI (.github/workflows/buck2-distributed.yml)
- rebuck2 driver + 3 workers, re-exec platform (platforms/defs.bzl), keyless
  rendezvous via github.run_id.
- Portable wasm toolchain: toolchains/BUCK reads [wasm] cc/cxx/ar from config
  (macOS Homebrew LLVM defaults; CI overrides with distro clang), wasm-lld.sh
  locates rust-lld via the sysroot. .github/actions/buck2-linux-toolchain
  installs rustc+wasm32v1-none, clang/lld, wasm-opt.
- The 5 Docker/devnet/Cardano tests carry a ci-infra label and are excluded.

See docs/buck2-notes.md for the full design.

Assisted-by: Claude:claude-opus-4.8
@gilescope gilescope added skip-changes-check-all ai-assisted AI (LLM) assisted contribution labels Jul 12, 2026
First CI run surfaced two Linux issues:
- re-exec (local_enabled=False) rejects reindeer's local_only git_fetch actions
  ("Trying to execute a local_only=True action on remote executor"). Switch to
  re-cache (hybrid): local_only runs local, the rest can go to the mesh.
- buck2 host auto-detection misfired on the x86_64 runner — third-party platform
  selects resolved to arm (blake3 neon.c on x86, stacker windows.c on Linux,
  if-addrs missing libc, windows-result raw-dylib on ELF). Pin the build with
  --target-platforms root//platforms:linux-x86_64 so os:linux+cpu:x86_64 selects
  the correct branches deterministically. Verified locally: blake3 → simd_x86.

Also: arch diagnostic step + soften the distribution check to a report.

Assisted-by: Claude:claude-opus-4.8
@datadog-official

This comment has been minimized.

gilescope added 23 commits July 12, 2026 19:17
Run #2 got past the platform + git_fetch fixes and deep into the compile, then
failed: `buck2 build root//...` also builds orphan platform-specific object
targets — ring's Windows `*-nasm.o` prebuilt_cxx_library targets, which reindeer
emits from the fixup's `cfg(target_os="windows")` block WITHOUT
`target_compatible_with`. So `root//...` doesn't skip them off-Windows and
ld.lld fails "unknown file type" linking Windows NASM objects on Linux.

These orphans are depended on by nothing on Linux (ring's lib correctly selects
the elf asm). Build the actual products instead — node + toolkit binaries + the
wasm blob — which pull the whole first-party/third-party closure through their
real, platform-correct deps. `buck2 test` then builds+runs every test target and
its deps (also not the orphans). This is the "builds and runs the tests" scope.

Follow-up: teach reindeer to emit target_compatible_with for os-cfg-gated cxx
targets so `root//...` is fully buildable.

Assisted-by: Claude:claude-opus-4.8
…op 2.3GB snapshot

Run #3 was a FALSE green. `buck2 … 2>&1 | tee X.log` returns tee's exit (0),
so an 8-target build failure in the test step still passed the job. Real result
was "Tests finished: Pass 1381. Fail 0. Build failure 8".

- set -o pipefail on the build+test steps so buck2's non-zero exit fails the job.
- The 8 failures were rustup, not code: rustc in the repo honours
  rust-toolchain.toml's pinned `components`, and parallel rustc actions race to
  rustup-install clippy on-the-fly -> "detected conflict: bin/cargo-clippy".
  Fix: pre-install those components in the composite (single-threaded) AND pin
  RUSTUP_TOOLCHAIN so rustc uses the installed 1.95 directly, never re-syncing.
- snapshot-key-prefix: "" — the monolith store snapshot was 2.3 GB per run
  (blows the actions-cache budget). Cold builds until sharded-CAS persistence.

Assisted-by: Claude:claude-opus-4.8
…ns it)

Run #4 (now honestly failing): rustc still raced a rustup install —
'rust-std-aarch64-unknown-linux-gnu' conflict. rust-toolchain.toml pins
targets = [wasm32v1-none, aarch64-unknown-linux-gnu]; the composite only
pre-installed wasm32v1-none, so rustc auto-installed the aarch64 std under
parallel actions and conflicted. RUSTUP_TOOLCHAIN didn't help — buck2 sanitizes
the action env, so rustc still reads rust-toolchain.toml. Pre-install every
pinned target so there's nothing left to sync at build time.

The secp256k1-sys wasm build-script cc failure had empty clang stderr and is
likely collateral from the racing rustup installs (rollback corrupts the shared
toolchain mid-build); re-evaluating with a clean toolchain.

Assisted-by: Claude:claude-opus-4.8
Run #5 (clean toolchain, real errors now):
- openssl-0.10 "not found in `ffi`": openssl-sys's build script probes the
  system openssl to set version cfgs; without libssl-dev + pkg-config it sets
  the wrong ones and openssl's ffi bindings don't resolve. Install them.
- secp256k1-sys 0.10.1 wasm.c fails on ubuntu clang-18: its vendored
  wasm-sysroot predates clang making implicit function declarations a hard
  error. Local fixup adds CFLAGS_wasm32v1_none=-Wno-error=implicit-function-
  declaration to the build-script env (symbols resolve at link). AGENTS.md
  documents this exact case. Verified: blob still builds on macOS.

Assisted-by: Claude:claude-opus-4.8
Run #6: the -Wno-error CFLAG applied but secp256k1's wasm.c still failed (buck
discards the build-script's clang stderr, so it looked mysterious). The real
cause: the cc-rs command carries wasm codegen flags (-mcpu=mvp -mmutable-globals)
but NO --target — cc-rs doesn't map the `wasm32v1-none` rust target name to a
clang triple, so clang treats -mcpu=mvp as an x86 CPU and dies. Add
--target=wasm32-unknown-unknown to the build-script CFLAGS (what secp256k1-sys
0.9 does itself, hence it built). Verified: blob still builds on macOS.

Assisted-by: Claude:claude-opus-4.8
Adopt the fixes the sibling bazel build already uses for these -sys crates:
- openssl (native-tls, Linux-only — macOS uses Security.framework): local
  openssl-sys fixup sets OPENSSL_LIB_DIR/OPENSSL_INCLUDE_DIR to the system
  paths so the build script probes the right version and emits consistent
  ossl3xx cfgs (fixes "not found in `ffi`"). Mirrors MODULE.bazel.
- secp256k1-sys wasm.c: toolchains/wasm-cc.sh wrapper forces
  --target=wasm32-unknown-unknown (cc-rs doesn't map wasm32v1-none), resolving
  a wasm-capable clang; wasm-ar.sh for llvm-ar. Mirrors bazel wasm32_cc.sh.
  CI points [wasm] cc/cxx/ar at these (absolute $GITHUB_WORKSPACE path — the cc
  shim cd's, so relative wouldn't resolve); macOS keeps Homebrew clang direct.
- cmake + perl in the toolchain action for aws-lc-sys (builds AWS-LC from src).
- Fix the indexmap local fixup (version-gate buildscript.run so third-party's
  1.x keeps has_std while 2.x/wasm stays no_std). Re-buckified third-party.

Verified: node binary still builds on macOS.

Assisted-by: Claude:claude-opus-4.8
Run #8: openssl cleared (the OPENSSL_LIB_DIR fix worked). But secp256k1's wasm.c
still failed — the [wasm] cc wrapper was at $GITHUB_WORKSPACE (the driver's path),
and rebuck2 dispatched the compile to a worker where the buck-baked absolute path
need not resolve. Install wasm-cc.sh/wasm-ar.sh to /usr/local/bin in the composite
(runs on every runner) and point [wasm] cc/cxx/ar there — a uniform path.

(aws-lc-sys still fails separately: its build-script C feature-test link doubles
the CRT — duplicate _start/_init under ld.lld — investigating.)

Assisted-by: Claude:claude-opus-4.8
secp256k1's wasm.c compile exits 1 with empty stderr — buck's buildscript_run
swallows the compiler diagnostic, making it impossible to see WHY clang fails
(the command itself looks correct: --target=wasm32-unknown-unknown, -mcpu=mvp,
-I wasm/wasm-sysroot). Capture clang's stderr in the wrapper and re-emit it on
failure so the real error reaches the driver log. Temporary diagnostic.

Assisted-by: Claude:claude-opus-4.8
buck's buildscript_run swallows the compiler stderr (even the wrapper's
re-emitted copy), so 10 runs couldn't see WHY clang fails secp256k1's wasm.c.
Add a standalone step: fetch the crate, run the exact compile via wasm-cc.sh,
surface clang's real diagnostic. continue-on-error so it never gates the job.
Temporary.

Assisted-by: Claude:claude-opus-4.8
The crates.io API needs a User-Agent; static.crates.io serves the .crate
tarball directly. This lets the diagnostic step actually run clang on
secp256k1's wasm.c and surface the real error.

Assisted-by: Claude:claude-opus-4.8
Subset flags compiled OK — so it's one of the omitted flags (the ENABLE_MODULE_*
incl. ellswift, -Dprintf(...)=, -Wall -Wextra). Use the exact buck flag set to
surface the real error.

Assisted-by: Claude:claude-opus-4.8
The compile works standalone but fails under buck's cc_shim, which passes
--ld-path=<ld_shim> to the compiler. For a -c compile that's an unused arg;
cc-rs's -Werror=unused-command-line-argument turns it fatal. Test A (with
--ld-path) should fail, B (+ -Qunused-arguments) should pass -> the fix.

Assisted-by: Claude:claude-opus-4.8
buck's cc shim passes --ld-path=<ld_shim> and -fno-sanitize=all to the C
compiler on every invocation, including the -c compiles cc-rs runs for
secp256k1-sys's vendored wasm.c, which never link. clang flags --ld-path as
an unused argument; cc-rs builds with -Werror=unused-command-line-argument,
so it became a fatal error -- and buck's buildscript_run wrapper swallowed the
stderr, leaving an empty diagnostic. Add -Qunused-arguments to wasm-cc.sh so
clang tolerates the (genuinely unused, on a compile) link flag.

Confirmed in CI: identical flags fail with --ld-path, pass with
-Qunused-arguments. Removes the DEBUG repro step now the cause is fixed.

Assisted-by: Claude:claude-opus-4.8
aws-lc-sys: force the CMAKE builder on Linux (AWS_LC_SYS_CMAKE_BUILDER=1) with
plain CC/CXX. The default cc_builder compiles+links compiler-feature probe
executables, and buck's cc shim routes links through --ld-path=<ld_shim> which
re-drives with clang++, doubling crt1.o -> 'ld.lld: duplicate symbol _start'.
cmake builds a static lib and, with plain clang/ld, avoids the double. Gated to
Linux; macOS's ld64 tolerates the double so cc_builder still works there.

secp256k1 wasm: -Qunused-arguments didn't clear it and cc-rs eats the compiler
stderr. Add a WASM_CC_LOG sink to wasm-cc.sh + a forced-local debug build so the
real clang diagnostic survives on the driver. TEMP debug, removed once diagnosed.

Assisted-by: Claude:claude-opus-4.8
aws-lc cmake path panicked in the `cmake` crate: 'environment variable
PROFILE not defined'. The crate reads Cargo's PROFILE/DEBUG for CMAKE_BUILD_TYPE;
reindeer's cargo_env omits them. Set PROFILE=release, DEBUG=false (Linux env).

wasm-cc.sh: the secp256k1 wasm compile succeeds under local execution but fails
under remote (worker) execution, where the action env may not carry a PATH with
/usr/bin. Resolve clang via absolute /usr/bin/clang before falling back to a
bare PATH lookup.

Assisted-by: Claude:claude-opus-4.8
Bump the external_cell_fixups / shared_fixups pin to buck2-fixups@de3d929,
which sets local_only=True on cargo build-script run actions. Non-hermetic
build scripts (openssl-sys version probe, secp256k1-sys wasm C compile) run on
a worker under distributed RE with a divergent env and fail/misdetect; pinning
them to the driver fixes secp256k1 (wasm compile) and openssl (ffi version
cfgs). Hermetic rustc/cxx compiles still distribute. Fixups dir is unchanged
between the old and new pin, so third-party/BUCK is unaffected.

Assisted-by: Claude:claude-opus-4.8
Build scripts probe the host and shell out to cc/cmake reading undeclared
absolute system paths; dispatched to a remote worker their env diverges and
they misbehave (openssl-sys version probe misdetects -> openssl references absent
ffi symbols; secp256k1-sys wasm C compile fails). All succeed run locally.

Pre-build the build-script-run closure with --prefer-local (host third-party
under linux-x86_64, wasm-deps under wasm32, each matching the main build's
config so it cache-hits), then the main distributed build reuses those outputs
while the hermetic rustc/cxx compiles still distribute. Achieves the effect of
local_only without touching the bundled prelude.

Reverts the inert external_cell_fixups pin bump (bundled prelude isn't overlaid
by that mechanism) and drops the WASM_CC_LOG debug sink.

Assisted-by: Claude:claude-opus-4.8
The build-script-run rule has no default output, so 'buck2 build <target>'
built nothing (log: 'does not have any outputs') -- the pre-build was a no-op
and the main build ran the scripts remote for the first time. Build the
[out_dir] sub-target to actually execute each build script, and use --local-only
(hard force) instead of --prefer-local (a hint the hybrid executor ignored when
workers were idle). set -f stops the shell globbing the [out_dir] suffix.

Assisted-by: Claude:claude-opus-4.8
Building all 58 build scripts --local-only surfaced protoc-needing ones
(sc-network, litep2p) that build fine distributed but panic locally without
protoc. Restrict the local pre-build to the two crates whose build scripts are
non-hermetic in a way RE breaks: openssl(-sys) host version probe and
secp256k1-sys vendored wasm C. Also the decisive test of whether the main
distributed build reuses the pre-built outputs.

Assisted-by: Claude:claude-opus-4.8
The wasm compile fails under buck on Linux (driver + worker), not only remote;
cc-rs eats clang's stderr. The --local-only pre-build reproduces it on the
driver's persistent fs, so have wasm-cc.sh append clang's diagnostic to
/tmp/wasm-cc-fail.log and dump it in an always() step. TEMP debug.

Assisted-by: Claude:claude-opus-4.8
/tmp is sandboxed by rebuck2's local executor so the diagnostic never reached
the host. Write it into the action CWD (under buck-out, the real materialization
root) and grep buck-out for it in the dump step. TEMP debug.

Assisted-by: Claude:claude-opus-4.8
The secp256k1 wasm compile error can't be surfaced in CI (cc-rs captures the
compiler stderr and rebuck2's executor tears down the action fs before a dump
step can read any side file). Drop the local pre-build + diagnostic scaffolding
and keep the straightforward distributed build; the real fixes (-Qunused-args
and absolute clang in wasm-cc.sh, aws-lc cmake+PROFILE) stay. Remaining
blockers are documented for a local-Linux repro.

Assisted-by: Claude:claude-opus-4.8
Verified end-to-end in an ubuntu:24.04 container (clang 18.1.3, matching CI).
Five root causes, all previously hidden because cc-rs swallows compiler stderr:

- secp256k1 wasm: the wasm cc-shim exec'd the macOS Homebrew clang path (absent
  on Linux -> FileNotFoundError). The [wasm] cc override in .buckconfig.local is
  root-cell-scoped, but toolchains/BUCK reads it from the toolchains cell. Pass
  it as a global -c wasm.cc/cxx/ar (BUCK_WASM_CFG) which reaches every cell.
- openssl: reindeer bakes openssl-sys's DEP_OPENSSL_* at buckify time from the
  host (macOS 3.5) -> openssl-0.10 gated 3.3+ symbols the ubuntu 3.0.13 ffi
  lacks. Pin DEP_OPENSSL_VERSION_NUMBER=30000000 + restate DEP_OPENSSL_CONF
  (fixup env replaces, not merges) via third-party/fixups/openssl.
- ittapi-sys: a shared fixup set buildscript.run=false, so jitprofiling.c
  (iJIT_* symbols) never compiled and the node link failed undefined. Local
  fixup re-enables it (wasmtime profiling pulls ittapi on Linux).
- protoc: composite installs protobuf-compiler (sc-network*/litep2p prost-build).
- binaryen: composite installs pinned v130 (ubuntu apt v108 lacks
  --signext-lowering, which the runtime-wasm blob genrule needs).

macOS build unaffected (verified): ittapi/openssl paths are Linux-only.

Assisted-by: Claude:claude-opus-4.8
The build is correct (full node+toolkit+wasm blob verified green in an
ubuntu:24.04 container), but the distributed CI driver got OOM-reclaimed mid-
build (exit 143, shutdown signal) at ~83% done: it runs the buck2 daemon + a
co-worker + all local_only actions (build scripts, metadata gen, final links)
on a 2-core/7GB free runner. 10G swap absorbs the peak.

Assisted-by: Claude:claude-opus-4.8
fallocate on /swapfile failed 'Text file busy' — GitHub runners ship an active
4G /swapfile. Add ours on /mnt (the large ephemeral disk) instead.

Assisted-by: Claude:claude-opus-4.8
…tion)

The driver kept dying mid-build with exit 143 ("runner received a shutdown
signal"), not a build error, and swap didn't help — it's disk, not memory. The
full build's buck-out is tens of GB (59G seen locally) and the rebuck2 CAS store
adds more; both default onto the free runner's ~14G / and overflow it, so the VM
is reclaimed. Symlink buck-out to /mnt (~70G) and set the driver store input to
/mnt/rebuck2. Keep a 6G swap for local rustc peaks.

Assisted-by: Claude:claude-opus-4.8
…he fix)

The runner already has an active /mnt/swapfile, so fallocate failed 'Text file
busy' and skipped the whole step incl. the buck-out redirect. Swap isn't the
fix anyway (disk is), so make it best-effort on a fresh path and let the
buck-out + store relocation to /mnt always run.

Assisted-by: Claude:claude-opus-4.8
The symlinked buck-out let the build run out of the disk trap (4739 commands,
no more exit 143) but buck2's materializer resolved the symlink inconsistently
under rebuck2 — sources landed where local rustc couldn't read them ('couldn't
read src/lib.rs'). A bind mount is transparent: buck2 sees a real buck-out dir
backed by /mnt's 70G.

Assisted-by: Claude:claude-opus-4.8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-assisted AI (LLM) assisted contribution skip-changes-check-all

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant