3 Build and Run
Chad Russell edited this page 2026-09-22 11:59:08 -04:00

Build and Run

Build & iterate

There is no host Rust/C toolchain (deliberate — no rustup, and the old dev toolbox is retired, 2026-09). Everything builds in a rust:bookworm container via rootless podman — glibc 2.36 baseline, so the binaries run on any newer host (Fedora atomic included):

cd ~/Code/bubblebox
podman run --rm -v "$PWD":/src -v "$HOME/.cache/bubblebox/portable-cargo":/cargo \
  -w /src -e CARGO_HOME=/cargo -e CARGO_TARGET_DIR=/src/target-bookworm \
  docker.io/library/rust:bookworm cargo build --release --workspace
# unit tests: the same invocation with `cargo test --workspace`
  • scripts/build-portable.sh wraps exactly this (plus staging + tarball for distribution). It passes --userns=keep-id, which works from real login shells; from non-login contexts (agent sandboxes, some CI runners — crun fails with readlink '') drop the flag: rootless podman's default mapping already maps container-root to the invoking user, so artifacts come out correctly owned either way.
  • Binaries land side-by-side in target-bookworm/release/{bubblebox, bubblebox-fuse}; the engine resolves a FUSE server next to the binary, so dev runs need no BUBBLEBOX_FUSE_BIN.
  • fuse-mount builds in the same container — the composefs-fuse crate is pure Rust; the old "no composefs C headers in the toolbox" constraint is gone. (default-members = ["cli"] still keeps a bare cargo build CLI-only.)
  • Warm rebuilds are fast (~25 s); the cargo registry cache persists under ~/.cache/bubblebox/portable-cargo.
  • cargo test --release does not relink release/bubblebox — run an explicit cargo build --release when the binary itself is the deliverable.
  • After engine changes the bee nightly CI should exercise, refresh its pair: cp target-bookworm/release/bubblebox{,-fuse} ~/.local/share/bubblebox-ci/bin/.
  • Sanity-check the host with bubblebox doctor before blaming the code.

The release binary is what ~/.local/bin wrappers and cjust invoke. Wrappers exec the path, so rebuilds are picked up automatically — no re-export unless the path changes. Portable distribution tarballs come from scripts/build-portable.sh (consumed by tools/nightly.sh on the CI runner as ENGINE_TAR).

bubblebox init rebuilds the FUSE server via podman run rust:bookworm cargo build --release (no host toolchain) — the one bootstrap step for consumer installs. Run it after changing fuse-mount/ there.

Global verbosity (the chatter from log.rs's info!/warn! macros): by default a normal run is silent on info — it prints only the command's result line, warn!s (misconfig/footgun — always shown), and fatal errors. To see progress/informational output (network chatter, remount notices, gc's "roots:…" context and orphan path list), pass -v/--verbose or set BUBBLEBOX_VERBOSE=1. -q/--quiet forces info off and wins over -v/BUBBLEBOX_VERBOSE (kept as an explicit "be quiet" knob). Verbosity never affects a tool's own output under run (bwrap passthrough). Note gc dropped its own --verbose subcommand flag in favor of this global one.

Running

BB=~/Code/bubblebox/target/release/bubblebox
$BB init                          # one-time: build the FUSE server
$BB build rg                     # build a package -> descriptor (Containerfile resolved from the registered source)
$BB import docker.io/library/alpine:3.20 --binary /bin/sh   # turn any OCI image into a package (name/binary derived if omitted)
$BB mount rg                      # mount its composefs rootfs (rootless FUSE)
$BB run rg -- --version           # run under bwrap (auto-mounts if needed)
$BB export rg                     # write ~/.local/bin/rg wrapper
$BB shell --base rust-dev         # enter a reusable base-shell devshell
$BB shell                         # enter the project devshell (./bubblebox.toml)

run resolves an entrypoint (TOML) or, if no TOML exists, synthesizes a default { package = <name> }. Multi-package entrypoints (compose = [...]) compose at runtime via overlay-in-userns (see run.rs::run_composed / compose_inner).

Sources & caches (specs vs binaries):

$BB source add personal ~/Code/bubblebox-pkgs                  # local source (specs)
$BB source add community git+https://git.example.com/chan         # git source (specs)
$BB source update community                                        # git pull --ff-only
$BB source list                                                    # registered sources
$BB source packages                                               # what the sources offer (metadata cards)
$BB source search ripgrep                                         # name+description match, both --json
$BB cache add mycache https://cache.example.com                    # binary cache
$BB cache list                                                     # registered + source channel caches
$BB install rg                    # pull + deps + default entrypoint + export
$BB install https://cache.example.com/rg --no-export  # full-URL form (self-contained)
$BB install mycache:rg            # registered-nickname form
$BB config cat rg                 # show effective (default ⊕ override) entrypoint
$BB config edit rg                # seed an override from the default, open $EDITOR
$BB list                          # current generation's packages (what's applied)
$BB info rg                       # per-pkg state: digest, origin, mount, footprint
$BB push rg --entrypoint ~/Code/bubblebox-pkgs/rg/entrypoint.toml   # publish the default entrypoint

A source is a directory of <pkg>/Containerfile + <pkg>/entrypoint.toml (the default entrypoint, co-located with the build; a "virtual entrypoint" that reuses another package's image ships an entrypoint.toml with no Containerfile) (+ optional channel.toml declaring substituent caches). It's either a local path (/path, ~/..., file:///path) or a git clone (git+https://...); source add infers which from the location. build resolves Containerfiles from registered sources; run/config resolve default entrypoints from them. A cache serves binaries (blobs/descriptor/index/default-entrypoint) over HTTPS/S3. A source's channel.toml may declare caches that participate in pull/install without being registered separately. install fetches the package's default entrypoint into remote-defaults/; provenance (origin = the cache/source) is recorded by the declarative layer — apply writes it into the generation manifest, which is the single "what I have" record (installed/ is RETIRED; imperative build/install packages are GC-rooted by their pkg bindings until they enter a generation). The user's override deep-merges over the default at run time (rules in Override layering & deep-merge below). Editing an override can't affect binary caching (the entrypoint is runtime metadata, merged in memory; the store is content-addressed by the OCI image).

Env vars (all optional; defaults shown):

  • BUBBLEBOX_DIR (/Code/bubblebox), BUBBLEBOX_DATA_DIR (/.local/share/bubblebox), BUBBLEBOX_RUN_DIR ($XDG_RUNTIME_DIR/bubblebox), BUBBLEBOX_CONFIG_DIR (~/.config/bubblebox — user overrides + sources.toml + caches.toml), BUBBLEBOX_FUSE_BIN.
  • BUBBLEBOX_CACHE_URL is honored as a single default cache only when no sources.toml AND no caches.toml exist (legacy back-compat).
  • Common::load exports the resolved BUBBLEBOX_* vars back to the environment so entrypoint TOML $VAR expansion and devshell init scripts see them (the old bash got this free by sourcing bubblebox-common).