diff --git a/Cargo.lock b/Cargo.lock index 8630cf70..2071f074 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -549,6 +549,7 @@ dependencies = [ "cargo-config2", "cargo_metadata", "clap", + "dialoguer", "futures", "heck 0.5.0", "indexmap 2.2.6", @@ -600,6 +601,7 @@ dependencies = [ "secrecy", "semver", "serde 1.0.197", + "thiserror", "tokio", "toml_edit 0.22.9", "unicode-width", @@ -753,6 +755,19 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static 1.4.0", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -879,6 +894,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror", + "zeroize", +] + [[package]] name = "difflib" version = "0.4.0" @@ -984,6 +1012,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.34" @@ -3177,6 +3211,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -3819,9 +3859,8 @@ dependencies = [ [[package]] name = "warg-api" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bf1e22e1e396b98a2181219b06d1a49a3478c1b9d87a29cd9cd819d714e6c3" +version = "0.5.0-dev" +source = "git+https://github.com/bytecodealliance/registry?rev=fd8c963#fd8c963660644f1b13cc8547731fbbcf749e1178" dependencies = [ "indexmap 2.2.6", "itertools 0.12.1", @@ -3834,9 +3873,8 @@ dependencies = [ [[package]] name = "warg-client" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56cfaf9781ca2d084468bbdd8bbc1e35947bb2a19f8d3940d899852f6dd78aa" +version = "0.5.0-dev" +source = "git+https://github.com/bytecodealliance/registry?rev=fd8c963#fd8c963660644f1b13cc8547731fbbcf749e1178" dependencies = [ "anyhow", "async-recursion", @@ -3878,9 +3916,8 @@ dependencies = [ [[package]] name = "warg-credentials" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626224ba1a00965282b669d2611654fd6292a15396ed8c850ce91684678fe19f" +version = "0.5.0-dev" +source = "git+https://github.com/bytecodealliance/registry?rev=fd8c963#fd8c963660644f1b13cc8547731fbbcf749e1178" dependencies = [ "anyhow", "indexmap 2.2.6", @@ -3892,9 +3929,8 @@ dependencies = [ [[package]] name = "warg-crypto" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2a8c47e96a7f1903931b34db9a1f0d22bcb3761a203ee6861db686daaedcb4b" +version = "0.5.0-dev" +source = "git+https://github.com/bytecodealliance/registry?rev=fd8c963#fd8c963660644f1b13cc8547731fbbcf749e1178" dependencies = [ "anyhow", "base64", @@ -3913,9 +3949,8 @@ dependencies = [ [[package]] name = "warg-protobuf" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceed0e698efd0fab8bb747efd452156a65149eb389f7fe2a6b6b3ced4e25ab24" +version = "0.5.0-dev" +source = "git+https://github.com/bytecodealliance/registry?rev=fd8c963#fd8c963660644f1b13cc8547731fbbcf749e1178" dependencies = [ "anyhow", "pbjson", @@ -3932,9 +3967,8 @@ dependencies = [ [[package]] name = "warg-protocol" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69be98a2e9e0aeace7cbd62184b11462d259c5e391e6208d59506c9a2d33571c" +version = "0.5.0-dev" +source = "git+https://github.com/bytecodealliance/registry?rev=fd8c963#fd8c963660644f1b13cc8547731fbbcf749e1178" dependencies = [ "anyhow", "base64", @@ -3955,9 +3989,8 @@ dependencies = [ [[package]] name = "warg-server" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07f15457ced83df5c2298f225fc83b6700e93c7bf320a2e4ef01114c0b34d7ce" +version = "0.5.0-dev" +source = "git+https://github.com/bytecodealliance/registry?rev=fd8c963#fd8c963660644f1b13cc8547731fbbcf749e1178" dependencies = [ "anyhow", "axum", @@ -3986,9 +4019,8 @@ dependencies = [ [[package]] name = "warg-transparency" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d272b3002b9e5f6f636817089ba091e1ba7b85858e72529f96e24bc9827f530" +version = "0.5.0-dev" +source = "git+https://github.com/bytecodealliance/registry?rev=fd8c963#fd8c963660644f1b13cc8547731fbbcf749e1178" dependencies = [ "anyhow", "indexmap 2.2.6", @@ -4447,6 +4479,7 @@ dependencies = [ "bytes", "cargo-component-core", "clap", + "dialoguer", "futures", "indexmap 2.2.6", "log", diff --git a/Cargo.toml b/Cargo.toml index 5604333a..feaa2047 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ bytes = { workspace = true } which = { workspace = true } shell-escape = "0.1.5" secrecy = { workspace = true } +dialoguer = { wokerspace = true } tempfile = { workspace = true } [dev-dependencies] @@ -69,11 +70,11 @@ members = ["crates/core", "crates/wit"] [workspace.dependencies] cargo-component-core = { path = "crates/core", version = "0.11.0" } -warg-protocol = "0.4.1" -warg-crypto = "0.4.1" -warg-client = "0.4.1" -warg-credentials = "0.4.1" -warg-server = "0.4.1" +warg-protocol = { git = "https://github.com/bytecodealliance/registry", rev = "fd8c963" } +warg-crypto = { git = "https://github.com/bytecodealliance/registry", rev = "fd8c963" } +warg-client = { git = "https://github.com/bytecodealliance/registry", rev = "fd8c963" } +warg-credentials = { git = "https://github.com/bytecodealliance/registry", rev = "fd8c963" } +warg-server = { git = "https://github.com/bytecodealliance/registry", rev = "fd8c963" } anyhow = "1.0.82" clap = { version = "4.5.4", features = ["derive"] } toml_edit = { version = "0.22.9", features = ["serde"] } @@ -91,6 +92,7 @@ wit-parser = "0.202.0" wit-component = "0.202.0" wasm-metadata = "0.202.0" parse_arg = "0.1.4" +dialoguer = "0.11.0" cargo_metadata = "0.18.1" cargo-config2 = "0.1.24" libc = "0.2.153" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 16a9cee6..64539f7f 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -30,6 +30,7 @@ log = { workspace = true } tokio = { workspace = true } secrecy = { workspace = true } clap = { workspace = true } +thiserror = "1.0.58" [target.'cfg(windows)'.dependencies.windows-sys] version = "0.52" diff --git a/crates/core/src/registry.rs b/crates/core/src/registry.rs index 5879d9bf..7bce80db 100644 --- a/crates/core/src/registry.rs +++ b/crates/core/src/registry.rs @@ -5,7 +5,7 @@ use crate::{ progress::{ProgressBar, ProgressStyle}, terminal::{Colors, Terminal}, }; -use anyhow::{bail, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use futures::{stream::FuturesUnordered, StreamExt}; use indexmap::IndexMap; use secrecy::Secret; @@ -21,10 +21,11 @@ use std::{ str::FromStr, sync::Arc, }; +use thiserror::Error; use url::Url; use warg_client::{ storage::{ContentStorage, PackageInfo, RegistryStorage}, - Config, FileSystemClient, RegistryUrl, StorageLockResult, + ClientError, Config, FileSystemClient, RegistryUrl, Retry, StorageLockResult, }; use warg_credentials::keyring::get_auth_token; use warg_crypto::hash::AnyHash; @@ -32,6 +33,20 @@ use warg_protocol::registry; use wit_component::DecodedWasm; use wit_parser::{PackageId, PackageName, Resolve, UnresolvedPackage, WorldId}; +#[derive(Debug, Error)] +/// Error for CLI commands +pub enum CommandError { + /// General errors + #[error(transparent)] + General(#[from] anyhow::Error), + /// Client Error + #[error("warg client error ({0}): {1}")] + WargClient(String, ClientError), + /// Client Error With Hint + #[error("warg client error with hint ({0}): {1}")] + WargHint(String, ClientError), +} + /// The name of the default registry. pub const DEFAULT_REGISTRY_NAME: &str = "default"; @@ -65,16 +80,18 @@ pub fn auth_token(config: &Config, registry: Option) -> Result, ) -> Result { - match FileSystemClient::try_new_with_config( - Some(url), + let client = match FileSystemClient::try_new_with_config( + config.home_url.as_deref(), config, - auth_token(config, Some(url.to_string()))?, - )? { + auth_token(config, config.home_url.clone())?, + ) + .await? + { StorageLockResult::Acquired(client) => Ok(client), StorageLockResult::NotAcquired(path) => { terminal.status_with_color( @@ -83,13 +100,18 @@ pub fn create_client( Colors::Cyan, )?; - Ok(FileSystemClient::new_with_config( - Some(url), + FileSystemClient::new_with_config( + config.home_url.as_deref(), config, - auth_token(config, Some(url.to_string()))?, - )?) + auth_token(config, config.home_url.clone())?, + ) + .await } + }?; + if let Some(retry) = retry { + retry.store_namespace(&client).await?; } + Ok(client) } /// Represents a WIT package dependency. @@ -440,7 +462,6 @@ impl<'a> DecodedDependency<'a> { /// Used to resolve dependencies for a WIT package. pub struct DependencyResolver<'a> { terminal: &'a Terminal, - registry_urls: &'a HashMap, warg_config: &'a Config, lock_file: Option>, registries: IndexMap<&'a str, Registry<'a>>, @@ -452,14 +473,12 @@ impl<'a> DependencyResolver<'a> { /// Creates a new dependency resolver. pub fn new( warg_config: &'a Config, - registry_urls: &'a HashMap, lock_file: Option>, terminal: &'a Terminal, network_allowed: bool, ) -> Result { Ok(DependencyResolver { terminal, - registry_urls, warg_config, lock_file, registries: Default::default(), @@ -473,7 +492,8 @@ impl<'a> DependencyResolver<'a> { &mut self, name: &'a registry::PackageName, dependency: &'a Dependency, - ) -> Result<()> { + retry: Option<&Retry>, + ) -> Result<(), CommandError> { match dependency { Dependency::Package(package) => { // Dependency comes from a registry, add a dependency to the resolver @@ -487,30 +507,27 @@ impl<'a> DependencyResolver<'a> { .transpose() }) { Some(Ok(locked)) => Some(locked), - Some(Err(e)) => return Err(e), + Some(Err(e)) => { + return Err(CommandError::General(e)); + } _ => None, }; let registry = match self.registries.entry(registry_name) { indexmap::map::Entry::Occupied(e) => e.into_mut(), - indexmap::map::Entry::Vacant(e) => { - let url = find_url( - Some(registry_name), - self.registry_urls, - self.warg_config.home_url.as_deref(), - )?; - e.insert(Registry { - client: Arc::new(create_client(self.warg_config, url, self.terminal)?), - packages: HashMap::new(), - dependencies: Vec::new(), - upserts: HashSet::new(), - }) - } + indexmap::map::Entry::Vacant(e) => e.insert(Registry { + client: Arc::new( + create_client(self.warg_config, self.terminal, retry).await?, + ), + packages: HashMap::new(), + dependencies: Vec::new(), + upserts: HashSet::new(), + }), }; registry .add_dependency(name, package_name, &package.version, registry_name, locked) - .await?; + .await? } Dependency::Local(p) => { // A local path dependency, insert a resolution immediately @@ -532,7 +549,7 @@ impl<'a> DependencyResolver<'a> { /// This will download all dependencies that are not already present in client storage. /// /// Returns the dependency resolution map. - pub async fn resolve(self) -> Result { + pub async fn resolve(self) -> Result { let Self { mut registries, mut resolutions, @@ -544,7 +561,6 @@ impl<'a> DependencyResolver<'a> { // Start by updating the packages that need updating // This will determine the contents that need to be downloaded let downloads = Self::update_packages(&mut registries, terminal, network_allowed).await?; - // Finally, download and resolve the dependencies for resolution in Self::download_and_resolve(registries, downloads, terminal, network_allowed).await? @@ -560,7 +576,7 @@ impl<'a> DependencyResolver<'a> { registries: &mut IndexMap<&'a str, Registry<'a>>, terminal: &Terminal, network_allowed: bool, - ) -> Result> { + ) -> Result, CommandError> { let task_count = registries .iter() .filter(|(_, r)| !r.upserts.is_empty()) @@ -570,7 +586,10 @@ impl<'a> DependencyResolver<'a> { if task_count > 0 { if !network_allowed { - bail!("a component registry update is required but network access is disabled"); + return Err(anyhow!( + "a component registry update is required but network access is disabled", + ) + .into()); } terminal.status("Updating", "component registry package logs")?; @@ -591,7 +610,15 @@ impl<'a> DependencyResolver<'a> { let client = registry.client.clone(); futures.push(tokio::spawn(async move { - (index, client.upsert(upserts.iter()).await) + ( + index, + client.upsert(upserts.iter()).await.map_err(|e| match &e { + ClientError::PackageDoesNotExistWithHint { .. } => { + CommandError::WargHint("error updating warg logs".to_string(), e) + } + _ => CommandError::WargClient("error updating warg logs".to_string(), e), + }), + ) })) } @@ -604,9 +631,7 @@ impl<'a> DependencyResolver<'a> { .get_index_mut(index) .expect("out of bounds registry index"); - res.with_context(|| { - format!("failed to update package logs for component registry `{name}`") - })?; + res?; log::info!("package logs successfully updated for component registry `{name}`"); finished += 1; @@ -875,16 +900,19 @@ impl<'a> Registry<'a> { packages: &'b mut HashMap, name: registry::PackageName, ) -> Result> { + let warg_reg = client.get_warg_registry(name.namespace()).await?; match packages.entry(name) { hash_map::Entry::Occupied(e) => Ok(Some(e.into_mut())), - hash_map::Entry::Vacant(e) => match client - .registry() - .load_package(client.get_warg_registry(), e.key()) - .await? - { - Some(p) => Ok(Some(e.insert(p))), - None => Ok(None), - }, + hash_map::Entry::Vacant(e) => { + match client + .registry() + .load_package(warg_reg.as_ref(), e.key()) + .await? + { + Some(p) => Ok(Some(e.insert(p))), + None => Ok(None), + } + } } } } diff --git a/crates/wit/Cargo.toml b/crates/wit/Cargo.toml index faa71795..1b665373 100644 --- a/crates/wit/Cargo.toml +++ b/crates/wit/Cargo.toml @@ -35,6 +35,7 @@ futures = { workspace = true } bytes = { workspace = true } tokio = { workspace = true } pretty_env_logger = { workspace = true } +dialoguer = { workspace = true } [dev-dependencies] assert_cmd = { workspace = true } diff --git a/crates/wit/src/bin/wit.rs b/crates/wit/src/bin/wit.rs index 3528a8da..d09613c2 100644 --- a/crates/wit/src/bin/wit.rs +++ b/crates/wit/src/bin/wit.rs @@ -1,7 +1,12 @@ use anyhow::Result; -use cargo_component_core::terminal::{Color, Terminal, Verbosity}; +use cargo_component_core::{ + registry::CommandError, + terminal::{Color, Terminal, Verbosity}, +}; use clap::Parser; +use dialoguer::{theme::ColorfulTheme, Confirm}; use std::process::exit; +use warg_client::{ClientError, Retry}; use wit::commands::{ AddCommand, BuildCommand, InitCommand, KeyCommand, PublishCommand, UpdateCommand, }; @@ -39,19 +44,74 @@ async fn main() -> Result<()> { pretty_env_logger::init(); let app = Wit::parse(); - - if let Err(e) = match app.command { - Command::Init(cmd) => cmd.exec().await, - Command::Add(cmd) => cmd.exec().await, - Command::Build(cmd) => cmd.exec().await, - Command::Publish(cmd) => cmd.exec().await, + if let Err(err) = match app.command { + Command::Init(cmd) => cmd.exec(), + Command::Add(cmd) => cmd.exec(None).await, + Command::Build(cmd) => cmd.exec(None).await, + Command::Publish(cmd) => cmd.exec(None).await, Command::Key(cmd) => cmd.exec().await, - Command::Update(cmd) => cmd.exec().await, + Command::Update(cmd) => cmd.exec(None).await, } { - let terminal = Terminal::new(Verbosity::Normal, Color::Auto); - terminal.error(format!("{e:?}"))?; - exit(1); + if let CommandError::WargHint(_, ClientError::PackageDoesNotExistWithHint { name, hint }) = + &err + { + if let Some((namespace, registry)) = hint.to_str().unwrap().split_once('=') { + let prompt = format!( + "The package `{}`, does not exist in the registry you're using. + However, the package namespace `{namespace}` does exist in the registry at {registry}. + Would you like to configure your warg cli to use this registry for packages with this namespace in the future? y/N\n", + name.name(), + ); + if Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt(prompt) + .interact() + .unwrap() + { + if let Err(e) = match Wit::parse().command { + Command::Init(cmd) => cmd.exec(), + Command::Add(cmd) => { + cmd.exec(Some(Retry::new( + namespace.to_string(), + registry.to_string(), + ))) + .await + } + Command::Build(cmd) => { + cmd.exec(Some(Retry::new( + namespace.to_string(), + registry.to_string(), + ))) + .await + } + Command::Publish(cmd) => { + cmd.exec(Some(Retry::new( + namespace.to_string(), + registry.to_string(), + ))) + .await + } + Command::Key(cmd) => cmd.exec().await, + Command::Update(cmd) => { + cmd.exec(Some(Retry::new( + namespace.to_string(), + registry.to_string(), + ))) + .await + } + } { + let terminal = Terminal::new(Verbosity::Normal, Color::Auto); + terminal.error(e)?; + exit(1); + } else { + return Ok(()); + } + } + } + } else { + let terminal = Terminal::new(Verbosity::Normal, Color::Auto); + terminal.error(err)?; + exit(1); + } } - Ok(()) } diff --git a/crates/wit/src/commands/add.rs b/crates/wit/src/commands/add.rs index 93eb4611..73245372 100644 --- a/crates/wit/src/commands/add.rs +++ b/crates/wit/src/commands/add.rs @@ -1,25 +1,27 @@ use crate::config::{Config, CONFIG_FILE_NAME}; -use anyhow::{bail, Context, Result}; +use anyhow::{anyhow, Context, Result}; use cargo_component_core::{ command::CommonOptions, - registry::{Dependency, DependencyResolution, DependencyResolver, RegistryPackage}, + registry::{ + CommandError, Dependency, DependencyResolution, DependencyResolver, RegistryPackage, + }, terminal::Terminal, VersionedPackageName, }; use clap::Args; use semver::VersionReq; use std::path::PathBuf; +use warg_client::Retry; use warg_protocol::registry::PackageName; async fn resolve_version( - config: &Config, warg_config: &warg_client::Config, package: &VersionedPackageName, registry: &Option, terminal: &Terminal, -) -> Result { - let mut resolver = - DependencyResolver::new(warg_config, &config.registries, None, terminal, true)?; + retry: Option, +) -> Result { + let mut resolver = DependencyResolver::new(warg_config, None, terminal, true)?; let dependency = Dependency::Package(RegistryPackage { name: Some(package.name.clone()), version: package @@ -30,7 +32,9 @@ async fn resolve_version( registry: registry.clone(), }); - resolver.add_dependency(&package.name, &dependency).await?; + resolver + .add_dependency(&package.name, &dependency, retry.as_ref()) + .await?; let dependencies = resolver.resolve().await?; assert_eq!(dependencies.len(), 1); @@ -76,7 +80,7 @@ pub struct AddCommand { impl AddCommand { /// Executes the command. - pub async fn exec(self) -> Result<()> { + pub async fn exec(self, retry: Option) -> Result<(), CommandError> { log::debug!("executing add command"); let (mut config, config_path) = Config::from_default_file()? @@ -84,7 +88,10 @@ impl AddCommand { let name = self.name.as_ref().unwrap_or(&self.package.name); if config.dependencies.contains_key(name) { - bail!("cannot add dependency `{name}` as it conflicts with an existing dependency"); + return Err(anyhow!( + "cannot add dependency `{name}` as it conflicts with an existing dependency" + ) + .into()); } let warg_config = warg_client::Config::from_default_file()?.unwrap_or_default(); @@ -103,11 +110,11 @@ impl AddCommand { } None => { let version = resolve_version( - &config, &warg_config, &self.package, &self.registry, &terminal, + retry, ) .await?; diff --git a/crates/wit/src/commands/build.rs b/crates/wit/src/commands/build.rs index cf672164..98a4fb49 100644 --- a/crates/wit/src/commands/build.rs +++ b/crates/wit/src/commands/build.rs @@ -1,11 +1,13 @@ use crate::{ build_wit_package, config::{Config, CONFIG_FILE_NAME}, + CommandError, }; use anyhow::{Context, Result}; use cargo_component_core::command::CommonOptions; use clap::Args; use std::{fs, path::PathBuf}; +use warg_client::Retry; /// Build a binary WIT package. #[derive(Args)] @@ -22,7 +24,7 @@ pub struct BuildCommand { impl BuildCommand { /// Executes the command. - pub async fn exec(self) -> Result<()> { + pub async fn exec(self, retry: Option) -> Result<(), CommandError> { log::debug!("executing build command"); let (config, config_path) = Config::from_default_file()? @@ -31,7 +33,14 @@ impl BuildCommand { let warg_config = warg_client::Config::from_default_file()?.unwrap_or_default(); let terminal = self.common.new_terminal(); - let (id, bytes) = build_wit_package(&config, &config_path, &warg_config, &terminal).await?; + let (id, bytes) = build_wit_package( + &config, + &config_path, + &warg_config, + &terminal, + retry.as_ref(), + ) + .await?; let output = self .output diff --git a/crates/wit/src/commands/init.rs b/crates/wit/src/commands/init.rs index 6bdb6aba..9a1f8879 100644 --- a/crates/wit/src/commands/init.rs +++ b/crates/wit/src/commands/init.rs @@ -1,5 +1,8 @@ -use crate::config::{ConfigBuilder, CONFIG_FILE_NAME}; -use anyhow::{bail, Result}; +use crate::{ + config::{ConfigBuilder, CONFIG_FILE_NAME}, + CommandError, +}; +use anyhow::anyhow; use cargo_component_core::{command::CommonOptions, registry::DEFAULT_REGISTRY_NAME}; use clap::Args; use std::path::PathBuf; @@ -24,15 +27,16 @@ pub struct InitCommand { impl InitCommand { /// Executes the command. - pub async fn exec(self) -> Result<()> { + pub fn exec(self) -> Result<(), CommandError> { log::debug!("executing init command"); let path = self.path.join(CONFIG_FILE_NAME); if path.is_file() { - bail!( - "WIT package configuration file `{path}` already exists", - path = path.display() - ); + return Err(anyhow!( + "WIT package configuration file `{0}` already exists", + path.display(), + ) + .into()); } let terminal = self.common.new_terminal(); diff --git a/crates/wit/src/commands/key.rs b/crates/wit/src/commands/key.rs index 9a0b6794..efd45b33 100644 --- a/crates/wit/src/commands/key.rs +++ b/crates/wit/src/commands/key.rs @@ -1,3 +1,4 @@ +use crate::CommandError; use anyhow::{Context, Result}; use cargo_component_core::{ command::CommonOptions, @@ -27,7 +28,7 @@ pub struct KeyCommand { impl KeyCommand { /// Executes the command. - pub async fn exec(self) -> Result<()> { + pub async fn exec(self) -> Result<(), CommandError> { let terminal = self.common.new_terminal(); let config = warg_client::Config::from_default_file()?.unwrap_or_default(); @@ -63,7 +64,7 @@ pub struct KeyIdCommand { impl KeyIdCommand { /// Executes the command. - pub async fn exec(self, config: Config) -> Result<()> { + pub async fn exec(self, config: Config) -> Result<(), CommandError> { let key = get_signing_key(Some(&self.url), &config.keys, config.home_url.as_deref())?; println!( "{fingerprint}", @@ -84,7 +85,7 @@ pub struct KeyNewCommand { impl KeyNewCommand { /// Executes the command. - pub async fn exec(self, terminal: &Terminal, mut config: Config) -> Result<()> { + pub async fn exec(self, terminal: &Terminal, mut config: Config) -> Result<(), CommandError> { let key = SigningKey::random(&mut OsRng).into(); set_signing_key( Some(&self.url), @@ -117,7 +118,7 @@ pub struct KeySetCommand { impl KeySetCommand { /// Executes the command. - pub async fn exec(self, terminal: &Terminal, mut config: Config) -> Result<()> { + pub async fn exec(self, terminal: &Terminal, mut config: Config) -> Result<(), CommandError> { let key = PrivateKey::decode( rpassword::prompt_password("input signing key (expected format is `:`): ") .context("failed to read signing key")?, @@ -155,7 +156,7 @@ pub struct KeyDeleteCommand { impl KeyDeleteCommand { /// Executes the command. - pub async fn exec(self, terminal: &Terminal, config: Config) -> Result<()> { + pub async fn exec(self, terminal: &Terminal, config: Config) -> Result<(), CommandError> { terminal.write_stdout( "⚠️ WARNING: this operation cannot be undone and the key will be permanently deleted ⚠️", Some(Colors::Yellow), diff --git a/crates/wit/src/commands/publish.rs b/crates/wit/src/commands/publish.rs index 3d3a9991..ef03e59f 100644 --- a/crates/wit/src/commands/publish.rs +++ b/crates/wit/src/commands/publish.rs @@ -1,10 +1,11 @@ use crate::{ config::{Config, CONFIG_FILE_NAME}, - publish_wit_package, PublishOptions, + publish_wit_package, CommandError, PublishOptions, }; use anyhow::{Context, Result}; -use cargo_component_core::{command::CommonOptions, registry::find_url}; +use cargo_component_core::command::CommonOptions; use clap::Args; +use warg_client::Retry; use warg_credentials::keyring::get_signing_key; use warg_crypto::signing::PrivateKey; use warg_protocol::registry::PackageName; @@ -36,7 +37,7 @@ pub struct PublishCommand { impl PublishCommand { /// Executes the command. - pub async fn exec(self) -> Result<()> { + pub async fn exec(self, retry: Option) -> Result<(), CommandError> { log::debug!("executing publish command"); let (config, config_path) = Config::from_default_file()? @@ -45,12 +46,6 @@ impl PublishCommand { let terminal = self.common.new_terminal(); let warg_config = warg_client::Config::from_default_file()?.unwrap_or_default(); - let url = find_url( - self.registry.as_deref(), - &config.registries, - warg_config.home_url.as_deref(), - )?; - let signing_key = if let Ok(key) = std::env::var("WIT_PUBLISH_KEY") { PrivateKey::decode(key).context( "failed to parse signing key from `WIT_PUBLISH_KEY` environment variable", @@ -68,13 +63,13 @@ impl PublishCommand { config: &config, config_path: &config_path, warg_config: &warg_config, - url, signing_key: &signing_key, package: self.package.as_ref(), init: self.init, dry_run: self.dry_run, }, &terminal, + retry, ) .await } diff --git a/crates/wit/src/commands/update.rs b/crates/wit/src/commands/update.rs index c8f4b6e6..afbe3fd3 100644 --- a/crates/wit/src/commands/update.rs +++ b/crates/wit/src/commands/update.rs @@ -1,7 +1,11 @@ -use crate::config::{Config, CONFIG_FILE_NAME}; +use crate::{ + config::{Config, CONFIG_FILE_NAME}, + CommandError, +}; use anyhow::{Context, Result}; use cargo_component_core::command::CommonOptions; use clap::Args; +use warg_client::Retry; /// Update dependencies as recorded in the lock file. #[derive(Args)] @@ -18,7 +22,7 @@ pub struct UpdateCommand { impl UpdateCommand { /// Executes the command. - pub async fn exec(self) -> Result<()> { + pub async fn exec(self, retry: Option) -> Result<(), CommandError> { log::debug!("executing update command"); let (config, config_path) = Config::from_default_file()? @@ -27,6 +31,15 @@ impl UpdateCommand { let warg_config = warg_client::Config::from_default_file()?.unwrap_or_default(); let terminal = self.common.new_terminal(); - crate::update_lockfile(&config, &config_path, &warg_config, &terminal, self.dry_run).await + crate::update_lockfile( + &config, + &config_path, + &warg_config, + &terminal, + self.dry_run, + retry, + ) + .await + .map_err(|e| e.into()) } } diff --git a/crates/wit/src/lib.rs b/crates/wit/src/lib.rs index 37d1dad8..85c5fdc4 100644 --- a/crates/wit/src/lib.rs +++ b/crates/wit/src/lib.rs @@ -2,24 +2,28 @@ #![deny(missing_docs)] -use anyhow::{bail, Context, Result}; +use anyhow::{anyhow, Context, Result}; use bytes::Bytes; use cargo_component_core::{ lock::{LockFile, LockFileResolver, LockedPackage, LockedPackageVersion}, - registry::{create_client, DecodedDependency, DependencyResolutionMap, DependencyResolver}, + registry::{ + create_client, CommandError, DecodedDependency, DependencyResolutionMap, DependencyResolver, + }, terminal::{Colors, Terminal}, }; use config::Config; use indexmap::{IndexMap, IndexSet}; use lock::{acquire_lock_file_ro, acquire_lock_file_rw, to_lock_file}; use std::{collections::HashSet, path::Path, time::Duration}; -use warg_client::storage::{ContentStorage, PublishEntry, PublishInfo}; +use warg_client::{ + storage::{ContentStorage, PublishEntry, PublishInfo}, + ClientError, Retry, +}; use warg_crypto::signing::PrivateKey; use warg_protocol::registry; use wasm_metadata::{Link, LinkType, RegistryMetadata}; use wit_component::DecodedWasm; use wit_parser::{PackageId, PackageName, Resolve, UnresolvedPackage}; - pub mod commands; pub mod config; mod lock; @@ -30,7 +34,8 @@ async fn resolve_dependencies( warg_config: &warg_client::Config, terminal: &Terminal, update_lock_file: bool, -) -> Result { + retry: Option<&Retry>, +) -> Result { let file_lock = acquire_lock_file_ro(terminal, config_path)?; let lock_file = file_lock .as_ref() @@ -46,14 +51,13 @@ async fn resolve_dependencies( let mut resolver = DependencyResolver::new( warg_config, - &config.registries, lock_file.as_ref().map(LockFileResolver::new), terminal, true, )?; for (name, dep) in &config.dependencies { - resolver.add_dependency(name, dep).await?; + resolver.add_dependency(name, dep, retry).await?; } let map = resolver.resolve().await?; @@ -81,7 +85,7 @@ async fn resolve_dependencies( fn parse_wit_package( dir: &Path, dependencies: &DependencyResolutionMap, -) -> Result<(Resolve, PackageId)> { +) -> Result<(Resolve, PackageId), CommandError> { let mut merged = Resolve::default(); // Start by decoding all of the dependencies @@ -89,10 +93,11 @@ fn parse_wit_package( for (name, resolution) in dependencies { let decoded = resolution.decode()?; if let Some(prev) = deps.insert(decoded.package_name().clone(), decoded) { - bail!( - "duplicate definitions of package `{prev}` found while decoding dependency `{name}`", - prev = prev.package_name() - ); + return Err(anyhow!( + "duplicate definitions of package `{prev}` found while decoding dependency `{name}`", + prev = prev.package_name() + ) + .into()); } } @@ -149,12 +154,7 @@ fn parse_wit_package( }; } - let package = merged.push(root).with_context(|| { - format!( - "failed to merge package from directory `{dir}`", - dir = dir.display() - ) - })?; + let package = merged.push(root)?; return Ok((merged, package)); @@ -163,7 +163,7 @@ fn parse_wit_package( deps: &'a IndexMap, order: &mut IndexSet, visiting: &mut HashSet<&'a PackageName>, - ) -> Result<()> { + ) -> Result<(), CommandError> { if order.contains(dep.package_name()) { return Ok(()); } @@ -180,7 +180,10 @@ fn parse_wit_package( // the package is resolved if let Some(dep) = deps.get(name) { if !visiting.insert(name) { - bail!("foreign dependency `{name}` forms a dependency cycle while parsing dependency `{other}`", other = resolution.name()); + return Err(anyhow!( + "foreign dependency `{name}` forms a dependency cycle while parsing dependency `{other}`", other = resolution.name() + ) + .into()); } visit(dep, deps, order, visiting)?; @@ -202,7 +205,9 @@ fn parse_wit_package( if let Some(dep) = deps.get(&package.name) { if !visiting.insert(&package.name) { - bail!("foreign dependency `{name}` forms a dependency cycle while parsing dependency `{other}`", name = package.name, other = resolution.name()); + return Err(anyhow!( + "foreign dependency `{name}` forms a dependency cycle while parsing dependency `{other}`", name = package.name, other = resolution.name() + ).into()); } visit(dep, deps, order, visiting)?; @@ -224,10 +229,10 @@ async fn build_wit_package( config_path: &Path, warg_config: &warg_client::Config, terminal: &Terminal, -) -> Result<(registry::PackageName, Vec)> { + retry: Option<&Retry>, +) -> Result<(registry::PackageName, Vec), CommandError> { let dependencies = - resolve_dependencies(config, config_path, warg_config, terminal, true).await?; - + resolve_dependencies(config, config_path, warg_config, terminal, true, retry).await?; let dir = config_path.parent().unwrap_or_else(|| Path::new(".")); let (mut resolve, package) = parse_wit_package(dir, &dependencies)?; @@ -255,7 +260,6 @@ struct PublishOptions<'a> { config: &'a Config, config_path: &'a Path, warg_config: &'a warg_client::Config, - url: &'a str, signing_key: &'a PrivateKey, package: Option<&'a registry::PackageName>, init: bool, @@ -312,12 +316,17 @@ fn add_registry_metadata(config: &Config, bytes: &[u8]) -> Result> { .context("failed to add registry metadata to component") } -async fn publish_wit_package(options: PublishOptions<'_>, terminal: &Terminal) -> Result<()> { +async fn publish_wit_package( + options: PublishOptions<'_>, + terminal: &Terminal, + retry: Option, +) -> Result<(), CommandError> { let (name, bytes) = build_wit_package( options.config, options.config_path, options.warg_config, terminal, + retry.as_ref(), ) .await?; @@ -328,8 +337,7 @@ async fn publish_wit_package(options: PublishOptions<'_>, terminal: &Terminal) - let bytes = add_registry_metadata(options.config, &bytes)?; let name = options.package.unwrap_or(&name); - let mut client = create_client(options.warg_config, options.url, terminal)?; - client.refresh_namespace(name.namespace()).await?; + let client = create_client(options.warg_config, terminal, retry.as_ref()).await?; let content = client .content() @@ -356,10 +364,25 @@ async fn publish_wit_package(options: PublishOptions<'_>, terminal: &Terminal) - content, }); - let record_id = client.publish_with_info(options.signing_key, info).await?; + let record_id = client + .publish_with_info(options.signing_key, info) + .await + .map_err(|e| match &e { + ClientError::PackageDoesNotExistWithHint { .. } => { + CommandError::WargHint("error publishing package".to_string(), e) + } + _ => CommandError::WargClient("error publishing package".to_string(), e), + })?; + client .wait_for_publish(name, &record_id, Duration::from_secs(1)) - .await?; + .await + .map_err(|e| match &e { + ClientError::PackageDoesNotExistWithHint { .. } => { + CommandError::WargHint("error publishing package".to_string(), e) + } + _ => CommandError::WargClient("error publishing package".to_string(), e), + })?; terminal.status( "Published", @@ -379,12 +402,12 @@ pub async fn update_lockfile( warg_config: &warg_client::Config, terminal: &Terminal, dry_run: bool, + retry: Option, ) -> Result<()> { // Resolve all dependencies as if the lock file does not exist - let mut resolver = - DependencyResolver::new(warg_config, &config.registries, None, terminal, true)?; + let mut resolver = DependencyResolver::new(warg_config, None, terminal, true)?; for (name, dep) in &config.dependencies { - resolver.add_dependency(name, dep).await?; + resolver.add_dependency(name, dep, retry.as_ref()).await?; } let map = resolver.resolve().await?; diff --git a/crates/wit/tests/publish.rs b/crates/wit/tests/publish.rs index 13d237b3..de970527 100644 --- a/crates/wit/tests/publish.rs +++ b/crates/wit/tests/publish.rs @@ -69,10 +69,14 @@ async fn it_does_a_dry_run_publish() -> Result<()> { )) .success(); - let client = FileSystemClient::new_with_config(None, &config, None)?; + let client = FileSystemClient::new_with_config(None, &config, None).await?; assert!(client - .download(&"test:qux".parse().unwrap(), &"0.1.0".parse().unwrap()) + .download( + None, + &"test:qux".parse().unwrap(), + &"0.1.0".parse().unwrap() + ) .await .unwrap_err() .to_string() @@ -116,7 +120,7 @@ async fn it_publishes_with_registry_metadata() -> Result<()> { .stderr(contains("Published package `test:qux` v0.1.0")) .success(); - let client = Client::new_with_config(None, &config, None)?; + let client = Client::new_with_config(None, &config, None).await?; let download = client .download_exact(&PackageName::new("test:qux")?, &Version::parse("0.1.0")?) .await?; diff --git a/src/bin/cargo-component.rs b/src/bin/cargo-component.rs index 6316ce41..c7f333a3 100644 --- a/src/bin/cargo-component.rs +++ b/src/bin/cargo-component.rs @@ -1,11 +1,18 @@ +use std::process::exit; + use anyhow::{bail, Result}; use cargo_component::{ commands::{AddCommand, KeyCommand, NewCommand, PublishCommand, UpdateCommand}, config::{CargoArguments, Config}, load_component_metadata, load_metadata, run_cargo_command, }; -use cargo_component_core::terminal::{Color, Terminal, Verbosity}; +use cargo_component_core::{ + registry::CommandError, + terminal::{Color, Terminal, Verbosity}, +}; use clap::{CommandFactory, Parser}; +use dialoguer::{theme::ColorfulTheme, Confirm}; +use warg_client::{ClientError, Retry}; fn version() -> &'static str { option_env!("CARGO_VERSION_INFO").unwrap_or(env!("CARGO_PKG_VERSION")) @@ -109,18 +116,91 @@ async fn main() -> Result<()> { match subcommand.as_deref() { // Check for built-in command or no command (shows help) Some(cmd) if BUILTIN_COMMANDS.contains(&cmd) => { - if let Err(e) = match CargoComponent::parse() { + if let Err(err) = match CargoComponent::parse() { CargoComponent::Component(cmd) | CargoComponent::Command(cmd) => match cmd { - Command::Add(cmd) => cmd.exec().await, + Command::Add(cmd) => cmd.exec(None).await, Command::Key(cmd) => cmd.exec().await, - Command::New(cmd) => cmd.exec().await, - Command::Update(cmd) => cmd.exec().await, - Command::Publish(cmd) => cmd.exec().await, + Command::New(cmd) => cmd.exec(None).await, + Command::Update(cmd) => cmd.exec(None).await, + Command::Publish(cmd) => cmd.exec(None).await, }, } { - let terminal = Terminal::new(Verbosity::Normal, Color::Auto); - terminal.error(format!("{e:?}"))?; - std::process::exit(1); + match err { + CommandError::General(e) => { + let terminal = Terminal::new(Verbosity::Normal, Color::Auto); + terminal.error(e)?; + exit(1); + } + CommandError::WargClient(reason, e) => { + let terminal = Terminal::new(Verbosity::Normal, Color::Auto); + terminal.error(format!("{reason}: {e}"))?; + exit(1); + } + CommandError::WargHint(reason, e) => { + if let ClientError::PackageDoesNotExistWithHint { name, hint } = &e { + let hint_reg = hint.to_str().unwrap(); + let mut terms = hint_reg.split('='); + let namespace = terms.next(); + let registry = terms.next(); + if let (Some(namespace), Some(registry)) = (namespace, registry) { + let prompt = format!( + "The package `{}`, does not exist in the registry you're using.\nHowever, the package namespace `{namespace}` does exist in the registry at {registry}.\nWould you like to configure your warg cli to use this registry for packages with this namespace in the future? y/N\n", + name.name() + ); + if Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt(prompt) + .interact() + .unwrap() + { + if let Err(e) = match CargoComponent::parse() { + CargoComponent::Component(cmd) + | CargoComponent::Command(cmd) => match cmd { + Command::Add(cmd) => { + cmd.exec(Some(Retry::new( + namespace.to_string(), + registry.to_string(), + ))) + .await + } + Command::Key(cmd) => cmd.exec().await, + Command::New(cmd) => { + cmd.exec(Some(Retry::new( + namespace.to_string(), + registry.to_string(), + ))) + .await + } + Command::Update(cmd) => { + cmd.exec(Some(Retry::new( + namespace.to_string(), + registry.to_string(), + ))) + .await + } + Command::Publish(cmd) => { + cmd.exec(Some(Retry::new( + namespace.to_string(), + registry.to_string(), + ))) + .await + } + }, + } { + let terminal = + Terminal::new(Verbosity::Normal, Color::Auto); + terminal.error(e)?; + exit(1); + } else { + let terminal = + Terminal::new(Verbosity::Normal, Color::Auto); + terminal.error(format!("{reason}: {e}"))?; + exit(1); + } + } + } + } + } + }; } } @@ -178,14 +258,62 @@ async fn main() -> Result<()> { &config, &metadata, &packages, - subcommand.as_deref(), + detect_subcommand().as_deref(), &cargo_args, &spawn_args, + None, ) .await { - config.terminal().error(format!("{e:?}"))?; - std::process::exit(1); + match e { + CommandError::General(e) => { + let terminal = Terminal::new(Verbosity::Normal, Color::Auto); + terminal.error(e)?; + exit(1); + } + CommandError::WargClient(reason, e) => { + let terminal = Terminal::new(Verbosity::Normal, Color::Auto); + terminal.error(format!("{reason}: {e}"))?; + exit(1); + } + CommandError::WargHint(reason, e) => { + if let ClientError::PackageDoesNotExistWithHint { name, hint } = &e { + let hint_reg = hint.to_str().unwrap(); + let mut terms = hint_reg.split('='); + let namespace = terms.next(); + let registry = terms.next(); + if let (Some(namespace), Some(registry)) = (namespace, registry) { + let prompt = format!( + "The package `{}`, does not exist in the registry you're using.\nHowever, the package namespace `{namespace}` does exist in the registry at {registry}.\nWould you like to configure your warg cli to use this registry for packages with this namespace in the future? y/N\n", + name.name() + ); + if Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt(prompt) + .interact() + .unwrap() + { + run_cargo_command( + &config, + &metadata, + &packages, + detect_subcommand().as_deref(), + &cargo_args, + &spawn_args, + Some(&Retry::new( + namespace.to_string(), + registry.to_string(), + )), + ) + .await?; + } else { + let terminal = Terminal::new(Verbosity::Normal, Color::Auto); + terminal.error(format!("{reason}: {e}"))?; + exit(1); + } + } + } + } + }; } } } diff --git a/src/commands/add.rs b/src/commands/add.rs index 794866f4..53ba6577 100644 --- a/src/commands/add.rs +++ b/src/commands/add.rs @@ -7,7 +7,9 @@ use crate::{ use anyhow::{bail, Context, Result}; use cargo_component_core::{ command::CommonOptions, - registry::{Dependency, DependencyResolution, DependencyResolver, RegistryPackage}, + registry::{ + CommandError, Dependency, DependencyResolution, DependencyResolver, RegistryPackage, + }, VersionedPackageName, }; use cargo_metadata::Package; @@ -18,6 +20,7 @@ use std::{ path::{Path, PathBuf}, }; use toml_edit::{value, DocumentMut, InlineTable, Item, Table, Value}; +use warg_client::Retry; use warg_protocol::registry::PackageName; /// Add a dependency for a WebAssembly component @@ -63,7 +66,7 @@ pub struct AddCommand { impl AddCommand { /// Executes the command - pub async fn exec(self) -> Result<()> { + pub async fn exec(self, retry: Option) -> Result<(), CommandError> { let config = Config::new(self.common.new_terminal())?; let metadata = load_metadata(self.manifest_path.as_deref())?; @@ -104,7 +107,7 @@ impl AddCommand { ), )?; } else { - let version = self.resolve_version(&config, &metadata, name, true).await?; + let version = self.resolve_version(&config, name, true, retry).await?; let version = version.trim_start_matches('^'); self.add(package, version)?; @@ -120,17 +123,12 @@ impl AddCommand { async fn resolve_version( &self, config: &Config, - metadata: &ComponentMetadata, name: &PackageName, network_allowed: bool, + retry: Option, ) -> Result { - let mut resolver = DependencyResolver::new( - config.warg(), - &metadata.section.registries, - None, - config.terminal(), - network_allowed, - )?; + let mut resolver = + DependencyResolver::new(config.warg(), None, config.terminal(), network_allowed)?; let dependency = Dependency::Package(RegistryPackage { name: Some(self.package.name.clone()), version: self @@ -142,7 +140,9 @@ impl AddCommand { registry: self.registry.clone(), }); - resolver.add_dependency(name, &dependency).await?; + resolver + .add_dependency(name, &dependency, retry.as_ref()) + .await?; let dependencies = resolver.resolve().await?; assert_eq!(dependencies.len(), 1); diff --git a/src/commands/key.rs b/src/commands/key.rs index 58a988c8..339c7ee4 100644 --- a/src/commands/key.rs +++ b/src/commands/key.rs @@ -1,6 +1,6 @@ use crate::config::Config; use anyhow::{Context, Result}; -use cargo_component_core::{command::CommonOptions, terminal::Colors}; +use cargo_component_core::{command::CommonOptions, registry::CommandError, terminal::Colors}; use clap::{Args, Subcommand}; use p256::ecdsa::SigningKey; use rand_core::OsRng; @@ -24,7 +24,7 @@ pub struct KeyCommand { impl KeyCommand { /// Executes the command. - pub async fn exec(self) -> Result<()> { + pub async fn exec(self) -> Result<(), CommandError> { log::debug!("executing key command"); let mut config = Config::new(self.common.new_terminal())?; @@ -61,7 +61,7 @@ pub struct KeyIdCommand { impl KeyIdCommand { /// Executes the command. - pub async fn exec(self, config: &Config) -> Result<()> { + pub async fn exec(self, config: &Config) -> Result<(), CommandError> { let key = get_signing_key( Some(&self.url), &config.warg.keys, @@ -86,7 +86,7 @@ pub struct KeyNewCommand { impl KeyNewCommand { /// Executes the command. - pub async fn exec(self, config: &mut Config) -> Result<()> { + pub async fn exec(self, config: &mut Config) -> Result<(), CommandError> { let key = SigningKey::random(&mut OsRng).into(); set_signing_key( Some(&self.url), @@ -119,7 +119,7 @@ pub struct KeySetCommand { impl KeySetCommand { /// Executes the command. - pub async fn exec(self, config: &mut Config) -> Result<()> { + pub async fn exec(self, config: &mut Config) -> Result<(), CommandError> { let key = PrivateKey::decode( rpassword::prompt_password("input signing key (expected format is `:`): ") .context("failed to read signing key")?, @@ -160,7 +160,7 @@ pub struct KeyDeleteCommand { impl KeyDeleteCommand { /// Executes the command. - pub async fn exec(self, config: &Config) -> Result<()> { + pub async fn exec(self, config: &Config) -> Result<(), CommandError> { config.terminal().write_stdout( "⚠️ WARNING: this operation cannot be undone and the key will be permanently deleted ⚠️", Some(Colors::Yellow), diff --git a/src/commands/new.rs b/src/commands/new.rs index d8266d42..ebc84a6e 100644 --- a/src/commands/new.rs +++ b/src/commands/new.rs @@ -1,8 +1,10 @@ use crate::{config::Config, generator::SourceGenerator, metadata, metadata::DEFAULT_WIT_DIR}; -use anyhow::{bail, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use cargo_component_core::{ command::CommonOptions, - registry::{Dependency, DependencyResolution, DependencyResolver, RegistryResolution}, + registry::{ + CommandError, Dependency, DependencyResolution, DependencyResolver, RegistryResolution, + }, }; use clap::Args; use heck::ToKebabCase; @@ -16,6 +18,7 @@ use std::{ }; use toml_edit::{table, value, DocumentMut, Item, Table, Value}; use url::Url; +use warg_client::Retry; const WIT_BINDGEN_RT_CRATE: &str = "wit-bindgen-rt"; @@ -140,7 +143,7 @@ impl<'a> PackageName<'a> { impl NewCommand { /// Executes the command. - pub async fn exec(self) -> Result<()> { + pub async fn exec(self, retry: Option) -> Result<(), CommandError> { log::debug!("executing new command"); let config = Config::new(self.common.new_terminal())?; @@ -159,8 +162,9 @@ impl NewCommand { }; let target = self - .resolve_target(&config, ®istries, target, true) - .await?; + .resolve_target(&config, target, true, retry) + .await + .map_err(|e| e)?; let source = self.generate_source(&target)?; let mut command = self.new_command(); @@ -171,7 +175,7 @@ impl NewCommand { } } Err(e) => { - bail!("failed to execute `cargo new` command: {e}") + return Err(anyhow!("failed to execute `cargo new` command: {e}").into()); } } @@ -512,10 +516,10 @@ world example {{ async fn resolve_target( &self, config: &Config, - registries: &HashMap, target: Option, network_allowed: bool, - ) -> Result)>> { + retry: Option, + ) -> Result)>, CommandError> { match target { Some(metadata::Target::Package { name, @@ -524,14 +528,15 @@ world example {{ }) => { let mut resolver = DependencyResolver::new( config.warg(), - registries, None, config.terminal(), network_allowed, )?; let dependency = Dependency::Package(package); - resolver.add_dependency(&name, &dependency).await?; + resolver + .add_dependency(&name, &dependency, retry.as_ref()) + .await?; let dependencies = resolver.resolve().await?; assert_eq!(dependencies.len(), 1); diff --git a/src/commands/publish.rs b/src/commands/publish.rs index fe9eb717..85e04275 100644 --- a/src/commands/publish.rs +++ b/src/commands/publish.rs @@ -3,10 +3,14 @@ use crate::{ is_wasm_target, load_metadata, publish, run_cargo_command, PackageComponentMetadata, PublishOptions, }; -use anyhow::{bail, Context, Result}; -use cargo_component_core::{command::CommonOptions, registry::find_url}; +use anyhow::{anyhow, Context, Result}; +use cargo_component_core::{ + command::CommonOptions, + registry::{find_url, CommandError}, +}; use clap::Args; use std::path::PathBuf; +use warg_client::Retry; use warg_credentials::keyring::get_signing_key; use warg_crypto::signing::PrivateKey; @@ -77,14 +81,14 @@ pub struct PublishCommand { impl PublishCommand { /// Executes the command. - pub async fn exec(self) -> Result<()> { + pub async fn exec(self, retry: Option) -> Result<(), CommandError> { log::debug!("executing publish command"); let config = Config::new(self.common.new_terminal())?; if let Some(target) = &self.target { if !is_wasm_target(target) { - bail!("target `{}` is not a WebAssembly target", target); + return Err(anyhow!("target `{}` is not a WebAssembly target", target).into()); } } @@ -164,13 +168,15 @@ impl PublishCommand { Some("build"), &cargo_build_args, &spawn_args, + retry.as_ref(), ) .await?; if outputs.len() != 1 { - bail!( + return Err(anyhow!( "expected one output from `cargo build`, got {len}", len = outputs.len() - ); + ) + .into()); } let options = PublishOptions { @@ -184,7 +190,7 @@ impl PublishCommand { dry_run: self.dry_run, }; - publish(&config, &options).await + publish(&config, &options, retry.as_ref()).await } fn build_args(&self) -> Result> { diff --git a/src/commands/update.rs b/src/commands/update.rs index 3f890b9c..0311d229 100644 --- a/src/commands/update.rs +++ b/src/commands/update.rs @@ -1,8 +1,9 @@ use crate::{load_component_metadata, load_metadata, Config}; use anyhow::Result; -use cargo_component_core::command::CommonOptions; +use cargo_component_core::{command::CommonOptions, registry::CommandError}; use clap::Args; use std::path::PathBuf; +use warg_client::Retry; /// Update dependencies as recorded in the component lock file #[derive(Args)] @@ -35,7 +36,7 @@ pub struct UpdateCommand { impl UpdateCommand { /// Executes the command. - pub async fn exec(self) -> Result<()> { + pub async fn exec(self, retry: Option) -> Result<(), CommandError> { log::debug!("executing update command"); let config = Config::new(self.common.new_terminal())?; let metadata = load_metadata(self.manifest_path.as_deref())?; @@ -51,6 +52,7 @@ impl UpdateCommand { lock_update_allowed, self.locked, self.dry_run, + retry.as_ref(), ) .await } diff --git a/src/lib.rs b/src/lib.rs index 9148fe70..b4f56ba3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,12 +3,12 @@ #![deny(missing_docs)] use crate::target::install_wasm32_wasi; -use anyhow::{bail, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use bindings::BindingsGenerator; use bytes::Bytes; use cargo_component_core::{ lock::{LockFile, LockFileResolver, LockedPackage, LockedPackageVersion}, - registry::create_client, + registry::{create_client, CommandError}, terminal::Colors, }; use cargo_config2::{PathAndArgs, TargetTripleRef}; @@ -31,7 +31,10 @@ use std::{ time::{Duration, SystemTime}, }; use tempfile::NamedTempFile; -use warg_client::storage::{ContentStorage, PublishEntry, PublishInfo}; +use warg_client::{ + storage::{ContentStorage, PublishEntry, PublishInfo}, + ClientError, Retry, +}; use warg_crypto::signing::PrivateKey; use warg_protocol::registry::PackageName; use wasm_metadata::{Link, LinkType, RegistryMetadata}; @@ -135,8 +138,9 @@ pub async fn run_cargo_command( subcommand: Option<&str>, cargo_args: &CargoArguments, spawn_args: &[String], -) -> Result> { - let import_name_map = generate_bindings(config, metadata, packages, cargo_args).await?; + retry: Option<&Retry>, +) -> Result, CommandError> { + let import_name_map = generate_bindings(config, metadata, packages, cargo_args, retry).await?; let cargo_path = std::env::var("CARGO") .map(PathBuf::from) @@ -195,7 +199,7 @@ pub async fn run_cargo_command( if let Some(format) = &cargo_args.message_format { if format != "json-render-diagnostics" { - bail!("unsupported cargo message format `{format}`"); + return Err(anyhow!("unsupported cargo message format `{format}`").into()); } } @@ -640,7 +644,8 @@ fn read_artifact(path: &Path, mut componentizable: bool) -> Result } fn last_modified_time(path: &Path) -> Result { - path.metadata() + Ok(path + .metadata() .with_context(|| { format!( "failed to read file metadata for `{path}`", @@ -653,7 +658,7 @@ fn last_modified_time(path: &Path) -> Result { "failed to retrieve last modified time for `{path}`", path = path.display() ) - }) + })?) } /// Loads the workspace metadata based on the given manifest path. @@ -718,8 +723,9 @@ async fn generate_bindings( metadata: &Metadata, packages: &[PackageComponentMetadata<'_>], cargo_args: &CargoArguments, -) -> Result>> { - let last_modified_exe = last_modified_time(&std::env::current_exe()?)?; + retry: Option<&Retry>, +) -> Result>, CommandError> { + let last_modified_exe = last_modified_time(&std::env::current_exe().unwrap())?; let file_lock = acquire_lock_file_ro(config.terminal(), metadata)?; let lock_file = file_lock .as_ref() @@ -737,8 +743,14 @@ async fn generate_bindings( env::current_dir().with_context(|| "couldn't get the current directory of the process")?; let resolver = lock_file.as_ref().map(LockFileResolver::new); - let resolution_map = - create_resolution_map(config, packages, resolver, cargo_args.network_allowed()).await?; + let resolution_map = create_resolution_map( + config, + packages, + resolver, + cargo_args.network_allowed(), + retry, + ) + .await?; let mut import_name_map = HashMap::new(); for PackageComponentMetadata { package, .. } in packages { let resolution = resolution_map.get(&package.id).expect("missing resolution"); @@ -778,12 +790,14 @@ async fn create_resolution_map<'a>( packages: &'a [PackageComponentMetadata<'_>], lock_file: Option>, network_allowed: bool, -) -> Result> { + retry: Option<&Retry>, +) -> Result, CommandError> { let mut map = PackageResolutionMap::default(); for PackageComponentMetadata { package, metadata } in packages { let resolution = - PackageDependencyResolution::new(config, metadata, lock_file, network_allowed).await?; + PackageDependencyResolution::new(config, metadata, lock_file, network_allowed, retry) + .await?; map.insert(package.id.clone(), resolution); } @@ -1087,7 +1101,11 @@ fn add_registry_metadata(package: &Package, bytes: &[u8], path: &Path) -> Result } /// Publish a component for the given workspace and publish options. -pub async fn publish(config: &Config, options: &PublishOptions<'_>) -> Result<()> { +pub async fn publish( + config: &Config, + options: &PublishOptions<'_>, + retry: Option<&Retry>, +) -> Result<(), CommandError> { if options.dry_run { config .terminal() @@ -1095,8 +1113,7 @@ pub async fn publish(config: &Config, options: &PublishOptions<'_>) -> Result<() return Ok(()); } - let mut client = create_client(config.warg(), options.registry_url, config.terminal())?; - client.refresh_namespace(options.name.namespace()).await?; + let client = create_client(config.warg(), config.terminal(), retry).await?; let bytes = fs::read(options.path).with_context(|| { format!( @@ -1138,10 +1155,24 @@ pub async fn publish(config: &Config, options: &PublishOptions<'_>) -> Result<() content, }); - let record_id = client.publish_with_info(options.signing_key, info).await?; + let record_id = client + .publish_with_info(options.signing_key, info) + .await + .map_err(|e| match &e { + ClientError::PackageDoesNotExistWithHint { .. } => { + CommandError::WargHint("error publishing package".to_string(), e) + } + _ => CommandError::WargClient("error publishing package".to_string(), e), + })?; client .wait_for_publish(options.name, &record_id, Duration::from_secs(1)) - .await?; + .await + .map_err(|e| match &e { + ClientError::PackageDoesNotExistWithHint { .. } => { + CommandError::WargHint("error publishing package".to_string(), e) + } + _ => CommandError::WargClient("error publishing package".to_string(), e), + })?; config.terminal().status( "Published", @@ -1166,9 +1197,10 @@ pub async fn update_lockfile( lock_update_allowed: bool, locked: bool, dry_run: bool, -) -> Result<()> { + retry: Option<&Retry>, +) -> Result<(), CommandError> { // Read the current lock file and generate a new one - let map = create_resolution_map(config, packages, None, network_allowed).await?; + let map = create_resolution_map(config, packages, None, network_allowed, retry).await?; let file_lock = acquire_lock_file_ro(config.terminal(), metadata)?; let orig_lock_file = file_lock diff --git a/src/registry.rs b/src/registry.rs index 640470ea..18a1acd7 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -4,11 +4,12 @@ use crate::{config::Config, metadata::ComponentMetadata}; use anyhow::Result; use cargo_component_core::{ lock::{LockFile, LockFileResolver, LockedPackage, LockedPackageVersion}, - registry::{DependencyResolution, DependencyResolutionMap, DependencyResolver}, + registry::{CommandError, DependencyResolution, DependencyResolutionMap, DependencyResolver}, }; use cargo_metadata::PackageId; use semver::Version; use std::collections::HashMap; +use warg_client::Retry; use warg_crypto::hash::AnyHash; use warg_protocol::registry::PackageName; @@ -27,12 +28,18 @@ impl<'a> PackageDependencyResolution<'a> { /// Creates a new package dependency resolution for the given package. /// /// Returns `Ok(None)` if the package is not a component package. + /// + /// The `Retry` argument indicates that this was created after an interactive retry. + /// This occurs when the CLI attempted to leverage a package from the wrong registry. + /// The server responds with a hint as to which registry is used and the client maps that package namespace to the domain provided. + /// After this, the CLI command is retried pub async fn new( config: &Config, metadata: &'a ComponentMetadata, lock_file: Option>, network_allowed: bool, - ) -> Result> { + retry: Option<&Retry>, + ) -> Result, CommandError> { Ok(Self { metadata, target_resolutions: Self::resolve_target_deps( @@ -40,9 +47,11 @@ impl<'a> PackageDependencyResolution<'a> { metadata, lock_file, network_allowed, + retry, ) .await?, - resolutions: Self::resolve_deps(config, metadata, lock_file, network_allowed).await?, + resolutions: Self::resolve_deps(config, metadata, lock_file, network_allowed, retry) + .await?, }) } @@ -58,22 +67,18 @@ impl<'a> PackageDependencyResolution<'a> { metadata: &ComponentMetadata, lock_file: Option>, network_allowed: bool, - ) -> Result { + retry: Option<&Retry>, + ) -> Result { let target_deps = metadata.section.target.dependencies(); if target_deps.is_empty() { return Ok(Default::default()); } - let mut resolver = DependencyResolver::new( - config.warg(), - &metadata.section.registries, - lock_file, - config.terminal(), - network_allowed, - )?; + let mut resolver = + DependencyResolver::new(config.warg(), lock_file, config.terminal(), network_allowed)?; for (name, dependency) in target_deps.iter() { - resolver.add_dependency(name, dependency).await?; + resolver.add_dependency(name, dependency, retry).await?; } resolver.resolve().await @@ -84,21 +89,17 @@ impl<'a> PackageDependencyResolution<'a> { metadata: &ComponentMetadata, lock_file: Option>, network_allowed: bool, - ) -> Result { + retry: Option<&Retry>, + ) -> Result { if metadata.section.dependencies.is_empty() { return Ok(Default::default()); } - let mut resolver = DependencyResolver::new( - config.warg(), - &metadata.section.registries, - lock_file, - config.terminal(), - network_allowed, - )?; + let mut resolver = + DependencyResolver::new(config.warg(), lock_file, config.terminal(), network_allowed)?; for (name, dependency) in &metadata.section.dependencies { - resolver.add_dependency(name, dependency).await?; + resolver.add_dependency(name, dependency, retry).await?; } resolver.resolve().await diff --git a/tests/publish.rs b/tests/publish.rs index b8c05645..3b1fdcb1 100644 --- a/tests/publish.rs +++ b/tests/publish.rs @@ -90,7 +90,7 @@ world foo { .cargo_component("publish") .env("CARGO_COMPONENT_PUBLISH_KEY", test_signing_key()) .assert() - .stderr(contains("error: package `test:foo` does not exist")) + .stderr(contains("package `test:foo` does not exist")) .failure(); Ok(()) @@ -201,7 +201,7 @@ async fn it_publishes_with_registry_metadata() -> Result<()> { validate_component(&project.release_wasm("foo"))?; - let client = Client::new_with_config(None, &config, None)?; + let client = Client::new_with_config(None, &config, None).await?; let download = client .download_exact(&PackageName::new("test:foo")?, &Version::parse("0.1.0")?) .await?; diff --git a/tests/support/mod.rs b/tests/support/mod.rs index e92d8b74..9f018c19 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -67,7 +67,7 @@ pub async fn publish( content: Vec, init: bool, ) -> Result<()> { - let client = FileSystemClient::new_with_config(None, config, None)?; + let client = FileSystemClient::new_with_config(None, config, None).await?; let digest = client .content()