1
0
Fork 0
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:
Jason Hall 2025-06-08 00:28:39 -04:00 committed by GitHub
commit 725b43f0e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 370 additions and 10 deletions

View file

@ -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

View file

@ -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

View file

@ -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),
}
}

View file

@ -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());
}
}

View file

@ -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);

View file

@ -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)> {

View file

@ -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(())
}