Containerize local builds with rootless nerdctl

This commit is contained in:
ms2k 2026-08-18 01:36:48 +09:00
commit 30fc02d79f
6 changed files with 216 additions and 31 deletions

45
README.md Normal file
View file

@ -0,0 +1,45 @@
# Lazaret
Rootless gVisor sandbox for development tools and coding agents.
## Status
- Early development
- Source only; no binary releases
- Linux x86-64, kernel 6.9 or newer
## Host requirements
- Unprivileged user namespaces
- Contiguous subordinate UID and GID ranges of at least 65,536 IDs
- `/usr/bin/newuidmap`, `/usr/bin/newgidmap`, and `/usr/bin/slirp4netns`
- `/dev/fuse`, `fuse-overlayfs`, and `fusermount3`
- Rootless nerdctl with BuildKit
- `cmp`, `install`, and `sha256sum`
## Build and install
```sh
tools/install-to-host
```
The default destination is `~/.local/bin`. Pass `--install-dir DIRECTORY` to override it.
## Configure and run
```sh
mkdir -p ~/.config/lazaret
cp docs/example.toml ~/.config/lazaret/config.toml
lazaret default
lazaret default -- COMMAND ARGUMENT...
```
Edit the example network allowlists and persistent volume paths before use.
## Repository
- [Product specification](docs/spec/lazaret.md)
- [Example configuration](docs/example.toml)
- [Verification entry point](tools/verify)
- [Apache License 2.0](LICENSE)
- [Third-party notices](THIRD_PARTY_NOTICES.md)

View file

@ -448,6 +448,27 @@ class VerificationTests(unittest.TestCase):
(ROOT / "patches/gvisor-9f8f6c917-MODULE.bazel.lock").is_file()
)
def test_canonical_build_uses_one_unprivileged_nerdctl_container(self) -> None:
build = (ROOT / "tools/build").read_text()
containerfile = (ROOT / "tools/container/Containerfile").read_text()
installer = (ROOT / "tools/install-to-host").read_text()
materializer = (ROOT / "tools/materialize-runsc").read_text()
self.assertIn("nerdctl build", build)
self.assertIn("nerdctl run --rm", build)
self.assertIn("tools/materialize-runsc --build", build)
self.assertIn("cargo build", build)
self.assertIn("GIT_CEILING_DIRECTORIES=/workspace", build)
self.assertNotIn("--privileged", build)
self.assertNotIn("docker.sock", build)
self.assertIn("rust:1.97.0-bookworm@sha256:", containerfile)
self.assertIn("ARG BAZEL_VERSION=8.3.1", containerfile)
self.assertIn("tools/build", installer)
self.assertNotIn("cargo build", installer)
self.assertNotIn("readelf", installer)
self.assertIn("bazel build", materializer)
self.assertNotIn("make -C", materializer)
def test_workspace_tests_use_nextest_and_cargo_retains_doctests(self) -> None:
source = (ROOT / "tools/lazaret_verify.py").read_text()
expected = [

67
tools/build Executable file
View file

@ -0,0 +1,67 @@
#!/usr/bin/env bash
set -euo pipefail
root="$(cd "$(dirname "$0")/.." && pwd)"
containerfile="$root/tools/container/Containerfile"
fail() {
printf 'build: %s\n' "$*" >&2
exit 1
}
if [[ "${1:-}" == --inside-container && $# -eq 1 ]]; then
[[ "${LAZARET_BUILD_CONTAINER:-}" == 1 ]] || fail 'container marker is absent'
cd "$root"
tools/materialize-fuser
tools/materialize-runsc --build
cargo build \
--release \
--locked \
--package lazaret-client \
--package lazaret-host-coordinator \
--package lazaret-escalation-client \
--bins
env RUSTFLAGS="-C relocation-model=static -C link-arg=-no-pie" cargo build \
--release \
--locked \
--target x86_64-unknown-linux-musl \
--package lazaret-sandbox-init \
--bin lazaret-sandbox-init
sandbox_init="$CARGO_TARGET_DIR/x86_64-unknown-linux-musl/release/lazaret-sandbox-init"
if readelf -l "$sandbox_init" | grep -q 'Requesting program interpreter' ||
! readelf -d "$sandbox_init" 2>&1 | grep -q 'There is no dynamic section'; then
fail 'sandbox init is dynamically linked'
fi
exit 0
elif [[ $# -ne 0 ]]; then
printf '%s\n' 'usage: tools/build' >&2
exit 2
fi
for command in nerdctl sha256sum; do
command -v "$command" >/dev/null 2>&1 || fail "$command is required"
done
mkdir -p "$root/.lazaret"
containerfile_digest="$(sha256sum "$containerfile")"
containerfile_digest="${containerfile_digest%% *}"
image="localhost/lazaret-builder:${containerfile_digest:0:16}"
proxy_options=()
for variable in ALL_PROXY HTTP_PROXY HTTPS_PROXY NO_PROXY all_proxy http_proxy https_proxy no_proxy; do
if [[ -v $variable ]]; then
proxy_options+=(--env "$variable")
fi
done
nerdctl build --tag "$image" --file "$containerfile" "$root/tools/container"
nerdctl run --rm \
--mount "type=bind,src=$root,dst=/workspace,ro" \
--mount "type=bind,src=$root/.lazaret,dst=/workspace/.lazaret" \
--workdir /workspace \
--env CARGO_HOME=/workspace/.lazaret/cache/cargo \
--env CARGO_TARGET_DIR=/workspace/.lazaret/target \
--env GIT_CEILING_DIRECTORIES=/workspace \
--env HOME=/workspace/.lazaret/home \
--env LAZARET_BUILD_CONTAINER=1 \
"${proxy_options[@]}" \
"$image" \
tools/build --inside-container

View file

@ -0,0 +1,49 @@
FROM docker.io/library/rust:1.97.0-bookworm@sha256:8fa55b2f3ddf97471ab6a767bfa3f37e6bad0986ba823e75fea57e2a2a5c3073
ARG BAZEL_VERSION=8.3.1
ARG BAZEL_SHA256=17247e8a84245f59d3bc633d0cfe0a840992a7760a11af1a30012d03da31604c
RUN apt-get update && apt-get install -y --no-install-recommends \
binutils \
build-essential \
busybox-static \
ca-certificates \
clang \
cmake \
crossbuild-essential-amd64 \
crossbuild-essential-arm64 \
curl \
diffutils \
erofs-utils \
git \
iproute2 \
iptables \
kmod \
libbpf-dev \
libc6-dev-i386 \
libffi-dev \
libnuma-dev \
libssl-dev \
musl-tools \
netcat-openbsd \
openjdk-17-jdk-headless \
patch \
pkg-config \
python-is-python3 \
python3 \
python3-pip \
python3-setuptools \
qemu-user-static \
unzip \
zip \
&& rm -rf /var/lib/apt/lists/*
RUN curl --fail --location \
"https://releases.bazel.build/${BAZEL_VERSION}/release/bazel-${BAZEL_VERSION}-linux-x86_64" \
--output /usr/local/bin/bazel \
&& printf '%s %s\n' "$BAZEL_SHA256" /usr/local/bin/bazel | sha256sum --check - \
&& chmod 0755 /usr/local/bin/bazel \
&& rustup target add x86_64-unknown-linux-musl \
&& rustup component add clippy rustfmt
WORKDIR /workspace

View file

@ -18,7 +18,7 @@ elif [ "$#" -ne 0 ]; then
exit 2
fi
for command in cargo cmp install readelf; do
for command in cmp install; do
command -v "$command" >/dev/null 2>&1 || {
printf '%s\n' "install-to-host: $command is required" >&2
exit 1
@ -26,37 +26,15 @@ for command in cargo cmp install readelf; do
done
cd "$root"
tools/materialize-fuser
tools/materialize-runsc --build
cargo build \
--release \
--locked \
--package lazaret-client \
--package lazaret-host-coordinator \
--package lazaret-escalation-client \
--bins
env RUSTFLAGS="-C relocation-model=static -C link-arg=-no-pie" cargo build \
--release \
--locked \
--target x86_64-unknown-linux-musl \
--package lazaret-sandbox-init \
--bin lazaret-sandbox-init
tools/build
target_dir="${CARGO_TARGET_DIR:-target}"
if [[ "$target_dir" != /* ]]; then
target_dir="$root/$target_dir"
fi
target_dir="$root/.lazaret/target"
install -d -m 755 "$install_dir"
for executable in lazaret lazaret-host-coordinator lazaret-escalation-client; do
install -m 755 "$target_dir/release/$executable" "$install_dir/$executable"
done
sandbox_init="$target_dir/x86_64-unknown-linux-musl/release/lazaret-sandbox-init"
if readelf -l "$sandbox_init" | grep -q 'Requesting program interpreter' ||
! readelf -d "$sandbox_init" 2>&1 | grep -q 'There is no dynamic section'; then
printf '%s\n' "install-to-host: sandbox init is dynamically linked" >&2
exit 1
fi
install -m 755 "$sandbox_init" "$install_dir/lazaret-sandbox-init"
runsc_build_id=9e711e005f13402e728a0c3ada3fdc67a38918da31f720e5be1006edc4272a51

View file

@ -42,7 +42,7 @@ fail() {
exit 1
}
for command in curl make patch sha256sum tar; do
for command in curl patch sha256sum tar; do
command -v "$command" >/dev/null 2>&1 || fail "$command is required"
done
@ -92,14 +92,39 @@ elif [[ "$(tr -d '\n' <"$source/.lazaret-materialization")" != "$stamp" ]]; then
fail 'materialized source identity mismatch'
fi
if [[ "$mode" != source ]]; then
[[ "${LAZARET_BUILD_CONTAINER:-}" == 1 ]] || fail 'build and test modes require tools/build'
command -v bazel >/dev/null 2>&1 || fail 'bazel is required'
[[ "$(bazel --version)" == "bazel $bazel_version" ]] || fail 'Bazel version mismatch'
fi
if [[ "$mode" == test ]]; then
make -C "$source" test \
TARGETS='//pkg/sentry/fsimpl/gofer:gofer_test //runsc/fsgofer:lisafs_test //test/syscalls:socket_ip_tcp_generic_loopback_test_native //test/syscalls:socket_ip_tcp_generic_loopback_test_runsc_systrap_hostnet'
make -C "$source" test OPTIONS='--test_filter=TestProcessStateIsTerminal' \
TARGETS=//runsc/container:container_test
(
cd "$source"
bazel test \
--color=no \
--curses=no \
--incompatible_sandbox_hermetic_tmp=false \
--keep_going \
--strip=never \
--test_output=errors \
--verbose_failures=true \
//pkg/sentry/fsimpl/gofer:gofer_test \
//runsc/fsgofer:lisafs_test \
//test/syscalls:socket_ip_tcp_generic_loopback_test_native \
//test/syscalls:socket_ip_tcp_generic_loopback_test_runsc_systrap_hostnet
bazel test \
--color=no \
--curses=no \
--incompatible_sandbox_hermetic_tmp=false \
--strip=never \
--test_filter=TestProcessStateIsTerminal \
--test_output=errors \
--verbose_failures=true \
//runsc/container:container_test
)
fi
if [[ "$mode" != source ]]; then
make -C "$source" build TARGETS=//runsc:runsc
(cd "$source" && bazel build --color=no --curses=no //runsc:runsc)
built="$source/bazel-bin/runsc/runsc_/runsc"
[[ -x "$built" ]] || fail 'canonical build did not produce runsc'
version_output="$($built --version 2>&1)"