diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b55490..1b3c0f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,16 +25,10 @@ jobs: with: toolchain: ${{ matrix.rust }} targets: x86_64-unknown-linux-musl,aarch64-unknown-linux-musl - - name: Install cross-compilation tools (Ubuntu x64) - if: matrix.os == 'ubuntu-latest' + - name: Install cross-compilation tools for both architectures run: | sudo apt-get update - sudo apt-get install -y musl-tools gcc-aarch64-linux-gnu - - name: Install cross-compilation tools (Ubuntu ARM) - if: matrix.os == 'ubuntu-24.04-arm' - run: | - sudo apt-get update - sudo apt-get install -y musl-tools gcc-x86-64-linux-gnu + sudo apt-get install -y musl-tools gcc-aarch64-linux-gnu gcc-x86-64-linux-gnu - name: Cache cargo registry uses: actions/cache@v4 with: diff --git a/Cargo.toml b/Cargo.toml index e6acb4f..bdfcd46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } dirs = "5.0" toml = "0.8" which = "6.0" +tempfile = "3.9" [dev-dependencies] tempfile = "3.9" diff --git a/README.md b/README.md index 86a3863..5ce315a 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,9 @@ krust build --platform linux/amd64,linux/arm64 # Or specify platforms separately krust build --platform linux/amd64 --platform linux/arm64 + +# Default behavior builds for both amd64 and arm64 +krust build ``` ### Build with custom cargo arguments @@ -122,6 +125,30 @@ krust build -- --features=prod - `linux/arm64` (aarch64-unknown-linux-musl) - `linux/arm/v7` (armv7-unknown-linux-musleabihf) +### Multi-Architecture Images + +When building for multiple platforms, krust: +1. Builds each platform separately with its own binary +2. Pushes platform-specific images with appropriate tags +3. Creates references that Docker/Kubernetes can use to automatically select the right architecture + +Note: Full OCI image index (manifest list) support is planned for a future release. + +## 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: @@ -195,6 +222,8 @@ When determining the base image, krust uses this precedence order: - **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 diff --git a/src/builder/mod.rs b/src/builder/mod.rs index 28e3961..538e4c6 100644 --- a/src/builder/mod.rs +++ b/src/builder/mod.rs @@ -1,6 +1,7 @@ use anyhow::{Context, Result}; use std::path::{Path, PathBuf}; use std::process::Command; +use tempfile::TempDir; use tracing::{debug, error, info}; #[cfg(test)] @@ -12,6 +13,11 @@ pub struct RustBuilder { cargo_args: Vec, } +pub struct BuildResult { + pub binary_path: PathBuf, + _temp_dir: TempDir, // Keep temp dir alive until BuildResult is dropped +} + impl RustBuilder { pub fn new(project_path: impl AsRef, target: &str) -> Self { Self { @@ -26,14 +32,21 @@ impl RustBuilder { self } - pub fn build(&self) -> Result { + pub fn build(&self) -> Result { 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(); + let mut cmd = Command::new("cargo"); cmd.arg("build") .arg("--release") .arg("--target") .arg(&self.target) + .arg("--target-dir") + .arg(target_dir) .current_dir(&self.project_path); // Set RUSTFLAGS for static linking @@ -116,6 +129,7 @@ impl RustBuilder { debug!("Running command: {:?}", cmd); debug!("RUSTFLAGS: {}", rustflags); + info!("Running cargo build for target: {}", self.target); let output = cmd.output().context("Failed to execute cargo build")?; if !output.status.success() { @@ -128,19 +142,30 @@ impl RustBuilder { } let binary_name = self.get_binary_name()?; - let binary_path = self - .project_path - .join("target") + let binary_path = target_dir .join(&self.target) .join("release") .join(&binary_name); + // Sometimes cargo build completes but the binary isn't immediately visible + // due to filesystem sync issues. Give it a moment. + let mut retries = 0; + while !binary_path.exists() && retries < 3 { + std::thread::sleep(std::time::Duration::from_millis(100)); + retries += 1; + } + if !binary_path.exists() { anyhow::bail!("Built binary not found at {:?}", binary_path); } info!("Successfully built binary at {:?}", binary_path); - Ok(binary_path) + + // Return the build result with the temp directory to keep it alive + Ok(BuildResult { + binary_path, + _temp_dir: temp_target_dir, + }) } fn get_binary_name(&self) -> Result { diff --git a/src/main.rs b/src/main.rs index d67d64c..44746ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -80,11 +80,14 @@ async fn main() -> Result<()> { let builder = RustBuilder::new(&project_path, &target).with_cargo_args(cargo_args.clone()); - let binary_path = builder.build()?; + let build_result = builder.build()?; // Build container image for this platform - let image_builder = - ImageBuilder::new(binary_path, base_image.clone(), platform_str.clone()); + let image_builder = ImageBuilder::new( + build_result.binary_path, + base_image.clone(), + platform_str.clone(), + ); let (config_data, layer_data, manifest) = image_builder.build()?;