1
0
Fork 0
mirror of https://github.com/imjasonh/krust synced 2026-07-08 06:45:32 +00:00
krust/README.md
Jason Hall 80656aa225
fix: Handle cross-compilation failures in platform detection test
The test now accepts linker errors as expected behavior when building
for detected platforms that lack cross-compilation toolchains on the
current runner (e.g., ARM runners without x86_64 toolchains).

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-08 00:17:10 -04:00

12 KiB

krust

A container image build tool for Rust applications, inspired by ko for Go.

Overview

krust builds container images for Rust applications without requiring Docker. It:

  • Executes cargo build to compile your Rust application as a static binary using musl libc
  • Packages the resulting binary into a minimal container image layer
  • Pushes images to OCI-compliant registries by default (use --no-push to skip)
  • Creates truly static binaries by default for maximum portability and security

Quick Start

# Install krust
cargo install --path .

# Set up your repository
export KRUST_REPO=ttl.sh/$USER

# Build and run your Rust app as a container
docker run $(krust build)

Installation

cargo install --path .

Prerequisites

Install the Linux musl targets for static binary cross-compilation:

# For linux/amd64 (most common)
rustup target add x86_64-unknown-linux-musl

# For linux/arm64
rustup target add aarch64-unknown-linux-musl

# Or install all supported targets at once
rustup target add \
    x86_64-unknown-linux-musl \
    aarch64-unknown-linux-musl \
    armv7-unknown-linux-musleabihf \
    arm-unknown-linux-musleabihf \
    i686-unknown-linux-musl \
    powerpc64le-unknown-linux-musl \
    s390x-unknown-linux-musl \
    riscv64gc-unknown-linux-musl

macOS Cross-compilation Setup

On macOS, you'll need a cross-compilation toolchain:

# Install musl cross-compilation tools
brew install filosottile/musl-cross/musl-cross

# Note: The musl-cross formula typically only includes x86_64 and aarch64 toolchains.
# For other architectures, you may need additional toolchains or use Docker/remote builders.

# Create a .cargo/config.toml in your project with:
cat > .cargo/config.toml << 'EOF'
[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"

[target.aarch64-unknown-linux-musl]
linker = "aarch64-linux-musl-gcc"

# For other architectures, you'll need to install the appropriate cross-compiler
# or use cargo-zigbuild which can target all platforms:
# cargo install cargo-zigbuild
# Then build with: cargo zigbuild --target <target>
EOF

Note: krust builds fully static binaries by default using musl libc, ensuring maximum portability across different Linux distributions and container environments.

Usage

krust outputs the pushed image reference by digest to stdout, with all other output going to stderr. This enables composability with other tools.

Build a project in the current directory

# Set your repository prefix
export KRUST_REPO=ttl.sh/jason

# Build and push (default behavior)
krust build

# Build without pushing
krust build --no-push

# Build, push, and run immediately
docker run $(krust build)

Build a specific directory

# Build and push a specific project
krust build path/to/rust/project

# Build without pushing
krust build example/hello-krust --no-push

Override the image name

# Use a specific image name (overrides KRUST_REPO)
krust build --image myregistry.io/myapp:v1.0

# Build for a specific platform
krust build --platform linux/arm64

# Build for multiple platforms (multi-arch)
krust build --platform linux/amd64,linux/arm64

# Or specify platforms separately
krust build --platform linux/amd64 --platform linux/arm64

# Default behavior detects platforms from base image
# If the base image supports multiple platforms, krust will build for all of them
krust build

Build with custom cargo arguments

krust build -- --features=prod

Supported Platforms

  • linux/amd64 (x86_64-unknown-linux-musl)
  • linux/arm64 (aarch64-unknown-linux-musl)
  • linux/arm/v7 (armv7-unknown-linux-musleabihf)
  • linux/arm/v6 (arm-unknown-linux-musleabihf)
  • linux/386 (i686-unknown-linux-musl)
  • linux/ppc64le (powerpc64le-unknown-linux-musl)
  • linux/s390x (s390x-unknown-linux-musl)
  • linux/riscv64 (riscv64gc-unknown-linux-musl)

Multi-Architecture Images

krust always pushes OCI image indexes (manifest lists) for consistency:

  1. Builds each platform separately with its own binary
  2. Pushes platform-specific images with unique tags
  3. Creates and pushes a manifest list that references all platforms
  4. Returns the manifest list digest for use with Docker/Kubernetes

This means even single-platform builds result in a manifest list, ensuring a uniform interface regardless of the number of platforms built.

Automatic Platform Detection

When you don't specify --platform, krust automatically detects which platforms to build for by inspecting the base image:

# If using cgr.dev/chainguard/static:latest (supports linux/amd64 and linux/arm64)
krust build  # Automatically builds for both amd64 and arm64

# If using a single-platform base image
krust build  # Builds only for the supported platform

# You can always override with explicit platforms
krust build --platform linux/amd64  # Build only for amd64 regardless of base image

This intelligent platform detection ensures your images support the same platforms as your base image, maintaining consistency throughout your image stack.

Build Process

krust builds your Rust application in an isolated environment:

  1. Temporary build directory - Each build uses a unique temporary directory via --target-dir
  2. Static compilation - Builds with RUSTFLAGS="-C target-feature=+crt-static" for musl targets
  3. Cross-compilation - Automatically configures the appropriate linker for the target platform
  4. Binary extraction - Copies the built binary from the temp directory for packaging
  5. Container creation - Packages the binary into a minimal OCI image

This approach ensures:

  • No conflicts between concurrent builds
  • Clean builds without interference from previous compilations
  • Safe parallel execution of multiple krust instances

Static Binaries

krust builds fully static binaries by default using:

  • musl libc for Linux targets
  • RUSTFLAGS="-C target-feature=+crt-static" for static linking
  • Distroless static base image (gcr.io/distroless/static:nonroot)

This ensures your applications work across all Linux distributions without dependency issues.

Why musl instead of glibc?

krust uses musl libc instead of glibc for several important reasons:

  1. True static linking - musl is designed for static linking, while glibc uses dynamic loading internally (NSS) that breaks in static binaries
  2. Smaller binaries - musl static binaries are typically 5-10x smaller than glibc equivalents
  3. No runtime surprises - glibc static binaries often fail at runtime with DNS resolution, user lookups, or locale issues
  4. Container-optimized - musl's simplicity makes it ideal for containers where you want minimal dependencies
  5. Security - Smaller attack surface with fewer moving parts

The tradeoff is that musl has slightly different behavior than glibc in some edge cases, but for most applications this is not an issue. If your application requires glibc-specific behavior, you can override the default by building locally with cargo and creating your own container image.

Environment Variables

  • KRUST_REPO - Default repository prefix for built images (e.g., ttl.sh/username)
  • KRUST_IMAGE - Override the full image reference for a build

Configuration

Project Configuration (Cargo.toml)

You can configure krust on a per-project basis by adding a [package.metadata.krust] section to your project's Cargo.toml:

[package.metadata.krust]
base-image = "cgr.dev/chainguard/static:latest"  # Override the default base image

This is the idiomatic way to configure build tools in Rust, similar to how cargo-deb, wasm-pack, and other Cargo extensions work.

Global Configuration

krust also looks for global configuration at ~/.config/krust/config.toml:

base_image = "cgr.dev/chainguard/static:latest"  # Default base image for all projects
default_registry = "ghcr.io"

[build]
cargo_args = ["--features", "production"]

[registries."ghcr.io"]
username = "myuser"
password = "mytoken"

Note: Registry authentication is not yet implemented. Currently, krust uses anonymous authentication.

Configuration Precedence

When determining the base image, krust uses this precedence order:

  1. Project-specific config in Cargo.toml (highest priority)
  2. Global config in ~/.config/krust/config.toml
  3. Built-in default: cgr.dev/chainguard/static:latest (lowest priority)

Key Features

  • Docker-free - Builds OCI container images without requiring Docker daemon
  • Static binaries - Produces truly static binaries using musl libc
  • Composable - Outputs image digest to stdout, enabling docker run $(krust build)
  • Multi-arch support - Build for multiple platforms in a single command
  • Cross-platform - Supports multiple architectures (amd64, arm64, arm/v7)
  • Minimal images - Uses distroless base images for security and size
  • OCI compliant - Works with any OCI-compliant container registry
  • Isolated builds - Each build uses a temporary directory to avoid conflicts
  • Concurrent builds - Multiple builds can run safely in parallel

Example

Build and run the example application:

# Set your repository (ttl.sh provides temporary anonymous storage)
export KRUST_REPO=ttl.sh/jason

# Build and push the example (default behavior)
krust build example/hello-krust

# Build without pushing
krust build example/hello-krust --no-push

# Build, push, and run the example
docker run $(krust build example/hello-krust)

# Or specify a custom image name with TTL (time-to-live)
# Images on ttl.sh expire based on the tag: 1h, 2d, 1w, etc.
krust build example/hello-krust --image ttl.sh/jason/hello:1h

Note: ttl.sh is a free, temporary container registry perfect for testing. Images are automatically deleted after their TTL expires.

CLI Reference

krust build [OPTIONS] [DIRECTORY] [-- <CARGO_ARGS>...]

Arguments:
  [DIRECTORY]      Path to the Rust project directory (defaults to current directory)
  [CARGO_ARGS]...  Additional cargo build arguments

Options:
  -i, --image <IMAGE>        Target image reference (overrides KRUST_REPO)
      --platform <PLATFORM>  Target platform [default: linux/amd64]
      --no-push              Skip pushing the image to registry
      --repo <REPO>          Repository prefix (uses KRUST_REPO env var)
  -v, --verbose              Enable verbose logging
  -h, --help                 Print help

Troubleshooting

macOS: "linking with cc failed"

This error occurs when the cross-compilation toolchain is not properly configured. Make sure you:

  1. Install musl-cross: brew install filosottile/musl-cross/musl-cross
  2. Create .cargo/config.toml in your project with the appropriate linker configuration

"target may not be installed"

Install the required target with rustup:

rustup target add x86_64-unknown-linux-musl

Platform mismatch warning when running images

This is normal when building linux/amd64 images on Apple Silicon. The images will still run correctly under emulation.

Development

Setting up development environment

# Clone the repository
git clone https://github.com/imjasonh/krust.git
cd krust

# Install cross-compilation toolchain (required for tests)
# On macOS:
brew install messense/macos-cross-toolchains/x86_64-unknown-linux-musl
brew install messense/macos-cross-toolchains/aarch64-unknown-linux-musl

# For full platform support, consider using cargo-zigbuild:
cargo install cargo-zigbuild

# Install Rust targets (at minimum for tests)
rustup target add x86_64-unknown-linux-musl
rustup target add aarch64-unknown-linux-musl

# For full platform support, add all targets:
rustup target add \
    armv7-unknown-linux-musleabihf \
    arm-unknown-linux-musleabihf \
    i686-unknown-linux-musl \
    powerpc64le-unknown-linux-musl \
    s390x-unknown-linux-musl \
    riscv64gc-unknown-linux-musl

# Install pre-commit hooks
pip install pre-commit
  or
brew install pre-commit

pre-commit install

# Build and test
cargo build
cargo test

Pre-commit hooks

This project uses pre-commit hooks to ensure code quality. The hooks will automatically:

  • Run cargo fmt to format code
  • Run cargo clippy to check for common mistakes
  • Run cargo check to ensure the project compiles
  • Fix trailing whitespace and ensure files end with newline
  • Validate YAML files

To run the hooks manually:

pre-commit run --all-files

Running tests

# Run all tests
cargo test

# Run only unit tests
cargo test --lib

# Run integration tests
cargo test --test integration_test

# Run with verbose output
cargo test -- --nocapture

License

MIT OR Apache-2.0