Write a complete OCI image layout under `out` (created if needed): `out/oci-layout`, `out/index.json`, and `out/blobs/sha256/<...>` blobs. `layers` are ordered base→top. Returns the manifest digest (`"sha256:..."`). The image config records the layers' uncompressed `diff_id`s; the manifest records the gzipped layer digests; `config.annotations` are passed through to the manifest.
(
out: &Path,
layers: &[OciLayer],
config: &OciImageConfig,
)
| 175 | /// records the gzipped layer digests; `config.annotations` are passed through to |
| 176 | /// the manifest. |
| 177 | pub fn write_image_layout( |
| 178 | out: &Path, |
| 179 | layers: &[OciLayer], |
| 180 | config: &OciImageConfig, |
| 181 | ) -> std::io::Result<String> { |
| 182 | let blobs = out.join("blobs").join("sha256"); |
| 183 | std::fs::create_dir_all(&blobs)?; |
| 184 | |
| 185 | // Image config blob. diff_ids are the UNCOMPRESSED layer digests. |
| 186 | let diff_ids: Vec<String> = layers.iter().map(|l| l.diff_id.clone()).collect(); |
| 187 | let history: Vec<serde_json::Value> = layers |
| 188 | .iter() |
| 189 | .map(|_| serde_json::json!({ "created_by": "atomic sandbox" })) |
| 190 | .collect(); |
| 191 | let config_json = serde_json::json!({ |
| 192 | "architecture": config.architecture, |
| 193 | "os": config.os, |
| 194 | "config": { |
| 195 | "Env": config.env, |
| 196 | "Entrypoint": config.entrypoint, |
| 197 | }, |
| 198 | "rootfs": { |
| 199 | "type": "layers", |
| 200 | "diff_ids": diff_ids, |
| 201 | }, |
| 202 | "history": history, |
| 203 | }); |
| 204 | let config_bytes = json_to_vec(&config_json)?; |
| 205 | let config_hex = sha256_hex(&config_bytes); |
| 206 | std::fs::write(blobs.join(&config_hex), &config_bytes)?; |
| 207 | let config_digest = format!("sha256:{config_hex}"); |
| 208 | |
| 209 | // Layer blobs (the gzipped bytes), keyed by their gzipped digest. |
| 210 | for layer in layers { |
| 211 | std::fs::write(blobs.join(digest_hex(&layer.digest)), &layer.tar_gz)?; |
| 212 | } |
| 213 | |
| 214 | // Manifest blob. Layer digests here are the GZIPPED digests. |
| 215 | let manifest_layers: Vec<serde_json::Value> = layers |
| 216 | .iter() |
| 217 | .map(|l| { |
| 218 | serde_json::json!({ |
| 219 | "mediaType": MEDIA_TYPE_LAYER, |
| 220 | "digest": l.digest, |
| 221 | "size": l.size, |
| 222 | }) |
| 223 | }) |
| 224 | .collect(); |
| 225 | let manifest_json = serde_json::json!({ |
| 226 | "schemaVersion": 2, |
| 227 | "mediaType": MEDIA_TYPE_MANIFEST, |
| 228 | "config": { |
| 229 | "mediaType": MEDIA_TYPE_CONFIG, |
| 230 | "digest": config_digest, |
| 231 | "size": config_bytes.len(), |
| 232 | }, |
| 233 | "layers": manifest_layers, |
| 234 | "annotations": config.annotations, |