use crate::core::store::layer_builder::{
build_layer, compute_dual_digest, write_oci_layout, LayerEntry,
};
use crate::core::types::{
ImageBuildPlan, LayerBuildResult, LayerStrategy, OciDescriptor, OciHistoryEntry,
OciImageConfig, OciIndex, OciLayerConfig, OciManifest,
};
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug)]
pub struct AssembledImage {
pub layout_dir: std::path::PathBuf,
pub manifest: OciManifest,
pub config: OciImageConfig,
pub layers: Vec<LayerBuildResult>,
pub total_size: u64,
}
pub fn assemble_image(
plan: &ImageBuildPlan,
layer_entries: &[Vec<LayerEntry>],
output_dir: &Path,
layer_config: &OciLayerConfig,
target_arch: Option<&str>,
) -> Result<AssembledImage, String> {
if plan.layers.len() != layer_entries.len() {
return Err(format!(
"layer count mismatch: plan has {} layers but {} entry sets provided",
plan.layers.len(),
layer_entries.len(),
));
}
let built_layers = build_all_layers(layer_entries, layer_config)?;
let diff_ids: Vec<String> = built_layers
.iter()
.map(|(r, _)| r.diff_id.clone())
.collect();
let layer_descriptors: Vec<OciDescriptor> = built_layers
.iter()
.map(|(r, _)| r.to_descriptor())
.collect();
let layer_results: Vec<LayerBuildResult> =
built_layers.iter().map(|(r, _)| r.clone()).collect();
let total_size: u64 = built_layers.iter().map(|(r, _)| r.compressed_size).sum();
let config = build_image_config(plan, target_arch, diff_ids);
let config_json =
serde_json::to_vec_pretty(&config).map_err(|e| format!("serialize config: {e}"))?;
let config_digest = compute_dual_digest(&config_json);
let manifest = OciManifest::new(config_digest.oci_digest(), layer_descriptors);
let manifest_json =
serde_json::to_vec_pretty(&manifest).map_err(|e| format!("serialize manifest: {e}"))?;
let manifest_digest = compute_dual_digest(&manifest_json);
write_oci_layout(output_dir, &built_layers, &config_json)?;
write_manifest_and_index(output_dir, &manifest_json, &manifest_digest.sha256)?;
write_docker_manifest(output_dir, plan, &layer_results, &config_digest.sha256)?;
debug_assert!(
output_dir.join("oci-layout").exists(),
"assemble_image: oci-layout missing"
);
debug_assert!(
output_dir.join("index.json").exists(),
"assemble_image: index.json missing"
);
debug_assert!(
manifest.layers.len() == layer_results.len(),
"assemble_image: manifest layer count mismatch"
);
Ok(AssembledImage {
layout_dir: output_dir.to_path_buf(),
manifest,
config,
layers: layer_results,
total_size,
})
}
fn build_all_layers(
layer_entries: &[Vec<LayerEntry>],
layer_config: &OciLayerConfig,
) -> Result<Vec<(LayerBuildResult, Vec<u8>)>, String> {
if layer_entries.len() > 1 {
let results: Vec<Result<(LayerBuildResult, Vec<u8>), String>> = std::thread::scope(|s| {
let handles: Vec<_> = layer_entries
.iter()
.enumerate()
.map(|(i, entries)| {
s.spawn(move || {
build_layer(entries, layer_config)
.map_err(|e| format!("layer {i} build failed: {e}"))
})
})
.collect();
handles
.into_iter()
.enumerate()
.map(|(i, h)| {
h.join()
.unwrap_or_else(|_| Err(format!("layer {i} build thread panicked")))
})
.collect()
});
results.into_iter().collect()
} else {
layer_entries
.iter()
.enumerate()
.map(|(i, entries)| {
build_layer(entries, layer_config)
.map_err(|e| format!("layer {i} build failed: {e}"))
})
.collect()
}
}
fn build_image_config(
plan: &ImageBuildPlan,
target_arch: Option<&str>,
diff_ids: Vec<String>,
) -> OciImageConfig {
let history: Vec<OciHistoryEntry> = plan
.layers
.iter()
.map(|strategy| OciHistoryEntry {
created: None,
created_by: Some(strategy_description(strategy)),
empty_layer: false,
comment: None,
})
.collect();
let arch = target_arch.unwrap_or("amd64");
let mut config = OciImageConfig::for_arch(arch, "linux", diff_ids);
config.history = history;
if let Some(ref ep) = plan.entrypoint {
config.config.entrypoint = ep.clone();
}
for (k, v) in &plan.labels {
config.config.labels.insert(k.clone(), v.clone());
}
config
}
fn write_manifest_and_index(
output_dir: &Path,
manifest_json: &[u8],
manifest_hex: &str,
) -> Result<(), String> {
std::fs::write(
output_dir.join(format!("blobs/sha256/{manifest_hex}")),
manifest_json,
)
.map_err(|e| format!("write manifest blob: {e}"))?;
let index = OciIndex::single(OciDescriptor {
media_type: "application/vnd.oci.image.manifest.v1+json".into(),
digest: format!("sha256:{manifest_hex}"),
size: manifest_json.len() as u64,
annotations: HashMap::new(),
});
let index_json =
serde_json::to_vec_pretty(&index).map_err(|e| format!("serialize index: {e}"))?;
std::fs::write(output_dir.join("index.json"), &index_json)
.map_err(|e| format!("write index.json: {e}"))
}
fn write_docker_manifest(
output_dir: &Path,
plan: &ImageBuildPlan,
layer_results: &[LayerBuildResult],
config_hex: &str,
) -> Result<(), String> {
let docker_layers: Vec<String> = layer_results
.iter()
.map(|r| {
let hex = r.digest.strip_prefix("sha256:").unwrap_or(&r.digest);
format!("blobs/sha256/{hex}")
})
.collect();
let docker_manifest = serde_json::json!([{
"RepoTags": [&plan.tag],
"Config": format!("blobs/sha256/{config_hex}"),
"Layers": docker_layers,
}]);
std::fs::write(
output_dir.join("manifest.json"),
serde_json::to_vec_pretty(&docker_manifest)
.map_err(|e| format!("serialize docker manifest: {e}"))?,
)
.map_err(|e| format!("write manifest.json: {e}"))
}
fn strategy_description(strategy: &LayerStrategy) -> String {
match strategy {
LayerStrategy::Packages { names } => format!("forjar: packages {}", names.join(", ")),
LayerStrategy::Files { paths } => format!("forjar: files {}", paths.join(", ")),
LayerStrategy::Build { command, .. } => format!("forjar: build {command}"),
LayerStrategy::Derivation { store_path } => format!("forjar: derivation {store_path}"),
}
}