mirror of
https://github.com/imjasonh/krust
synced 2026-07-07 22:35:25 +00:00
feat: Add multi-arch support
- Support building for multiple platforms with --platform flag - Accept comma-separated platforms or multiple --platform flags - Default to building for linux/amd64 and linux/arm64 - Add ARM runner (ubuntu-24.04-arm) to CI matrix - Create manifest list for multi-arch images (TODO: push to registry) - Update integration tests to handle multi-platform builds - Add cross-compilation setup instructions to README - Add .cargo/config.toml for local development Breaking changes: - Default behavior now builds for multiple platforms (amd64+arm64) - Use --platform linux/amd64 to build for single platform
This commit is contained in:
parent
11083dc527
commit
6153452303
8 changed files with 260 additions and 37 deletions
5
.cargo/config.toml
Normal file
5
.cargo/config.toml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
[target.x86_64-unknown-linux-musl]
|
||||
linker = "x86_64-unknown-linux-musl-gcc"
|
||||
|
||||
[target.aarch64-unknown-linux-musl]
|
||||
linker = "aarch64-unknown-linux-musl-gcc"
|
||||
21
.github/workflows/ci.yml
vendored
21
.github/workflows/ci.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
os: [ubuntu-latest, ubuntu-24.04-arm]
|
||||
rust: [stable, beta]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
@ -24,12 +24,17 @@ jobs:
|
|||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
targets: x86_64-unknown-linux-musl
|
||||
- name: Install cross-compilation tools (Ubuntu)
|
||||
targets: x86_64-unknown-linux-musl,aarch64-unknown-linux-musl
|
||||
- name: Install cross-compilation tools (Ubuntu x64)
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y musl-tools
|
||||
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
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
|
|
@ -105,11 +110,11 @@ jobs:
|
|||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: x86_64-unknown-linux-musl
|
||||
targets: x86_64-unknown-linux-musl,aarch64-unknown-linux-musl
|
||||
- name: Install musl tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y musl-tools
|
||||
sudo apt-get install -y musl-tools gcc-aarch64-linux-gnu
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build krust
|
||||
|
|
@ -136,6 +141,10 @@ jobs:
|
|||
run: |
|
||||
cd example/hello-krust
|
||||
krust build --no-push --image local.test/hello:latest ./
|
||||
- name: Test multi-arch build
|
||||
run: |
|
||||
cd example/hello-krust
|
||||
krust build --no-push --platform linux/amd64,linux/arm64 --image local.test/multiarch:latest ./
|
||||
|
||||
# Takes too long to run on CI, so it's commented out for now.
|
||||
# coverage:
|
||||
|
|
|
|||
18
README.md
18
README.md
|
|
@ -100,8 +100,14 @@ krust build example/hello-krust --no-push
|
|||
# Use a specific image name (overrides KRUST_REPO)
|
||||
krust build --image myregistry.io/myapp:v1.0
|
||||
|
||||
# Build for a different platform
|
||||
# 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
|
||||
```
|
||||
|
||||
### Build with custom cargo arguments
|
||||
|
|
@ -185,6 +191,7 @@ When determining the base image, krust uses this precedence order:
|
|||
- **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
|
||||
|
|
@ -260,6 +267,15 @@ 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
|
||||
|
||||
# Install Rust targets
|
||||
rustup target add x86_64-unknown-linux-musl
|
||||
rustup target add aarch64-unknown-linux-musl # Optional, for ARM64 support
|
||||
|
||||
# Install pre-commit hooks
|
||||
pip install pre-commit
|
||||
or
|
||||
|
|
|
|||
|
|
@ -25,9 +25,10 @@ pub enum Commands {
|
|||
#[arg(short, long, env = "KRUST_IMAGE")]
|
||||
image: Option<String>,
|
||||
|
||||
/// Target platform (e.g., linux/amd64, linux/arm64)
|
||||
#[arg(long, default_value = "linux/amd64")]
|
||||
platform: String,
|
||||
/// Target platforms (e.g., linux/amd64, linux/arm64)
|
||||
/// Can be specified multiple times or as a comma-separated list
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
platform: Option<Vec<String>>,
|
||||
|
||||
/// Skip pushing the image to the registry after building
|
||||
#[arg(long)]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ pub mod builder;
|
|||
pub mod cli;
|
||||
pub mod config;
|
||||
pub mod image;
|
||||
pub mod manifest;
|
||||
pub mod registry;
|
||||
|
||||
pub use anyhow::Result;
|
||||
|
|
|
|||
120
src/main.rs
120
src/main.rs
|
|
@ -5,6 +5,7 @@ use krust::{
|
|||
cli::{Cli, Commands},
|
||||
config::Config,
|
||||
image::ImageBuilder,
|
||||
manifest::{ImageIndex, ManifestDescriptor, Platform},
|
||||
registry::RegistryClient,
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
|
@ -57,33 +58,100 @@ async fn main() -> Result<()> {
|
|||
format!("{}/{}:latest", repo, project_name)
|
||||
};
|
||||
|
||||
// Build the Rust binary
|
||||
let target = get_rust_target_triple(&platform)?;
|
||||
let builder = RustBuilder::new(&project_path, &target).with_cargo_args(cargo_args);
|
||||
|
||||
let binary_path = builder.build()?;
|
||||
|
||||
// Build container image
|
||||
let image_builder = ImageBuilder::new(binary_path, base_image, platform.clone());
|
||||
|
||||
let (config_data, layer_data, manifest) = image_builder.build()?;
|
||||
|
||||
// Push by default unless --no-push is specified
|
||||
if !no_push {
|
||||
info!("Pushing image to registry...");
|
||||
let auth = oci_distribution::secrets::RegistryAuth::Anonymous;
|
||||
let mut registry_client = RegistryClient::new(auth)?;
|
||||
|
||||
let layers = vec![(layer_data, manifest.layers[0].media_type.clone())];
|
||||
|
||||
let digest_ref = registry_client
|
||||
.push_image(&image_ref, config_data, layers)
|
||||
.await?;
|
||||
|
||||
// Print only the digest reference to stdout
|
||||
println!("{}", digest_ref);
|
||||
// Determine platforms to build for
|
||||
let platforms = if let Some(platforms) = platform {
|
||||
platforms
|
||||
} else {
|
||||
info!("Successfully built image: {}", image_ref);
|
||||
// Default to common platforms
|
||||
vec!["linux/amd64".to_string(), "linux/arm64".to_string()]
|
||||
};
|
||||
|
||||
// Build for each platform
|
||||
let mut manifest_descriptors = Vec::new();
|
||||
let mut single_platform_digest = None;
|
||||
let auth = oci_distribution::secrets::RegistryAuth::Anonymous;
|
||||
let mut registry_client = RegistryClient::new(auth)?;
|
||||
|
||||
for platform_str in &platforms {
|
||||
info!("Building for platform: {}", platform_str);
|
||||
|
||||
// Build the Rust binary for this platform
|
||||
let target = get_rust_target_triple(platform_str)?;
|
||||
let builder =
|
||||
RustBuilder::new(&project_path, &target).with_cargo_args(cargo_args.clone());
|
||||
|
||||
let binary_path = builder.build()?;
|
||||
|
||||
// Build container image for this platform
|
||||
let image_builder =
|
||||
ImageBuilder::new(binary_path, base_image.clone(), platform_str.clone());
|
||||
|
||||
let (config_data, layer_data, manifest) = image_builder.build()?;
|
||||
|
||||
// Push platform-specific image if not --no-push
|
||||
if !no_push {
|
||||
info!("Pushing image for platform: {}", platform_str);
|
||||
|
||||
let layers = vec![(layer_data, manifest.layers[0].media_type.clone())];
|
||||
|
||||
// For single platform, push to the main tag
|
||||
let push_ref = if platforms.len() == 1 {
|
||||
image_ref.clone()
|
||||
} else {
|
||||
// For multi-platform, use platform-specific tag
|
||||
format!("{}-{}", image_ref, platform_str.replace('/', "-"))
|
||||
};
|
||||
|
||||
let digest_ref = registry_client
|
||||
.push_image(&push_ref, config_data, layers)
|
||||
.await?;
|
||||
|
||||
// Store digest for single platform builds
|
||||
if platforms.len() == 1 {
|
||||
single_platform_digest = Some(digest_ref.clone());
|
||||
}
|
||||
|
||||
// Parse platform string
|
||||
let parts: Vec<&str> = platform_str.split('/').collect();
|
||||
let (os, arch) = if parts.len() >= 2 {
|
||||
(parts[0].to_string(), parts[1].to_string())
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("Invalid platform format: {}", platform_str));
|
||||
};
|
||||
|
||||
// Add to manifest list
|
||||
manifest_descriptors.push(ManifestDescriptor {
|
||||
media_type: manifest.media_type.clone(),
|
||||
size: serde_json::to_vec(&manifest)?.len() as i64,
|
||||
digest: digest_ref.split('@').next_back().unwrap_or("").to_string(),
|
||||
platform: Platform {
|
||||
architecture: arch,
|
||||
os,
|
||||
variant: None,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create and push manifest list if not --no-push
|
||||
if !no_push && platforms.len() > 1 {
|
||||
info!("Creating and pushing manifest list...");
|
||||
let index = ImageIndex::new(manifest_descriptors);
|
||||
let _index_data = serde_json::to_vec(&index)?;
|
||||
|
||||
// TODO: Push manifest list to registry
|
||||
// For now, just print the multi-arch image reference
|
||||
println!("{}", image_ref);
|
||||
} else if !no_push && platforms.len() == 1 {
|
||||
// Single platform, print the digest reference
|
||||
if let Some(digest_ref) = single_platform_digest {
|
||||
println!("{}", digest_ref);
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
"Successfully built image for {} platform(s)",
|
||||
platforms.len()
|
||||
);
|
||||
info!("Skipping push (--no-push specified)");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
40
src/manifest.rs
Normal file
40
src/manifest.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// OCI Image Index (manifest list) for multi-arch support
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ImageIndex {
|
||||
#[serde(rename = "schemaVersion")]
|
||||
pub schema_version: i32,
|
||||
#[serde(rename = "mediaType")]
|
||||
pub media_type: String,
|
||||
pub manifests: Vec<ManifestDescriptor>,
|
||||
}
|
||||
|
||||
/// Descriptor for a platform-specific manifest in the index
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ManifestDescriptor {
|
||||
#[serde(rename = "mediaType")]
|
||||
pub media_type: String,
|
||||
pub size: i64,
|
||||
pub digest: String,
|
||||
pub platform: Platform,
|
||||
}
|
||||
|
||||
/// Platform information for a manifest
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Platform {
|
||||
pub architecture: String,
|
||||
pub os: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub variant: Option<String>,
|
||||
}
|
||||
|
||||
impl ImageIndex {
|
||||
pub fn new(manifests: Vec<ManifestDescriptor>) -> Self {
|
||||
Self {
|
||||
schema_version: 2,
|
||||
media_type: "application/vnd.oci.image.index.v1+json".to_string(),
|
||||
manifests,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -72,6 +72,8 @@ fn test_build_with_krust_repo_env() -> Result<()> {
|
|||
let mut cmd = Command::cargo_bin("krust")?;
|
||||
cmd.arg("build")
|
||||
.arg("--no-push")
|
||||
.arg("--platform")
|
||||
.arg("linux/amd64")
|
||||
.arg(".") // Explicitly pass current directory
|
||||
.env("KRUST_REPO", "test.local")
|
||||
.current_dir(&example_dir);
|
||||
|
|
@ -80,7 +82,7 @@ fn test_build_with_krust_repo_env() -> Result<()> {
|
|||
.success()
|
||||
.stderr(predicate::str::contains("Building Rust project"))
|
||||
.stderr(predicate::str::contains(
|
||||
"Successfully built image: test.local/hello-krust:latest",
|
||||
"Successfully built image for 1 platform(s)",
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -95,6 +97,8 @@ fn test_command_substitution_syntax() -> Result<()> {
|
|||
let output = cmd
|
||||
.arg("build")
|
||||
.arg("--no-push")
|
||||
.arg("--platform")
|
||||
.arg("linux/amd64")
|
||||
.arg("--image")
|
||||
.arg("test.local/hello:latest")
|
||||
.arg(".") // Explicitly pass current directory
|
||||
|
|
@ -123,6 +127,8 @@ fn test_verbose_logging() -> Result<()> {
|
|||
cmd.arg("--verbose")
|
||||
.arg("build")
|
||||
.arg("--no-push")
|
||||
.arg("--platform")
|
||||
.arg("linux/amd64")
|
||||
.arg("--image")
|
||||
.arg("test.local/hello:latest")
|
||||
.arg(".") // Explicitly pass current directory
|
||||
|
|
@ -155,6 +161,8 @@ fn test_full_build_and_run_workflow() -> Result<()> {
|
|||
let mut cmd = Command::cargo_bin("krust")?;
|
||||
let output = cmd
|
||||
.arg("build")
|
||||
.arg("--platform")
|
||||
.arg("linux/amd64")
|
||||
.arg(".") // Explicitly pass current directory
|
||||
.env("KRUST_REPO", "ttl.sh/krust-test")
|
||||
.current_dir(&example_dir)
|
||||
|
|
@ -176,3 +184,78 @@ fn test_full_build_and_run_workflow() -> Result<()> {
|
|||
assert!(docker_stdout.contains("Hello from krust example!"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_platform_build() -> Result<()> {
|
||||
let example_dir = env::current_dir()?.join("example").join("hello-krust");
|
||||
|
||||
let mut cmd = Command::cargo_bin("krust")?;
|
||||
cmd.arg("build")
|
||||
.arg("--no-push")
|
||||
.arg("--platform")
|
||||
.arg("linux/amd64")
|
||||
.arg("--image")
|
||||
.arg("test.local/single-platform:latest")
|
||||
.arg(".")
|
||||
.current_dir(&example_dir);
|
||||
|
||||
cmd.assert().success().stderr(predicate::str::contains(
|
||||
"Building for platform: linux/amd64",
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_platform_build() -> Result<()> {
|
||||
// Check if we have the ARM64 target installed
|
||||
let has_arm64_target = StdCommand::new("rustup")
|
||||
.args(&["target", "list", "--installed"])
|
||||
.output()
|
||||
.map(|output| {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
stdout.contains("aarch64-unknown-linux-musl")
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
let example_dir = env::current_dir()?.join("example").join("hello-krust");
|
||||
|
||||
let mut cmd = Command::cargo_bin("krust")?;
|
||||
|
||||
if has_arm64_target {
|
||||
// Test with both platforms if ARM64 is available
|
||||
cmd.arg("build")
|
||||
.arg("--no-push")
|
||||
.arg("--platform")
|
||||
.arg("linux/amd64,linux/arm64")
|
||||
.arg("--image")
|
||||
.arg("test.local/multi-platform:latest")
|
||||
.arg(".")
|
||||
.current_dir(&example_dir);
|
||||
|
||||
cmd.assert()
|
||||
.success()
|
||||
.stderr(predicate::str::contains(
|
||||
"Building for platform: linux/amd64",
|
||||
))
|
||||
.stderr(predicate::str::contains(
|
||||
"Building for platform: linux/arm64",
|
||||
));
|
||||
} else {
|
||||
// Test with multiple x64 platforms (same platform twice to test the multi-platform flow)
|
||||
cmd.arg("build")
|
||||
.arg("--no-push")
|
||||
.arg("--platform")
|
||||
.arg("linux/amd64")
|
||||
.arg("--platform")
|
||||
.arg("linux/amd64")
|
||||
.arg("--image")
|
||||
.arg("test.local/multi-platform:latest")
|
||||
.arg(".")
|
||||
.current_dir(&example_dir);
|
||||
|
||||
cmd.assert().success().stderr(predicate::str::contains(
|
||||
"Building for platform: linux/amd64",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue