1
0
Fork 0
mirror of https://github.com/imjasonh/krust synced 2026-07-06 22:12:32 +00:00

Use cargo-zigbuild for cross-compilation, add build caching

- Integrate cargo-zigbuild as the default build backend, falling back
  to cargo build with best-effort linker detection if not installed
- Auto-install rustup targets when missing (rustup target add)
- Use persistent target/krust/ directory instead of temp dirs so
  incremental compilation works across runs
- Remove .cargo/config.toml from repo (add to .gitignore) since
  zigbuild handles cross-linker configuration
- Improve error messages: suggest cargo-zigbuild when linker not found
- Simplify CI to use zigbuild instead of per-target system linkers
- Simplify Makefile by removing setup-cross-compile/verify targets
- Update README and CLAUDE.md to reflect new approach

https://claude.ai/code/session_01EcsZDNeWn56wFqryb4Wq7r
This commit is contained in:
Claude 2026-03-17 20:33:41 +00:00
parent 68a9a86e1c
commit bb76a120c3
No known key found for this signature in database
8 changed files with 181 additions and 244 deletions

View file

@ -1,5 +0,0 @@
[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"
[target.aarch64-unknown-linux-musl]
linker = "aarch64-linux-musl-gcc"

View file

@ -25,10 +25,10 @@ jobs:
toolchain: ${{ matrix.rust }}
targets: x86_64-unknown-linux-musl,aarch64-unknown-linux-musl
components: rustfmt,clippy
- name: Install cross-compilation tools for both architectures
- name: Install cargo-zigbuild and zig
run: |
sudo apt-get update
sudo apt-get install -y musl-tools gcc-aarch64-linux-gnu gcc-x86-64-linux-gnu
sudo snap install zig --classic --beta || sudo apt-get update && sudo apt-get install -y ziglang
cargo install cargo-zigbuild
- name: Cache cargo registry
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4
with:
@ -47,19 +47,6 @@ jobs:
- run: make check-fmt
- run: make lint
- name: Setup cargo config for CI cross-compilation
run: |
mkdir -p .cargo
cat > .cargo/config.toml << 'EOF'
[target.x86_64-unknown-linux-musl]
linker = "musl-gcc"
[target.aarch64-unknown-linux-musl]
linker = "aarch64-linux-gnu-gcc"
EOF
- run: make verify-cross-compile
- run: make build
- run: make test
- run: make test-e2e

1
.gitignore vendored
View file

@ -16,6 +16,7 @@ Thumbs.db
# Cargo
Cargo.lock
.cargo/
# Coverage
tarpaulin-report.html

View file

@ -90,16 +90,13 @@ GAR has special handling for blob uploads that differs from the standard OCI spe
- Blob uploads require `.to_vec()` due to reqwest's `'static` requirement for request bodies
- This is acceptable as reqwest streams the data internally
### Cross-Compilation on macOS
### Cross-Compilation
For Linux targets from macOS, you need:
1. Target toolchain: `rustup target add x86_64-unknown-linux-musl`
2. Cross-linker: `brew install filosottile/musl-cross/musl-cross`
3. Cargo config to specify the linker:
```toml
[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"
```
krust uses `cargo-zigbuild` for cross-compilation by default. This eliminates the need for
per-target system linkers and `.cargo/config.toml` linker configuration. If zigbuild is not
available, krust falls back to `cargo build` with best-effort system linker detection.
Required targets are auto-installed via `rustup target add` when needed.
### Rust Static Linking
@ -137,6 +134,7 @@ Key crates chosen:
- `tar` + `flate2` - Layer creation
- `sha256` - Digest calculation
- `tracing` - Structured logging
- `cargo-zigbuild` - Cross-compilation backend (external tool, not a crate dep)
## Testing Strategy
@ -207,7 +205,7 @@ Potential enhancements identified:
1. ~~Registry authentication support~~ ✓ Implemented (supports Docker credential helpers)
2. ~~YAML resolution for Kubernetes deployments~~ ✓ Implemented (`krust resolve`)
3. Multi-platform image manifests
4. Build caching
4. ~~Build caching~~ ✓ Implemented (persistent `target/krust/` directory)
5. Image layer optimization
6. Support for custom Dockerfile-like configs
7. SBOM (Software Bill of Materials) generation
@ -232,8 +230,8 @@ krust build example/hello-krust
krust build -v 2>&1 | less
# Check static linking
file target/x86_64-unknown-linux-musl/release/binary
ldd target/x86_64-unknown-linux-musl/release/binary # should say "not a dynamic executable"
file target/krust/x86_64-unknown-linux-musl/release/binary
ldd target/krust/x86_64-unknown-linux-musl/release/binary # should say "not a dynamic executable"
# Verify pushed image
crane manifest $(krust build --no-push example/hello-krust)

View file

@ -27,7 +27,6 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
dirs = "6.0"
toml = "0.9"
which = "8.0"
tempfile = "3.9"
base64 = "0.22"
[dev-dependencies]

View file

@ -1,4 +1,4 @@
.PHONY: test test-unit test-e2e test-testscript build run clean fmt lint check-fmt check install setup-cross-compile verify-cross-compile
.PHONY: test test-unit test-e2e test-testscript build run clean fmt lint check-fmt check install
# Test flags - run single-threaded to avoid env var races
TEST_FLAGS := -- --test-threads=1
@ -7,44 +7,6 @@ TEST_FLAGS := -- --test-threads=1
build:
cargo build --verbose
# Setup cargo config for cross-compilation
setup-cross-compile:
@mkdir -p .cargo
@cat > .cargo/config.toml <<'EOF'
[target.x86_64-unknown-linux-musl]
linker = "musl-gcc"
[target.aarch64-unknown-linux-musl]
linker = "aarch64-linux-gnu-gcc"
EOF
@echo "Created .cargo/config.toml for cross-compilation"
# Verify cross-compilation setup
verify-cross-compile:
@echo "=== Rust Installation ==="
@echo "Installed targets:"
@rustup target list --installed
@echo ""
@echo "Cargo version:"
@cargo --version
@echo ""
@echo "Rustc version:"
@rustc --version
@echo ""
@echo "=== Available Linkers ==="
@which x86_64-unknown-linux-musl-gcc 2>/dev/null && echo "✓ x86_64-unknown-linux-musl-gcc found" || echo "✗ x86_64-unknown-linux-musl-gcc not found"
@which x86_64-linux-musl-gcc 2>/dev/null && echo "✓ x86_64-linux-musl-gcc found" || echo "✗ x86_64-linux-musl-gcc not found"
@which musl-gcc 2>/dev/null && echo "✓ musl-gcc found" || echo "✗ musl-gcc not found"
@which aarch64-linux-gnu-gcc 2>/dev/null && echo "✓ aarch64-linux-gnu-gcc found" || echo "✗ aarch64-linux-gnu-gcc not found"
@which rust-lld 2>/dev/null && echo "✓ rust-lld found" || echo "✗ rust-lld not found"
@echo ""
@echo "=== Cargo Config ==="
@if [ -f .cargo/config.toml ]; then \
cat .cargo/config.toml; \
else \
echo "No cargo config found at .cargo/config.toml"; \
fi
# Run the project
run:
cargo run

109
README.md
View file

@ -33,52 +33,20 @@ cargo install --path .
### Prerequisites
Install the Linux musl targets for static binary cross-compilation:
Install [`cargo-zigbuild`](https://github.com/rust-cross/cargo-zigbuild) for cross-compilation (recommended):
```bash
# For linux/amd64 (most common)
rustup target add x86_64-unknown-linux-musl
# Install zig (cargo-zigbuild's cross-compilation backend)
# macOS:
brew install zig
# Linux:
sudo snap install zig --classic --beta # or via your package manager
# 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
# Install cargo-zigbuild
cargo install cargo-zigbuild
```
#### macOS Cross-compilation Setup
On macOS, you'll need a cross-compilation toolchain:
```bash
# 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
```
krust will automatically install the required rustup targets when building. If `cargo-zigbuild` is not installed, krust falls back to `cargo build` and will attempt to find a system cross-linker.
Note: krust builds fully static binaries by default using musl libc, ensuring maximum portability across different Linux distributions and container environments.
@ -166,19 +134,14 @@ This intelligent platform detection ensures your images support the same platfor
## Build Process
krust builds your Rust application in an isolated environment:
krust builds your Rust application and packages it into a container image:
1. **Temporary build directory** - Each build uses a unique temporary directory via `--target-dir`
1. **Target installation** - Automatically installs the required rustup target if missing
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
3. **Cross-compilation** - Uses `cargo-zigbuild` for seamless cross-compilation (falls back to `cargo build` with system linkers)
4. **Cached builds** - Uses `target/krust/` as the build directory, so incremental compilation works across runs
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:
@ -438,20 +401,15 @@ krust resolve -f deployment.yaml | kubectl apply -f -
## Troubleshooting
### macOS: "linking with `cc` failed"
### "linking with `cc` failed" or linker errors
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:
Install `cargo-zigbuild` for seamless cross-compilation:
```bash
rustup target add x86_64-unknown-linux-musl
cargo install cargo-zigbuild
```
If you prefer not to use zigbuild, install the appropriate system cross-linker for your target platform.
### 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.
@ -465,32 +423,11 @@ This is normal when building linux/amd64 images on Apple Silicon. The images wil
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:
# Install cargo-zigbuild for cross-compilation
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
pip install pre-commit # or: brew install pre-commit
pre-commit install
# Build and test
@ -505,23 +442,17 @@ The project includes a comprehensive Makefile for common development tasks:
```bash
# Building
make build # Build the project
make build-verbose # Build with verbose output
# Testing (runs single-threaded to avoid env var races)
make test # Run all tests
make test-unit # Run unit tests only
make test-e2e # Run end-to-end tests only
make test-verbose # Run all tests with verbose output
# Code quality
make fmt # Format code
make lint # Run clippy linter
make check-fmt # Check formatting without fixing
make check # Run all checks (format, lint, test)
# Cross-compilation
make setup-cross-compile # Set up cargo config for cross-compilation
make verify-cross-compile # Verify cross-compilation setup
```
### Pre-commit hooks

View file

@ -1,8 +1,7 @@
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
use tracing::{debug, error, info};
use tracing::{debug, info, warn};
#[cfg(test)]
mod tests;
@ -15,7 +14,6 @@ pub struct RustBuilder {
pub struct BuildResult {
pub binary_path: PathBuf,
_temp_dir: TempDir, // Keep temp dir alive until BuildResult is dropped
}
impl RustBuilder {
@ -32,94 +30,91 @@ impl RustBuilder {
self
}
/// Check if cargo-zigbuild is available.
fn has_zigbuild() -> bool {
Command::new("cargo")
.args(["zigbuild", "--version"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
/// Check if the rustup target is installed, and install it if not.
fn ensure_target_installed(target: &str) -> Result<()> {
let output = Command::new("rustup")
.args(["target", "list", "--installed"])
.output()
.context("Failed to run rustup. Is rustup installed?")?;
let installed = String::from_utf8_lossy(&output.stdout);
if installed.lines().any(|line| line.trim() == target) {
return Ok(());
}
info!("Installing rustup target: {}", target);
let status = Command::new("rustup")
.args(["target", "add", target])
.status()
.context("Failed to run rustup target add")?;
if !status.success() {
anyhow::bail!(
"Failed to install target '{}'. Run: rustup target add {}",
target,
target
);
}
Ok(())
}
/// Get the persistent target directory for krust builds.
/// Uses `<project>/target/krust/` so cargo can reuse build caches.
fn target_dir(&self) -> PathBuf {
self.project_path.join("target").join("krust")
}
pub fn build(&self) -> Result<BuildResult> {
info!("Building Rust project at {:?}", self.project_path);
// Use a unique target directory to avoid conflicts between concurrent builds
let temp_target_dir =
tempfile::tempdir().context("Failed to create temporary directory")?;
let target_dir = temp_target_dir.path();
// Ensure the target is installed via rustup
Self::ensure_target_installed(&self.target)?;
let target_dir = self.target_dir();
let use_zigbuild = Self::has_zigbuild();
let mut cmd = Command::new("cargo");
cmd.arg("build")
.arg("--release")
if use_zigbuild {
info!("Using cargo-zigbuild for cross-compilation");
cmd.arg("zigbuild");
} else {
warn!(
"cargo-zigbuild not found, falling back to cargo build. \
Install it for easier cross-compilation: cargo install cargo-zigbuild"
);
cmd.arg("build");
}
cmd.arg("--release")
.arg("--target")
.arg(&self.target)
.arg("--target-dir")
.arg(target_dir)
.arg(&target_dir)
.current_dir(&self.project_path);
// Set RUSTFLAGS for static linking
let rustflags = if self.target.contains("musl") {
// For musl targets, ensure fully static linking
"-C target-feature=+crt-static"
} else {
// For GNU targets, link statically where possible
"-C target-feature=+crt-static -C link-arg=-static-libgcc"
};
cmd.env("RUSTFLAGS", rustflags);
// For cross-compilation on non-Linux platforms, set linker if available
if cfg!(not(target_os = "linux")) && self.target.contains("linux") {
// Check if we have a musl cross-compiler available
if self.target.contains("x86_64-unknown-linux-musl") {
// On Windows, use rust-lld with full path
if cfg!(target_os = "windows") {
// Get the rust sysroot to find rust-lld
let sysroot_output = Command::new("rustc")
.arg("--print")
.arg("sysroot")
.output()
.context("Failed to get rustc sysroot")?;
if sysroot_output.status.success() {
let sysroot = String::from_utf8_lossy(&sysroot_output.stdout)
.trim()
.to_string();
let rust_lld = PathBuf::from(&sysroot)
.join("lib")
.join("rustlib")
.join("x86_64-pc-windows-msvc")
.join("bin")
.join("rust-lld.exe");
if rust_lld.exists() {
cmd.env(
"CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER",
rust_lld.to_string_lossy().to_string(),
);
debug!("Using linker: {}", rust_lld.display());
} else {
// Fallback to just "rust-lld" and hope it's in PATH
cmd.env("CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER", "rust-lld");
debug!("Using linker: rust-lld (in PATH)");
}
} else {
// Fallback to just "rust-lld"
cmd.env("CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER", "rust-lld");
debug!("Using linker: rust-lld (fallback)");
}
} else {
// Try common linker names on other platforms
let linkers = if cfg!(target_os = "macos") {
vec![
"x86_64-unknown-linux-musl-gcc",
"x86_64-linux-musl-gcc",
"musl-gcc",
]
} else {
vec!["x86_64-linux-musl-gcc", "musl-gcc", "x86_64-linux-gnu-gcc"]
};
for linker in &linkers {
if which::which(linker).is_ok() {
cmd.env("CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER", linker);
debug!("Using linker: {}", linker);
break;
}
}
}
}
// When not using zigbuild, try to find a cross-linker for non-native targets
if !use_zigbuild {
self.configure_cross_linker(&mut cmd);
}
for arg in &self.cargo_args {
@ -133,11 +128,20 @@ impl RustBuilder {
let output = cmd.output().context("Failed to execute cargo build")?;
if !output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
error!("Cargo build failed!");
error!("stdout:\n{}", stdout);
error!("stderr:\n{}", stderr);
// Provide actionable error messages
let stderr_str = stderr.to_string();
if stderr_str.contains("linker") && stderr_str.contains("not found") {
anyhow::bail!(
"Cross-compilation linker not found for target '{}'.\n\
Install cargo-zigbuild for easy cross-compilation:\n\
\n cargo install cargo-zigbuild\n\
\nOr install the appropriate system linker for your platform.",
self.target
);
}
anyhow::bail!("Cargo build failed: {}", stderr);
}
@ -163,11 +167,71 @@ impl RustBuilder {
info!("Successfully built binary at {:?}", binary_path);
// Return the build result with the temp directory to keep it alive
Ok(BuildResult {
binary_path,
_temp_dir: temp_target_dir,
})
Ok(BuildResult { binary_path })
}
/// Configure cross-compilation linker when not using zigbuild.
/// This is a best-effort fallback for users who don't have zigbuild installed.
fn configure_cross_linker(&self, cmd: &mut Command) {
// Only needed when cross-compiling
let is_cross = if cfg!(target_os = "linux") {
// On Linux, check if we're building for a different arch
let host_arch = std::env::consts::ARCH;
let target_arch = self.target.split('-').next().unwrap_or("");
host_arch != target_arch
&& !(host_arch == "x86_64" && target_arch == "x86_64")
&& !(host_arch == "aarch64" && target_arch == "aarch64")
} else {
// On non-Linux hosts, always need cross-linker for Linux targets
self.target.contains("linux")
};
if !is_cross {
return;
}
// Build the CARGO_TARGET_<triple>_LINKER env var name
let env_var = format!(
"CARGO_TARGET_{}_LINKER",
self.target.to_uppercase().replace('-', "_")
);
// Try common linker names based on target
let linkers: Vec<&str> = if self.target.contains("x86_64") && self.target.contains("musl") {
vec!["x86_64-linux-musl-gcc", "musl-gcc"]
} else if self.target.contains("aarch64") && self.target.contains("musl") {
vec!["aarch64-linux-musl-gcc", "aarch64-linux-gnu-gcc"]
} else if self.target.contains("armv7") {
vec!["arm-linux-gnueabihf-gcc", "armv7-linux-musleabihf-gcc"]
} else if self.target.contains("arm-") {
vec!["arm-linux-gnueabihf-gcc", "arm-linux-musleabihf-gcc"]
} else if self.target.contains("i686") {
vec!["i686-linux-musl-gcc", "i686-linux-gnu-gcc"]
} else if self.target.contains("powerpc64le") {
vec!["powerpc64le-linux-gnu-gcc"]
} else if self.target.contains("s390x") {
vec!["s390x-linux-gnu-gcc"]
} else if self.target.contains("riscv64") {
vec!["riscv64-linux-gnu-gcc"]
} else {
vec![]
};
for linker in &linkers {
if which::which(linker).is_ok() {
cmd.env(&env_var, linker);
debug!("Using linker: {}", linker);
return;
}
}
if !linkers.is_empty() {
debug!(
"No cross-linker found for target '{}'. Build may fail. \
Consider installing cargo-zigbuild: cargo install cargo-zigbuild",
self.target
);
}
}
fn get_binary_name(&self) -> Result<String> {