From 1f2db16bdefe439f003b640a731fbfdff780e483 Mon Sep 17 00:00:00 2001 From: Mikhail Katychev Date: Fri, 24 Jul 2026 15:46:14 -0500 Subject: [PATCH] fix(oci): handle title metadata --- Cargo.lock | 112 ++++---------------- Cargo.toml | 2 +- crates/wasm-pkg-client/src/oci/publisher.rs | 29 +++-- crates/wkg/src/oci.rs | 15 ++- crates/wkg/tests/e2e.rs | 74 +++++++++++++ 5 files changed, 130 insertions(+), 102 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 11a9d82..2f938d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1864,9 +1864,9 @@ dependencies = [ [[package]] name = "oci-wasm" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efc8835b4a949024f418e8a7f25db76038da0d3d670a1507a6e346dd06ae7ccb" +checksum = "87689298bd74f0f2675fcac99956a34c31098ca3bdced3d7635e71dc03c5ca21" dependencies = [ "anyhow", "chrono", @@ -1875,8 +1875,8 @@ dependencies = [ "serde_json", "sha2 0.11.0", "tokio", - "wit-component 0.252.0", - "wit-parser 0.252.0", + "wit-component", + "wit-parser", ] [[package]] @@ -3590,16 +3590,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.252.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8185ae345fa5687c054626ff9a50e7089797a343d9904d1dc9820eb4c4d3196f" -dependencies = [ - "leb128fmt", - "wasmparser 0.252.0", -] - [[package]] name = "wasm-encoder" version = "0.253.0" @@ -3607,19 +3597,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59972d6cd272259de647b7c1f1912e45e289c75ffd4be04e10695507cd7e1b59" dependencies = [ "leb128fmt", - "wasmparser 0.253.0", -] - -[[package]] -name = "wasm-metadata" -version = "0.252.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b7e08e02a3cd55bf778009d4cd6faae50da011f293644daf78a531a32d6d142" -dependencies = [ - "anyhow", - "indexmap 2.14.0", - "wasm-encoder 0.252.0", - "wasmparser 0.252.0", + "wasmparser", ] [[package]] @@ -3637,8 +3615,8 @@ dependencies = [ "serde_json", "spdx", "url", - "wasm-encoder 0.253.0", - "wasmparser 0.253.0", + "wasm-encoder", + "wasmparser", ] [[package]] @@ -3671,10 +3649,10 @@ dependencies = [ "tracing", "tracing-subscriber", "url", - "wasm-metadata 0.253.0", + "wasm-metadata", "wasm-pkg-common", - "wit-component 0.253.0", - "wit-parser 0.253.0", + "wit-component", + "wit-parser", ] [[package]] @@ -3716,12 +3694,12 @@ dependencies = [ "tokio-util", "toml", "tracing", - "wasm-metadata 0.253.0", + "wasm-metadata", "wasm-pkg-client", "wasm-pkg-common", "windows-sys 0.59.0", - "wit-component 0.253.0", - "wit-parser 0.253.0", + "wit-component", + "wit-parser", ] [[package]] @@ -3737,18 +3715,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.252.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3eb099dcadcde5be9eef55e3a337128efd4e44b4c93122487e4d2e4e1c6627c" -dependencies = [ - "bitflags", - "hashbrown 0.17.1", - "indexmap 2.14.0", - "semver", -] - [[package]] name = "wasmparser" version = "0.253.0" @@ -3771,7 +3737,7 @@ dependencies = [ "leb128fmt", "memchr", "unicode-width", - "wasm-encoder 0.253.0", + "wasm-encoder", ] [[package]] @@ -4111,25 +4077,6 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-component" -version = "0.252.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76db0662b590f45d33d0e363fa13539a5a1eecd35d5a12fe208c335461c1053d" -dependencies = [ - "anyhow", - "bitflags", - "indexmap 2.14.0", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder 0.252.0", - "wasm-metadata 0.252.0", - "wasmparser 0.252.0", - "wit-parser 0.252.0", -] - [[package]] name = "wit-component" version = "0.253.0" @@ -4143,30 +4090,11 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder 0.253.0", - "wasm-metadata 0.253.0", - "wasmparser 0.253.0", + "wasm-encoder", + "wasm-metadata", + "wasmparser", "wat", - "wit-parser 0.253.0", -] - -[[package]] -name = "wit-parser" -version = "0.252.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4266bea110371c620ccf3201c5023676046bc4556e5c7cfb5d500bda5ebc162d" -dependencies = [ - "anyhow", - "hashbrown 0.17.1", - "id-arena", - "indexmap 2.14.0", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-ident", - "wasmparser 0.252.0", + "wit-parser", ] [[package]] @@ -4185,7 +4113,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-ident", - "wasmparser 0.253.0", + "wasmparser", ] [[package]] @@ -4211,7 +4139,7 @@ dependencies = [ "wasm-pkg-client", "wasm-pkg-common", "wasm-pkg-core", - "wit-component 0.253.0", + "wit-component", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a5721d4..1df382f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ indexmap = "2.5" oci-client = { version = "0.17", default-features = false, features = [ "rustls-tls", ] } -oci-wasm = { version = "0.5", default-features = false, features = [ +oci-wasm = { version = "0.6", default-features = false, features = [ "rustls-tls", ] } petgraph = "0.8.3" diff --git a/crates/wasm-pkg-client/src/oci/publisher.rs b/crates/wasm-pkg-client/src/oci/publisher.rs index 2a6a715..62acaf5 100644 --- a/crates/wasm-pkg-client/src/oci/publisher.rs +++ b/crates/wasm-pkg-client/src/oci/publisher.rs @@ -1,6 +1,14 @@ use std::collections::BTreeMap; -use oci_client::{Reference, RegistryOperation}; +use oci_client::{ + Reference, RegistryOperation, + annotations::{ + ORG_OPENCONTAINERS_IMAGE_AUTHORS, ORG_OPENCONTAINERS_IMAGE_DESCRIPTION, + ORG_OPENCONTAINERS_IMAGE_LICENSES, ORG_OPENCONTAINERS_IMAGE_SOURCE, + ORG_OPENCONTAINERS_IMAGE_TITLE, ORG_OPENCONTAINERS_IMAGE_URL, + ORG_OPENCONTAINERS_IMAGE_VERSION, + }, +}; use tokio::io::AsyncReadExt; use crate::publisher::PackagePublisher; @@ -26,39 +34,44 @@ impl PackagePublisher for OciBackend { crate::Error::InvalidComponent(anyhow::anyhow!("Unable to parse WASM: {e}")) })?; let meta = payload.metadata(); - let (config, layer) = oci_wasm::WasmConfig::from_raw_component(buf, None) + let (config, mut layer) = oci_wasm::WasmConfig::from_raw_component(buf, None) .map_err(crate::Error::InvalidComponent)?; + // Set the layer title so OCI tools can name the file on disk + layer.annotations = Some(BTreeMap::from_iter([( + ORG_OPENCONTAINERS_IMAGE_TITLE.to_string(), + format!("{}.wasm", package.name()), + )])); let mut annotations = BTreeMap::from_iter([( - "org.opencontainers.image.version".to_string(), + ORG_OPENCONTAINERS_IMAGE_VERSION.to_string(), version.to_string(), )]); if let Some(desc) = &meta.description { annotations.insert( - "org.opencontainers.image.description".to_string(), + ORG_OPENCONTAINERS_IMAGE_DESCRIPTION.to_string(), desc.to_string(), ); } if let Some(licenses) = &meta.licenses { annotations.insert( - "org.opencontainers.image.licenses".to_string(), + ORG_OPENCONTAINERS_IMAGE_LICENSES.to_string(), licenses.to_string(), ); } if let Some(source) = &meta.source { annotations.insert( - "org.opencontainers.image.source".to_string(), + ORG_OPENCONTAINERS_IMAGE_SOURCE.to_string(), source.to_string(), ); } if let Some(homepage) = &meta.homepage { annotations.insert( - "org.opencontainers.image.url".to_string(), + ORG_OPENCONTAINERS_IMAGE_URL.to_string(), homepage.to_string(), ); } if let Some(authors) = &meta.authors { annotations.insert( - "org.opencontainers.image.authors".to_string(), + ORG_OPENCONTAINERS_IMAGE_AUTHORS.to_string(), authors.to_string(), ); } diff --git a/crates/wkg/src/oci.rs b/crates/wkg/src/oci.rs index a21670a..c19c9fe 100644 --- a/crates/wkg/src/oci.rs +++ b/crates/wkg/src/oci.rs @@ -5,6 +5,7 @@ use clap::{Args, Subcommand}; use docker_credential::DockerCredential; use oci_client::{ Reference, + annotations::ORG_OPENCONTAINERS_IMAGE_TITLE, client::{ClientConfig, ClientProtocol, PushResponse}, secrets::RegistryAuth, }; @@ -146,10 +147,22 @@ fn parse_key_val(s: &str) -> anyhow::Result<(String, String)> { impl PushArgs { pub async fn run(self) -> anyhow::Result<()> { let client = get_client(self.common); - let (conf, layer) = WasmConfig::from_component(&self.file, self.author) + let (conf, mut layer) = WasmConfig::from_component(&self.file, self.author) .await .context("Unable to parse component")?; + let title = self + .file + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| format!("{}.wasm", self.reference.repository().replace('/', "_"))); + // Set the layer title so OCI tools can name the file on disk + // TODO: unify with OciBackend::publish + layer.annotations = Some(std::collections::BTreeMap::from_iter([( + ORG_OPENCONTAINERS_IMAGE_TITLE.to_string(), + title, + )])); + let annotations = match self.annotation.len() { 0 => None, _ => Some(self.annotation.into_iter().collect()), diff --git a/crates/wkg/tests/e2e.rs b/crates/wkg/tests/e2e.rs index 6fbcfb6..29858d1 100644 --- a/crates/wkg/tests/e2e.rs +++ b/crates/wkg/tests/e2e.rs @@ -52,6 +52,22 @@ async fn build_and_publish_with_metadata() { panic!("OciManifest should be an image manifest"); }; + let layer = manifest + .layers + .first() + .expect("OciManifest should have at least one layer"); + let layer_annotations = layer + .annotations + .as_ref() + .expect("Layer should have annotations"); + assert_eq!( + layer_annotations + .get(oci_client::annotations::ORG_OPENCONTAINERS_IMAGE_TITLE) + .expect("layer missing image.title"), + "http.wasm", + "layer title should be the package name with .wasm suffix", + ); + let annotations = manifest .annotations .expect("OciManifest should have annotations"); @@ -90,6 +106,64 @@ async fn build_and_publish_with_metadata() { ); } +#[cfg(feature = "docker-tests")] +#[tokio::test] +async fn oci_push_sets_layer_title() { + use oci_client::{Reference, client::ClientConfig, manifest::OciManifest}; + + let (_config, registry, _container) = common::start_registry().await; + + let fixture = common::load_fixture("wasi-http").await; + let status = fixture + .command() + .args(["wit", "build"]) + .status() + .await + .expect("Should be able to build wit package"); + assert!(status.success(), "Build should succeed"); + + let wasm_file = fixture.fixture_path.join("wasi:http@0.2.0.wasm"); + let image_ref = format!("{registry}/wasi/http:0.2.0"); + let status = fixture + .command() + .args(["oci", "push", "--insecure", ®istry.to_string()]) + .arg(&image_ref) + .arg(&wasm_file) + .status() + .await + .expect("Should be able to run wkg oci push"); + assert!(status.success(), "`wkg oci push` should succeed"); + + let client = oci_client::Client::new(ClientConfig { + protocol: oci_client::client::ClientProtocol::Http, + ..Default::default() + }); + let reference: Reference = image_ref.parse().unwrap(); + let (manifest, _) = client + .pull_manifest(&reference, &oci_client::secrets::RegistryAuth::Anonymous) + .await + .expect("Should be able to fetch manifest"); + let manifest = match manifest { + OciManifest::Image(m) => m, + _ => panic!("OciManifest should be an image manifest"), + }; + let layer = manifest + .layers + .first() + .expect("manifest should have a wasm layer"); + let layer_annotations = layer + .annotations + .as_ref() + .expect("wasm layer should have annotations"); + assert_eq!( + layer_annotations + .get(oci_client::annotations::ORG_OPENCONTAINERS_IMAGE_TITLE) + .expect("layer should have image.title"), + "wasi:http@0.2.0.wasm", + "layer title should be the pushed file's basename", + ); +} + #[cfg(feature = "docker-tests")] #[tokio::test] async fn publish_workspace_packages() {