proot-next

Building

Building from source, the static binaries, the tests and the documentation.

Toolchain

A stable Rust toolchain, edition 2024, Rust 1.89 or newer. The only crate dependency is libc, and there are no native libraries to install.

rustup component add rustfmt clippy
cargo build --release          # target/release/proot, dynamically linked to the host glibc

Static binaries

Releases ship static musl binaries. .cargo/config.toml configures both targets to link self-contained with Rust's own linker, using the C runtime objects rustup ships, so no cross toolchain is needed on either side:

rustup target add x86_64-unknown-linux-musl aarch64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl
cargo build --release --target aarch64-unknown-linux-musl

The aarch64 binary cross-builds on an x86_64 machine the same way. It cannot be run there: ptrace does not work under emulation, so the aarch64 tests run on aarch64 hardware, which is also what CI does.

Tests

tests/probes/build.sh                           # 32-bit probes, static musl
scripts/fetch-rootfs.sh ghcr.io/linuxserver/proot-apps:debian-trixie rootfs
PROOT_TEST_ROOTFS=$PWD/rootfs cargo test        # unit tests + tests/e2e.rs
cargo clippy --all-targets -- -D warnings
pre-commit run --all-files                      # rustfmt, clippy, codespell

tests/e2e.rs drives the built binary: the smoke script, a real rootfs, bwrap mode, debugger transparency, /proc link semantics, unix sockets through bindings, the i386 socketcall client and the 32-bit ARM probe. Each case is a failure first seen in a real application. Cases whose rootfs or probe is absent skip with a message. A change to translation, exec or the tracer is also run against a desktop application through proot-apps and, when it touches bwrap mode, against Steam in a selkies container with tests/steam/check.sh:

tests/steam/check.sh --home ~/steamtest --display :20 --force \
    --games ~/steamtest/games --wgltri ~/steamtest/games/wgltri.exe --client /usr/lib/steam/bin_steam.sh

It runs the soldier, sniper and scout-on-soldier containers, OpenGL and Vulkan for both architectures against what the host itself renders with, an X11 client, a native benchmark, a Windows OpenGL program through GE-Proton from ~/Steam/compatibilitytools.d, and the client's sign-in window. --games names a root holding usr/bin/glmark2 and usr/bin/vkcube under the home, where the containers see it (dpkg-deb -x of the packages will do); a binary the runtime cannot load is skipped. GE-Proton runs in the runtime its manifest asks for, as Steam runs it: GE-Proton 10 in sniper, GE-Proton 11 in Steam Linux Runtime 4, which an anonymous steamcmd cannot fetch. --home holds the runtimes as Steam installs them under Steam/steamapps/common. --force makes pressure-vessel take the stand-in even where its own bubblewrap works, and --proot names a build to check rather than the installed one. The Windows program is built from tests/steam/wgltri.c:

x86_64-w64-mingw32-gcc -O2 -o wgltri.exe tests/steam/wgltri.c -lopengl32 -lgdi32 -luser32 -mconsole

A host without 32-bit driver libraries leaves native 32-bit games on llvmpipe while 64-bit Proton keeps the GPU; on NVIDIA that is the container runtime's doing, whose CDI specification carries no 32-bit libraries (nvidia-container-toolkit#2035). The check compares each architecture's renderer in the container with the host's, which tells that apart from a fault of bwrap mode.

Diagnostics while developing

VariableEffect
PROOT_VERBOSE=1..3Errors and fatal signals with the module and address they happened at, every translation, every ptrace stop.
PROOT_EXEC_DEBUG=1Each rewritten execve with the loader and argv it was given.
PROOT_BWRAP_KEEP=1Keep the private root bwrap mode materializes.

Documentation

The usage site is a fumadocs application in docs/, the code reference is rustdoc, and the docs workflow publishes both to one GitHub Pages site. Locally:

cd docs && npm install && npm run dev   # http://localhost:3000
npm run build                           # static export in docs/out
cargo doc --no-deps --open              # code reference

No lockfile is kept. The one transitive dependency that has to stay put, mdast-util-to-markdown, is pinned through overrides in package.json.

On this page