mirror of
https://github.com/imjasonh/krust
synced 2026-07-08 06:45:32 +00:00
Merge pull request #4 from imjasonh/feat/detect-base-image-platforms
feat: Add automatic platform detection from base image
This commit is contained in:
commit
725b43f0e2
7 changed files with 370 additions and 10 deletions
61
.github/workflows/ci.yml
vendored
61
.github/workflows/ci.yml
vendored
|
|
@ -104,7 +104,6 @@ jobs:
|
|||
integration:
|
||||
name: Integration Test
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test, fmt, clippy] # Only run after other tests pass
|
||||
services:
|
||||
registry:
|
||||
image: registry:2
|
||||
|
|
@ -166,6 +165,66 @@ jobs:
|
|||
# Run the image - should automatically select the right architecture
|
||||
docker run --rm $IMAGE_REF
|
||||
|
||||
# Test extended platform support
|
||||
extended-platforms:
|
||||
name: Extended Platform Support
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: x86_64-unknown-linux-musl,i686-unknown-linux-musl,aarch64-unknown-linux-musl,armv7-unknown-linux-musleabihf
|
||||
- name: Install cross-compilation tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y musl-tools gcc-multilib
|
||||
- name: Setup cargo config
|
||||
run: |
|
||||
mkdir -p .cargo
|
||||
cat > .cargo/config.toml << 'EOF'
|
||||
[target.x86_64-unknown-linux-musl]
|
||||
linker = "x86_64-linux-musl-gcc"
|
||||
|
||||
[target.i686-unknown-linux-musl]
|
||||
linker = "musl-gcc"
|
||||
rustflags = ["-C", "target-cpu=i686"]
|
||||
|
||||
[target.aarch64-unknown-linux-musl]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
|
||||
[target.armv7-unknown-linux-musleabihf]
|
||||
linker = "arm-linux-gnueabihf-gcc"
|
||||
EOF
|
||||
- name: Build krust
|
||||
run: cargo build --release
|
||||
- name: Test multi-platform build with Alpine base
|
||||
run: |
|
||||
# Create a test project that uses Alpine (which supports many platforms)
|
||||
mkdir -p test-alpine-platforms/src
|
||||
cat > test-alpine-platforms/Cargo.toml << 'EOF'
|
||||
[package]
|
||||
name = "test-alpine"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[package.metadata.krust]
|
||||
base-image = "alpine:latest"
|
||||
EOF
|
||||
cat > test-alpine-platforms/src/main.rs << 'EOF'
|
||||
fn main() {
|
||||
println!("Hello from Alpine multi-platform test!");
|
||||
}
|
||||
EOF
|
||||
|
||||
# Test that platform detection works (even if we can't build all platforms)
|
||||
cd test-alpine-platforms
|
||||
../target/release/krust build --no-push --image test.local/alpine-test:latest . 2>&1 | tee build.log
|
||||
|
||||
# Verify platform detection happened
|
||||
grep -q "Detecting available platforms from base image: alpine:latest" build.log
|
||||
grep -q "Found platforms:" build.log
|
||||
|
||||
# Takes too long to run on CI, so it's commented out for now.
|
||||
# coverage:
|
||||
# name: Code coverage
|
||||
|
|
|
|||
61
README.md
61
README.md
|
|
@ -40,8 +40,16 @@ rustup target add x86_64-unknown-linux-musl
|
|||
# For linux/arm64
|
||||
rustup target add aarch64-unknown-linux-musl
|
||||
|
||||
# For linux/arm/v7
|
||||
rustup target add armv7-unknown-linux-musleabihf
|
||||
# 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
|
||||
|
|
@ -52,6 +60,9 @@ 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]
|
||||
|
|
@ -59,6 +70,11 @@ 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
|
||||
```
|
||||
|
||||
|
|
@ -109,7 +125,8 @@ 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
|
||||
# Default behavior detects platforms from base image
|
||||
# If the base image supports multiple platforms, krust will build for all of them
|
||||
krust build
|
||||
```
|
||||
|
||||
|
|
@ -124,6 +141,11 @@ krust build -- --features=prod
|
|||
- `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
|
||||
|
||||
|
|
@ -135,6 +157,23 @@ krust always pushes OCI image indexes (manifest lists) for consistency:
|
|||
|
||||
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:
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
|
|
@ -302,9 +341,21 @@ cd krust
|
|||
brew install messense/macos-cross-toolchains/x86_64-unknown-linux-musl
|
||||
brew install messense/macos-cross-toolchains/aarch64-unknown-linux-musl
|
||||
|
||||
# Install Rust targets
|
||||
# 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 # Optional, for ARM64 support
|
||||
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
|
||||
|
|
|
|||
|
|
@ -191,6 +191,11 @@ pub fn get_rust_target_triple(platform: &str) -> Result<String> {
|
|||
"linux/amd64" => Ok("x86_64-unknown-linux-musl".to_string()),
|
||||
"linux/arm64" => Ok("aarch64-unknown-linux-musl".to_string()),
|
||||
"linux/arm/v7" => Ok("armv7-unknown-linux-musleabihf".to_string()),
|
||||
"linux/arm/v6" => Ok("arm-unknown-linux-musleabihf".to_string()),
|
||||
"linux/386" => Ok("i686-unknown-linux-musl".to_string()),
|
||||
"linux/ppc64le" => Ok("powerpc64le-unknown-linux-musl".to_string()),
|
||||
"linux/s390x" => Ok("s390x-unknown-linux-musl".to_string()),
|
||||
"linux/riscv64" => Ok("riscv64gc-unknown-linux-musl".to_string()),
|
||||
_ => anyhow::bail!("Unsupported platform: {}", platform),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,26 @@ mod tests {
|
|||
get_rust_target_triple("linux/arm/v7").unwrap(),
|
||||
"armv7-unknown-linux-musleabihf"
|
||||
);
|
||||
assert_eq!(
|
||||
get_rust_target_triple("linux/arm/v6").unwrap(),
|
||||
"arm-unknown-linux-musleabihf"
|
||||
);
|
||||
assert_eq!(
|
||||
get_rust_target_triple("linux/386").unwrap(),
|
||||
"i686-unknown-linux-musl"
|
||||
);
|
||||
assert_eq!(
|
||||
get_rust_target_triple("linux/ppc64le").unwrap(),
|
||||
"powerpc64le-unknown-linux-musl"
|
||||
);
|
||||
assert_eq!(
|
||||
get_rust_target_triple("linux/s390x").unwrap(),
|
||||
"s390x-unknown-linux-musl"
|
||||
);
|
||||
assert_eq!(
|
||||
get_rust_target_triple("linux/riscv64").unwrap(),
|
||||
"riscv64gc-unknown-linux-musl"
|
||||
);
|
||||
assert!(get_rust_target_triple("windows/amd64").is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
29
src/main.rs
29
src/main.rs
|
|
@ -58,18 +58,39 @@ async fn main() -> Result<()> {
|
|||
format!("{}/{}:latest", repo, project_name)
|
||||
};
|
||||
|
||||
// Initialize registry client early for platform detection
|
||||
let auth = oci_distribution::secrets::RegistryAuth::Anonymous;
|
||||
let mut registry_client = RegistryClient::new(auth)?;
|
||||
|
||||
// Determine platforms to build for
|
||||
let platforms = if let Some(platforms) = platform {
|
||||
// Use explicitly specified platforms
|
||||
platforms
|
||||
} else {
|
||||
// Default to common platforms
|
||||
vec!["linux/amd64".to_string(), "linux/arm64".to_string()]
|
||||
// Detect platforms from base image
|
||||
info!(
|
||||
"Detecting available platforms from base image: {}",
|
||||
base_image
|
||||
);
|
||||
match registry_client.get_image_platforms(&base_image).await {
|
||||
Ok(detected_platforms) => {
|
||||
if detected_platforms.is_empty() {
|
||||
info!("No platforms detected, using defaults");
|
||||
vec!["linux/amd64".to_string(), "linux/arm64".to_string()]
|
||||
} else {
|
||||
info!("Detected platforms: {:?}", detected_platforms);
|
||||
detected_platforms
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
info!("Failed to detect platforms: {}. Using defaults.", e);
|
||||
vec!["linux/amd64".to_string(), "linux/arm64".to_string()]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Build for each platform
|
||||
let mut manifest_descriptors = Vec::new();
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -178,6 +178,87 @@ impl RegistryClient {
|
|||
|
||||
Ok(image_ref)
|
||||
}
|
||||
|
||||
/// Fetch the manifest for an image and extract available platforms
|
||||
pub async fn get_image_platforms(&mut self, image_ref: &str) -> Result<Vec<String>> {
|
||||
let reference: Reference = image_ref
|
||||
.parse()
|
||||
.context("Failed to parse image reference")?;
|
||||
|
||||
debug!("Fetching manifest for {}", reference);
|
||||
|
||||
// Pull the manifest
|
||||
let (manifest, _) = self
|
||||
.client
|
||||
.pull_manifest(&reference, &self.auth)
|
||||
.await
|
||||
.context("Failed to pull manifest")?;
|
||||
|
||||
// Parse platforms based on manifest type
|
||||
match manifest {
|
||||
OciManifest::Image(_) => {
|
||||
// Single platform image - we need to fetch the config to determine platform
|
||||
debug!("Single platform image detected");
|
||||
// For now, we'll assume it's linux/amd64 if we can't determine
|
||||
// In a real implementation, we'd fetch the config blob
|
||||
Ok(vec!["linux/amd64".to_string()])
|
||||
}
|
||||
OciManifest::ImageIndex(index) => {
|
||||
// Multi-platform image - extract platforms from index
|
||||
debug!(
|
||||
"Multi-platform image detected with {} manifests",
|
||||
index.manifests.len()
|
||||
);
|
||||
let mut platforms: Vec<String> = index
|
||||
.manifests
|
||||
.iter()
|
||||
.filter_map(|m| {
|
||||
m.platform.as_ref().and_then(|p| {
|
||||
// Filter out invalid platforms
|
||||
if p.os == "unknown"
|
||||
|| p.architecture == "unknown"
|
||||
|| p.os.is_empty()
|
||||
|| p.architecture.is_empty()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut platform = format!("{}/{}", p.os, p.architecture);
|
||||
if let Some(variant) = &p.variant {
|
||||
if !variant.is_empty() {
|
||||
platform.push('/');
|
||||
platform.push_str(variant);
|
||||
}
|
||||
}
|
||||
// Normalize and filter platforms
|
||||
let normalized = match platform.as_str() {
|
||||
"linux/amd64" => Some("linux/amd64".to_string()),
|
||||
"linux/arm64" | "linux/arm64/v8" => Some("linux/arm64".to_string()),
|
||||
"linux/arm/v7" => Some("linux/arm/v7".to_string()),
|
||||
"linux/arm/v6" => Some("linux/arm/v6".to_string()),
|
||||
"linux/386" => Some("linux/386".to_string()),
|
||||
"linux/ppc64le" => Some("linux/ppc64le".to_string()),
|
||||
"linux/s390x" => Some("linux/s390x".to_string()),
|
||||
"linux/riscv64" => Some("linux/riscv64".to_string()),
|
||||
_ => {
|
||||
debug!("Skipping unsupported platform: {}", platform);
|
||||
None
|
||||
}
|
||||
};
|
||||
normalized
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Deduplicate platforms
|
||||
platforms.sort();
|
||||
platforms.dedup();
|
||||
|
||||
info!("Found platforms: {:?}", platforms);
|
||||
Ok(platforms)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_image_reference(image: &str) -> Result<(String, String, String)> {
|
||||
|
|
|
|||
|
|
@ -339,3 +339,126 @@ fn test_multi_arch_build_and_run() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_platform_detection_from_base_image() -> 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("--image")
|
||||
.arg("test.local/platform-detection:latest")
|
||||
.arg(".")
|
||||
.current_dir(&example_dir)
|
||||
.env("RUST_LOG", "info");
|
||||
|
||||
// The default base image (cgr.dev/chainguard/static:latest) supports multiple platforms
|
||||
// so we should see platform detection happening
|
||||
let output = cmd.output()?;
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
// We should always see platform detection
|
||||
assert!(stderr.contains("Detecting available platforms from base image"));
|
||||
assert!(stderr.contains("Detected platforms"));
|
||||
|
||||
// The build might fail if we don't have all cross-compilation toolchains
|
||||
// (e.g., on ARM runners trying to build for x86_64)
|
||||
if !output.status.success() {
|
||||
assert!(
|
||||
stderr.contains("linker")
|
||||
|| stderr.contains("target may not be installed")
|
||||
|| stderr.contains("cross-compilation"),
|
||||
"Build failed for unexpected reason: {}",
|
||||
stderr
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_explicit_platform_overrides_detection() -> Result<()> {
|
||||
let example_dir = env::current_dir()?.join("example").join("hello-krust");
|
||||
let platform = get_test_platform();
|
||||
|
||||
let mut cmd = Command::cargo_bin("krust")?;
|
||||
cmd.arg("build")
|
||||
.arg("--no-push")
|
||||
.arg("--platform")
|
||||
.arg(platform)
|
||||
.arg("--image")
|
||||
.arg("test.local/explicit-platform:latest")
|
||||
.arg(".")
|
||||
.current_dir(&example_dir)
|
||||
.env("RUST_LOG", "info");
|
||||
|
||||
// When platform is explicitly specified, we should NOT see platform detection
|
||||
let output = cmd.output()?;
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
assert!(output.status.success());
|
||||
assert!(!stderr.contains("Detecting available platforms from base image"));
|
||||
assert!(stderr.contains(&format!("Building for platform: {}", platform)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_alpine_base_image_many_platforms() -> Result<()> {
|
||||
// Create a temporary directory for this test
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let test_project_dir = temp_dir.path().join("test-alpine");
|
||||
std::fs::create_dir_all(&test_project_dir)?;
|
||||
|
||||
// Create a simple Cargo.toml with Alpine as base image
|
||||
let cargo_toml = r#"[package]
|
||||
name = "test-alpine"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
[package.metadata.krust]
|
||||
base-image = "alpine:latest"
|
||||
"#;
|
||||
std::fs::write(test_project_dir.join("Cargo.toml"), cargo_toml)?;
|
||||
|
||||
// Create src directory and main.rs
|
||||
std::fs::create_dir_all(test_project_dir.join("src"))?;
|
||||
std::fs::write(
|
||||
test_project_dir.join("src/main.rs"),
|
||||
r#"fn main() { println!("Hello from Alpine test!"); }"#,
|
||||
)?;
|
||||
|
||||
let mut cmd = Command::cargo_bin("krust")?;
|
||||
let output = cmd
|
||||
.arg("build")
|
||||
.arg("--no-push")
|
||||
.arg("--image")
|
||||
.arg("test.local/alpine-platforms:latest")
|
||||
.arg(".")
|
||||
.current_dir(&test_project_dir)
|
||||
.env("RUST_LOG", "info")
|
||||
.output()?;
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
// Alpine typically supports many platforms, but we might not have all the toolchains
|
||||
// So we check that platform detection happened
|
||||
assert!(stderr.contains("Detecting available platforms from base image: alpine:latest"));
|
||||
assert!(stderr.contains("Found platforms:") || stderr.contains("Detected platforms:"));
|
||||
|
||||
// The build might fail due to missing toolchains for some platforms, which is OK
|
||||
// We're mainly testing that platform detection works
|
||||
if !output.status.success() {
|
||||
// Check if it failed due to missing toolchains (expected)
|
||||
assert!(
|
||||
stderr.contains("target may not be installed")
|
||||
|| stderr.contains("linker")
|
||||
|| stderr.contains("cross-compilation"),
|
||||
"Build failed for unexpected reason: {}",
|
||||
stderr
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue