diff --git a/Cargo.lock b/Cargo.lock index a9bd2469..256e7c3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -558,6 +558,7 @@ dependencies = [ "wasi-preview1-component-adapter-provider", "wasm-metadata 0.227.1", "wasm-pkg-client", + "wasm-pkg-common", "wasmparser 0.227.1", "wasmprinter 0.227.1", "wat", @@ -2456,9 +2457,9 @@ dependencies = [ [[package]] name = "oci-wasm" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f147e207436277483c23cb8e55ccd039ee1657c6a8d19471a6de187da6973ef8" +checksum = "0609c917e8f4c44cd9d619a6e4741535d1cc199cfd995ad75580742bf6d624e8" dependencies = [ "anyhow", "chrono", @@ -2467,8 +2468,8 @@ dependencies = [ "serde_json", "sha2", "tokio", - "wit-component 0.219.1", - "wit-parser 0.219.1", + "wit-component 0.224.1", + "wit-parser 0.224.1", ] [[package]] @@ -4626,22 +4627,22 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.219.1" +version = "0.222.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29cbbd772edcb8e7d524a82ee8cef8dd046fc14033796a754c3ad246d019fa54" +checksum = "3432682105d7e994565ef928ccf5856cf6af4ba3dddebedb737f61caed70f956" dependencies = [ "leb128", - "wasmparser 0.219.1", + "wasmparser 0.222.0", ] [[package]] name = "wasm-encoder" -version = "0.222.0" +version = "0.224.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3432682105d7e994565ef928ccf5856cf6af4ba3dddebedb737f61caed70f956" +checksum = "1ab7a13a23790fe91ea4eb7526a1f3131001d874e3e00c2976c48861f2e82920" dependencies = [ "leb128", - "wasmparser 0.222.0", + "wasmparser 0.224.1", ] [[package]] @@ -4654,11 +4655,21 @@ dependencies = [ "wasmparser 0.227.1", ] +[[package]] +name = "wasm-encoder" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bc393c395cb621367ff02d854179882b9a351b4e0c93d1397e6090b53a5c2a" +dependencies = [ + "leb128fmt", + "wasmparser 0.235.0", +] + [[package]] name = "wasm-metadata" -version = "0.219.1" +version = "0.224.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af5a8e37a5e996861e1813f8de30911c47609c9ff51a7284f7dbd754dc3a9f3" +checksum = "c93c9e49fa2749be3c5ab28ad4be03167294915cd3b2ded3f04f760cef5cfb86" dependencies = [ "anyhow", "indexmap 2.7.0", @@ -4666,8 +4677,9 @@ dependencies = [ "serde_derive", "serde_json", "spdx", - "wasm-encoder 0.219.1", - "wasmparser 0.219.1", + "url", + "wasm-encoder 0.224.1", + "wasmparser 0.224.1", ] [[package]] @@ -4689,11 +4701,30 @@ dependencies = [ "wasmparser 0.227.1", ] +[[package]] +name = "wasm-metadata" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b055604ba04189d54b8c0ab2c2fc98848f208e103882d5c0b984f045d5ea4d20" +dependencies = [ + "anyhow", + "auditable-serde", + "flate2", + "indexmap 2.7.0", + "serde", + "serde_derive", + "serde_json", + "spdx", + "url", + "wasm-encoder 0.235.0", + "wasmparser 0.235.0", +] + [[package]] name = "wasm-pkg-client" -version = "0.9.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a191865bbb4982a938b5bd427560af8eafcaa2990770ed7049ffeaa05075c424" +checksum = "8337ee27b10ff71b17a1e31e569b7e9eb0267e1e79a414e8ee90fbc780081faa" dependencies = [ "anyhow", "async-trait", @@ -4704,6 +4735,7 @@ dependencies = [ "futures-util", "oci-client", "oci-wasm", + "reqwest", "secrecy", "serde", "serde_json", @@ -4718,23 +4750,22 @@ dependencies = [ "warg-client", "warg-crypto", "warg-protocol", - "wasm-metadata 0.219.1", + "wasm-metadata 0.235.0", "wasm-pkg-common", - "wit-component 0.219.1", + "wit-component 0.235.0", ] [[package]] name = "wasm-pkg-common" -version = "0.9.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2687dbb47e3bf1bdfa94ffb5e8983d374875df0a608d0cb63626a19660139048" +checksum = "7cab484707abc3826aaab22f19e0b73ec27114b3d14bc9fc7dadcd35f101438f" dependencies = [ "anyhow", "bytes", "etcetera", "futures-util", "http", - "reqwest", "semver", "serde", "serde_json", @@ -4742,7 +4773,6 @@ dependencies = [ "thiserror 1.0.69", "tokio", "toml", - "tracing", ] [[package]] @@ -4771,24 +4801,23 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.219.1" +version = "0.222.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c771866898879073c53b565a6c7b49953795159836714ac56a5befb581227c5" +checksum = "4adf50fde1b1a49c1add6a80d47aea500c88db70551805853aa8b88f3ea27ab5" dependencies = [ - "ahash", "bitflags", - "hashbrown 0.14.5", "indexmap 2.7.0", "semver", ] [[package]] name = "wasmparser" -version = "0.222.0" +version = "0.224.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4adf50fde1b1a49c1add6a80d47aea500c88db70551805853aa8b88f3ea27ab5" +checksum = "04f17a5917c2ddd3819e84c661fae0d6ba29d7b9c1f0e96c708c65a9c4188e11" dependencies = [ "bitflags", + "hashbrown 0.15.2", "indexmap 2.7.0", "semver", ] @@ -4806,6 +4835,18 @@ dependencies = [ "serde", ] +[[package]] +name = "wasmparser" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161296c618fa2d63f6ed5fffd1112937e803cb9ec71b32b01a76321555660917" +dependencies = [ + "bitflags", + "hashbrown 0.15.2", + "indexmap 2.7.0", + "semver", +] + [[package]] name = "wasmprinter" version = "0.2.80" @@ -5152,9 +5193,9 @@ dependencies = [ [[package]] name = "wit-component" -version = "0.219.1" +version = "0.224.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1673163c0cb14a6a19ddbf44dd4efe6f015ec1ebb8156710ac32501f19fba2" +checksum = "923637fe647372efbbb654757f8c884ba280924477e1d265eca7e35d4cdcea8b" dependencies = [ "anyhow", "bitflags", @@ -5163,10 +5204,10 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder 0.219.1", - "wasm-metadata 0.219.1", - "wasmparser 0.219.1", - "wit-parser 0.219.1", + "wasm-encoder 0.224.1", + "wasm-metadata 0.224.1", + "wasmparser 0.224.1", + "wit-parser 0.224.1", ] [[package]] @@ -5188,11 +5229,30 @@ dependencies = [ "wit-parser 0.227.1", ] +[[package]] +name = "wit-component" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a57a11109cc553396f89f3a38a158a97d0b1adaec113bd73e0f64d30fb601f" +dependencies = [ + "anyhow", + "bitflags", + "indexmap 2.7.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.235.0", + "wasm-metadata 0.235.0", + "wasmparser 0.235.0", + "wit-parser 0.235.0", +] + [[package]] name = "wit-parser" -version = "0.219.1" +version = "0.224.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a86f669283257e8e424b9a4fc3518e3ade0b95deb9fbc0f93a1876be3eda598" +checksum = "e3477d8d0acb530d76beaa8becbdb1e3face08929db275f39934963eb4f716f8" dependencies = [ "anyhow", "id-arena", @@ -5203,7 +5263,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser 0.219.1", + "wasmparser 0.224.1", ] [[package]] @@ -5224,6 +5284,24 @@ dependencies = [ "wasmparser 0.227.1", ] +[[package]] +name = "wit-parser" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a1f95a87d03a33e259af286b857a95911eb46236a0f726cbaec1227b3dfc67a" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.7.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.235.0", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index f0960ca0..555574ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ url = { workspace = true } wasi-preview1-component-adapter-provider = { workspace = true } wasm-metadata = { workspace = true } wasm-pkg-client = { workspace = true } +wasm-pkg-common = { workspace = true } wasmparser = { workspace = true } which = { workspace = true } wit-bindgen-core = { workspace = true } @@ -96,8 +97,8 @@ serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.115" tempfile = "3.10.1" tokio = { version = "1.44.2", default-features = false, features = [ - "macros", - "rt-multi-thread", + "macros", + "rt-multi-thread", ] } tokio-util = "0.7.10" toml_edit = { version = "0.22.9", features = ["serde"] } @@ -109,7 +110,8 @@ warg-protocol = "0.9.0" warg-server = "0.9.0" wasi-preview1-component-adapter-provider = "29.0.1" wasm-metadata = "0.227.0" -wasm-pkg-client = "0.9.0" +wasm-pkg-client = "0.11.0" +wasm-pkg-common = { version = "0.11.0", features = ["oci_extras"] } wasmparser = "0.227.0" wasmprinter = "0.227.0" wat = "1.220.0" diff --git a/src/commands/new.rs b/src/commands/new.rs index 12638234..2ab2d82f 100644 --- a/src/commands/new.rs +++ b/src/commands/new.rs @@ -1,12 +1,11 @@ -use std::{ - borrow::Cow, - fs, - path::{Path, PathBuf}, - process::Command, - sync::Arc, -}; - -use anyhow::{bail, Context, Result}; +use std::borrow::Cow; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::str::FromStr; +use std::sync::Arc; + +use anyhow::{bail, ensure, Context, Result}; use cargo_component_core::{ command::CommonOptions, registry::{Dependency, DependencyResolution, DependencyResolver, RegistryResolution}, @@ -16,23 +15,61 @@ use heck::ToKebabCase; use semver::VersionReq; use toml_edit::{table, value, DocumentMut, Item, Table, Value}; use wasm_pkg_client::caching::{CachingClient, FileCache}; +use wasm_pkg_client::{CustomConfig, PackageRef, Registry, RegistryMapping, RegistryMetadata}; -use crate::{ - config::Config, generate_bindings, generator::SourceGenerator, load_component_metadata, - load_metadata, metadata, metadata::DEFAULT_WIT_DIR, CargoArguments, -}; +use crate::config::Config; +use crate::generator::SourceGenerator; +use crate::metadata::DEFAULT_WIT_DIR; +use crate::{generate_bindings, load_component_metadata, load_metadata, metadata, CargoArguments}; const WIT_BINDGEN_RT_CRATE: &str = "wit-bindgen-rt"; -fn escape_wit(s: &str) -> Cow { - match s { - "use" | "type" | "func" | "u8" | "u16" | "u32" | "u64" | "s8" | "s16" | "s32" | "s64" - | "float32" | "float64" | "char" | "record" | "flags" | "variant" | "enum" | "union" - | "bool" | "string" | "option" | "result" | "future" | "stream" | "list" | "_" | "as" - | "from" | "static" | "interface" | "tuple" | "import" | "export" | "world" | "package" => { - Cow::Owned(format!("%{s}")) - } - _ => s.into(), +/// Name of a given package +struct PackageName<'a> { + /// Namespace of the package + namespace: String, + + /// Name of the package + name: String, + + /// Value that should be used when displaying the package name + display: Cow<'a, str>, +} + +impl<'a> PackageName<'a> { + /// Create a new package name + fn new(namespace: &str, name: Option<&'a str>, path: &'a Path) -> Result { + let (name, display) = match name { + Some(name) => (name.into(), name.into()), + None => ( + path.file_name().expect("invalid path").to_string_lossy(), + // `cargo new` prints the given path to the new package, so + // use the path for the display value. + path.as_os_str().to_string_lossy(), + ), + }; + + let namespace_kebab = namespace.to_kebab_case(); + ensure!( + !namespace_kebab.is_empty(), + "invalid component namespace `{namespace}`" + ); + + wit_parser::validate_id(&namespace_kebab).with_context(|| { + format!("component namespace `{namespace}` is not a legal WIT identifier") + })?; + + let name_kebab = name.to_kebab_case(); + ensure!(!name_kebab.is_empty(), "invalid component name `{name}`"); + + wit_parser::validate_id(&name_kebab) + .with_context(|| format!("component name `{name}` is not a legal WIT identifier"))?; + + Ok(Self { + namespace: namespace_kebab, + name: name_kebab, + display, + }) } } @@ -87,10 +124,20 @@ pub struct NewCommand { #[clap(long = "target", short = 't', value_name = "TARGET", requires = "lib")] pub target: Option, - /// Use the specified default registry when generating the package. + /// Registry to use as the default when generating the package + /// + /// (e.g. 'oci://ghcr.io') + /// NOTE: you may need to also specify --registry-ns-prefix #[clap(long = "registry", value_name = "REGISTRY")] pub registry: Option, + /// Namespace prefix to use with the custom registry provided, + /// most commonly used with an OCI registry (e.g. 'oci://ghcr.io') + /// + /// (e.g. 'bytecodealliance/') + #[clap(long = "registry-ns-prefix", value_name = "REGISTRY_NS_PREFIX")] + pub registry_ns_prefix: Option, + /// Disable the use of `rustfmt` when generating source code. #[clap(long = "no-rustfmt")] pub no_rustfmt: bool, @@ -100,55 +147,58 @@ pub struct NewCommand { pub path: PathBuf, } -struct PackageName<'a> { - namespace: String, - name: String, - display: Cow<'a, str>, -} - -impl<'a> PackageName<'a> { - fn new(namespace: &str, name: Option<&'a str>, path: &'a Path) -> Result { - let (name, display) = match name { - Some(name) => (name.into(), name.into()), - None => ( - path.file_name().expect("invalid path").to_string_lossy(), - // `cargo new` prints the given path to the new package, so - // use the path for the display value. - path.as_os_str().to_string_lossy(), - ), - }; - - let namespace_kebab = namespace.to_kebab_case(); - if namespace_kebab.is_empty() { - bail!("invalid component namespace `{namespace}`"); - } - - wit_parser::validate_id(&namespace_kebab).with_context(|| { - format!("component namespace `{namespace}` is not a legal WIT identifier") - })?; - - let name_kebab = name.to_kebab_case(); - if name_kebab.is_empty() { - bail!("invalid component name `{name}`"); - } - - wit_parser::validate_id(&name_kebab) - .with_context(|| format!("component name `{name}` is not a legal WIT identifier"))?; - - Ok(Self { - namespace: namespace_kebab, - name: name_kebab, - display, - }) - } -} - impl NewCommand { /// Executes the command. pub async fn exec(self) -> Result<()> { log::debug!("executing new command"); - let config = Config::new(self.common.new_terminal(), self.common.config.clone()).await?; + // Build configuration + let mut config = + Config::new(self.common.new_terminal(), self.common.config.clone()).await?; + + // Support OCI registries when resolving target worlds + match (self.target.as_ref(), self.registry.as_ref()) { + // Support specifying OCI registries with + (Some(target), Some(oci_uri)) if oci_uri.starts_with("oci://") => { + // Build registry & mapping configuration + let raw_registry = oci_uri.split_at(6).1; + let registry = + Registry::from_str(raw_registry).context("parsing provided registry")?; + + // Configure OCI metadata + let mut metadata = RegistryMetadata::default(); + metadata.preferred_protocol = Some("oci".into()); + metadata.set_oci_registry(Some(registry.clone().into())); + if let Some(ref raw_ns_prefix) = self.registry_ns_prefix { + // Ensure prefix, if provided, ends with '/' + let ns_prefix = if raw_ns_prefix.is_empty() || raw_ns_prefix.ends_with("/") { + raw_ns_prefix.into() + } else { + format!("{raw_ns_prefix}/") + }; + metadata.set_oci_namespace_prefix(Some(ns_prefix)) + } + + // Build registry mapping + log::debug!( + "using namespace registry [{raw_registry}] {} for target [{target}]", + self.registry_ns_prefix + .as_ref() + .map(|v| format!("(prefix {}", v)) + .unwrap_or_default(), + ); + let registry_mapping = RegistryMapping::Custom(CustomConfig { registry, metadata }); + + // Create an override for the given target package + config.pkg_config.set_package_registry_override( + PackageRef::from_str(target) + .with_context(|| format!("converting [{target}] to package ref"))?, + registry_mapping, + ); + } + // Ignore other cases + _ => {} + } let name = PackageName::new(&self.namespace, self.name.as_deref(), &self.path)?; @@ -161,9 +211,19 @@ impl NewCommand { Some(s) => Some(format!("{s}@{version}", version = VersionReq::STAR).parse()?), None => None, }; - let client = config.client(self.common.cache_dir.clone(), false).await?; - let target = self.resolve_target(Arc::clone(&client), target).await?; - let source = self.generate_source(&target).await?; + let client = config + .client(self.common.cache_dir.clone(), false) + .await + .context("building client")?; + + let target = self + .resolve_target(Arc::clone(&client), target) + .await + .context("resolving target world")?; + let source = self + .generate_source(&target) + .await + .context("generating source code")?; let mut command = self.new_command(); match command.status() { @@ -292,7 +352,32 @@ impl NewCommand { } } - component["dependencies"] = Item::Table(Table::new()); + // Build table of dependencies + let mut dependencies = Table::new(); + if let Some(target) = &self.target { + let pkg_ref = PackageRef::from_str(target) + .with_context(|| format!("converting [{target}] to package ref"))?; + if let Some(meta) = config.pkg_config.package_registry_override(&pkg_ref) { + // TODO: create anon registry mapping + let registry = Some(String::from("anon_mapping")); + // TODO: properly determine version version + let version = + VersionReq::parse("0.1.0").context("failed to parse versoin requirement")?; + + // TODO: fix TOML configuration building + // dependencies.insert( + // target, + // value(Dependency::Package( + // cargo_component_core::registry::RegistryPackage { + // name: Some(pkg_ref.clone()), + // version, + // registry, + // }, + // )), + // ); + } + } + component["dependencies"] = Item::Table(dependencies); if self.proxy { component["proxy"] = value(true); @@ -552,3 +637,16 @@ world example {{ } } } + +/// Escape an identifier used in WIT, adding the `%` prefix if it's a known identifier +fn escape_wit(s: &str) -> Cow { + match s { + "use" | "type" | "func" | "u8" | "u16" | "u32" | "u64" | "s8" | "s16" | "s32" | "s64" + | "float32" | "float64" | "char" | "record" | "flags" | "variant" | "enum" | "union" + | "bool" | "string" | "option" | "result" | "future" | "stream" | "list" | "_" | "as" + | "from" | "static" | "interface" | "tuple" | "import" | "export" | "world" | "package" => { + Cow::Owned(format!("%{s}")) + } + _ => s.into(), + } +} diff --git a/tests/new.rs b/tests/new.rs index 7017aeae..88a11b93 100644 --- a/tests/new.rs +++ b/tests/new.rs @@ -305,3 +305,42 @@ fn it_supports_the_proxy_option() -> Result<()> { Ok(()) } + +/// Ensure that cargo-component supports using the new command with target and custom registry +#[test] +fn target_with_registry() -> Result<()> { + let dir: TempDir = TempDir::new()?; + + cargo_component([ + "new", + "--lib", + "--target", + "docs:adder", + "--registry", + "oci://ghcr.io", + "--registry-ns-prefix", + "bytecodealliance/", + "adder", + ]) + .current_dir(dir.path()) + .assert() + .try_success()?; + + // Ensure things still work if people forget the trailing / + cargo_component([ + "new", + "--lib", + "--target", + "docs:adder", + "--registry", + "oci://ghcr.io", + "--registry-ns-prefix", + "bytecodealliance", + "adder-no-trailing-slash", + ]) + .current_dir(dir.path()) + .assert() + .try_success()?; + + Ok(()) +}