From d8030c3ef17f6642d9e0961a4f861e1220c40cfa Mon Sep 17 00:00:00 2001 From: Daniel Macovei Date: Wed, 17 Apr 2024 14:13:12 -0500 Subject: [PATCH] support dependencies across multiple namespaces in wit and cargo component --- Cargo.lock | 89 ++++++++---- Cargo.toml | 12 +- crates/core/Cargo.toml | 1 + crates/core/src/registry.rs | 144 +++++++++++++------- crates/wit/Cargo.toml | 1 + crates/wit/src/bin/wit.rs | 103 +++++++++++--- crates/wit/src/commands/add.rs | 25 ++-- crates/wit/src/commands/build.rs | 13 +- crates/wit/src/commands/init.rs | 18 ++- crates/wit/src/commands/key.rs | 11 +- crates/wit/src/commands/publish.rs | 15 +-- crates/wit/src/commands/update.rs | 19 ++- crates/wit/src/lib.rs | 80 ++++++----- crates/wit/tests/publish.rs | 10 +- src/bin/cargo-component.rs | 209 +++++++++++++++++++++++------ src/commands/add.rs | 22 ++- src/commands/key.rs | 12 +- src/commands/new.rs | 25 ++-- src/commands/publish.rs | 20 ++- src/commands/update.rs | 6 +- src/lib.rs | 64 ++++++--- src/registry.rs | 38 +++--- tests/publish.rs | 2 +- tests/support/mod.rs | 2 +- 24 files changed, 651 insertions(+), 290 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 68e11537..32dc0a65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -549,6 +549,7 @@ dependencies = [ "cargo-config2", "cargo_metadata", "clap", + "dialoguer", "futures", "heck", "indexmap 2.2.5", @@ -600,6 +601,7 @@ dependencies = [ "secrecy", "semver", "serde 1.0.197", + "thiserror", "tokio", "toml_edit 0.22.6", "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.33" @@ -3172,6 +3206,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" @@ -3357,18 +3397,18 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", @@ -3808,9 +3848,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?branch=namespace-enhancements#00819c6d45730316432eacf2b3fa5df63a08584a" dependencies = [ "indexmap 2.2.5", "itertools 0.12.1", @@ -3823,9 +3862,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?branch=namespace-enhancements#00819c6d45730316432eacf2b3fa5df63a08584a" dependencies = [ "anyhow", "async-recursion", @@ -3867,9 +3905,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?branch=namespace-enhancements#00819c6d45730316432eacf2b3fa5df63a08584a" dependencies = [ "anyhow", "indexmap 2.2.5", @@ -3881,9 +3918,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?branch=namespace-enhancements#00819c6d45730316432eacf2b3fa5df63a08584a" dependencies = [ "anyhow", "base64", @@ -3902,9 +3938,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?branch=namespace-enhancements#00819c6d45730316432eacf2b3fa5df63a08584a" dependencies = [ "anyhow", "pbjson", @@ -3921,9 +3956,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?branch=namespace-enhancements#00819c6d45730316432eacf2b3fa5df63a08584a" dependencies = [ "anyhow", "base64", @@ -3944,9 +3978,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?branch=namespace-enhancements#00819c6d45730316432eacf2b3fa5df63a08584a" dependencies = [ "anyhow", "axum", @@ -3975,9 +4008,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?branch=namespace-enhancements#00819c6d45730316432eacf2b3fa5df63a08584a" dependencies = [ "anyhow", "indexmap 2.2.5", @@ -4436,6 +4468,7 @@ dependencies = [ "bytes", "cargo-component-core", "clap", + "dialoguer", "futures", "indexmap 2.2.5", "log", diff --git a/Cargo.toml b/Cargo.toml index abb03e5d..d7c2522a 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 } [dev-dependencies] assert_cmd = { workspace = true } @@ -68,11 +69,11 @@ members = ["crates/core", "crates/wit"] [workspace.dependencies] cargo-component-core = { path = "crates/core", version = "0.10.1" } -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", branch = "namespace-enhancements" } +warg-crypto = { git = "https://github.com/bytecodealliance/registry", branch = "namespace-enhancements" } +warg-client = { git = "https://github.com/bytecodealliance/registry", branch = "namespace-enhancements" } +warg-credentials = { git = "https://github.com/bytecodealliance/registry", branch = "namespace-enhancements" } +warg-server = { git = "https://github.com/bytecodealliance/registry", branch = "namespace-enhancements" } anyhow = "1.0.80" clap = { version = "4.5.1", features = ["derive"] } toml_edit = { version = "0.22.6", features = ["serde"] } @@ -90,6 +91,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.19" 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..a219d8f4 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,44 @@ use warg_protocol::registry; use wit_component::DecodedWasm; use wit_parser::{PackageId, PackageName, Resolve, UnresolvedPackage, WorldId}; +#[derive(Debug, Error)] +/// Error for WIT commands +pub enum WargError { + /// General errors + #[error("Error: `{0}`")] + General(anyhow::Error), + /// Client Error + #[error("Warg Client Error: {0}")] + WargClient(ClientError), + /// Client Error With Hint + #[error("Warg Client Error: {0}")] + WargHint(ClientError), +} + +/// Error from warg client +pub struct WargClientError(pub ClientError); + +impl std::fmt::Debug for WargClientError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("WargClientError").field(&self.0).finish() + } +} + +impl From for WargError { + fn from(value: anyhow::Error) -> Self { + WargError::General(value) + } +} + +impl From for WargError { + fn from(value: WargClientError) -> Self { + match &value.0 { + ClientError::PackageDoesNotExistWithHint { .. } => WargError::WargHint(value.0.into()), + _ => WargError::WargClient(value.0.into()), + } + } +} + /// The name of the default registry. pub const DEFAULT_REGISTRY_NAME: &str = "default"; @@ -65,16 +104,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 +124,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 +486,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 +497,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 +516,8 @@ impl<'a> DependencyResolver<'a> { &mut self, name: &'a registry::PackageName, dependency: &'a Dependency, - ) -> Result<()> { + retry: Option<&Retry>, + ) -> Result<(), WargError> { match dependency { Dependency::Package(package) => { // Dependency comes from a registry, add a dependency to the resolver @@ -487,30 +531,28 @@ impl<'a> DependencyResolver<'a> { .transpose() }) { Some(Ok(locked)) => Some(locked), - Some(Err(e)) => return Err(e), + Some(Err(e)) => { + return Err(WargError::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 + .map_err(|e| WargClientError(e.into()))?; } Dependency::Local(p) => { // A local path dependency, insert a resolution immediately @@ -532,7 +574,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, @@ -560,7 +602,7 @@ impl<'a> DependencyResolver<'a> { registries: &mut IndexMap<&'a str, Registry<'a>>, terminal: &Terminal, network_allowed: bool, - ) -> Result> { + ) -> Result, WargError> { let task_count = registries .iter() .filter(|(_, r)| !r.upserts.is_empty()) @@ -570,7 +612,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 +636,13 @@ 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| WargClientError(e)), + ) })) } @@ -604,9 +655,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.map_err(|e| WargError::WargClient(e.0))?; log::info!("package logs successfully updated for component registry `{name}`"); finished += 1; @@ -875,16 +924,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 e46244e8..724c58c8 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..2c7217c3 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::WargError, + terminal::{Color, Terminal, Verbosity}, +}; use clap::Parser; +use dialoguer::{theme::ColorfulTheme, Confirm}; use std::process::exit; +use warg_client::{with_interactive_retry, ClientError, Retry}; use wit::commands::{ AddCommand, BuildCommand, InitCommand, KeyCommand, PublishCommand, UpdateCommand, }; @@ -38,20 +43,88 @@ pub enum Command { 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, - Command::Key(cmd) => cmd.exec().await, - Command::Update(cmd) => cmd.exec().await, - } { - let terminal = Terminal::new(Verbosity::Normal, Color::Auto); - terminal.error(format!("{e:?}"))?; - exit(1); - } + with_interactive_retry(|retry: Option| async { + let app = Wit::parse(); + if let Err(e) = match app.command { + Command::Init(cmd) => cmd.exec(), + Command::Add(cmd) => cmd.exec(retry).await, + Command::Build(cmd) => cmd.exec(retry).await, + Command::Publish(cmd) => cmd.exec(retry).await, + Command::Key(cmd) => cmd.exec().await, + Command::Update(cmd) => cmd.exec(retry).await, + } + { + match e { + WargError::General(e) => { + let terminal = Terminal::new(Verbosity::Normal, Color::Auto); + terminal.error(e)?; + exit(1); + } + WargError::WargClient(e) => { + let terminal = Terminal::new(Verbosity::Normal, Color::Auto); + terminal.error(e)?; + exit(1); + } + WargError::WargHint(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 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); + } + } + } + } + } + } + } + Ok(()) + }).await?; Ok(()) } diff --git a/crates/wit/src/commands/add.rs b/crates/wit/src/commands/add.rs index 93eb4611..e391a71b 100644 --- a/crates/wit/src/commands/add.rs +++ b/crates/wit/src/commands/add.rs @@ -1,25 +1,25 @@ 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::{Dependency, DependencyResolution, DependencyResolver, RegistryPackage, WargError}, 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 +30,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 +78,7 @@ pub struct AddCommand { impl AddCommand { /// Executes the command. - pub async fn exec(self) -> Result<()> { + pub async fn exec(self, retry: Option) -> Result<(), WargError> { log::debug!("executing add command"); let (mut config, config_path) = Config::from_default_file()? @@ -84,7 +86,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 +108,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..cfb738ba 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}, + WargError, }; 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<(), WargError> { 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..d2bf15a4 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}, + WargError, +}; +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<(), WargError> { 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..a8ccfda4 100644 --- a/crates/wit/src/commands/key.rs +++ b/crates/wit/src/commands/key.rs @@ -1,3 +1,4 @@ +use crate::WargError; 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<(), WargError> { 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<(), WargError> { 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<(), WargError> { 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<(), WargError> { 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<(), WargError> { 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..a1013a50 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, PublishOptions, WargError, }; 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<(), WargError> { 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..ddbd2b99 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}, + WargError, +}; 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<(), WargError> { 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..6f7651ec 100644 --- a/crates/wit/src/lib.rs +++ b/crates/wit/src/lib.rs @@ -2,24 +2,29 @@ #![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, DecodedDependency, DependencyResolutionMap, DependencyResolver, + WargClientError, WargError, + }, 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}, + 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 +35,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 +52,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 +86,7 @@ async fn resolve_dependencies( fn parse_wit_package( dir: &Path, dependencies: &DependencyResolutionMap, -) -> Result<(Resolve, PackageId)> { +) -> Result<(Resolve, PackageId), WargError> { let mut merged = Resolve::default(); // Start by decoding all of the dependencies @@ -89,10 +94,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 +155,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 +164,7 @@ fn parse_wit_package( deps: &'a IndexMap, order: &mut IndexSet, visiting: &mut HashSet<&'a PackageName>, - ) -> Result<()> { + ) -> Result<(), WargError> { if order.contains(dep.package_name()) { return Ok(()); } @@ -180,7 +181,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 +206,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 +230,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), WargError> { 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 +261,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 +317,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<(), WargError> { let (name, bytes) = build_wit_package( options.config, options.config_path, options.warg_config, terminal, + retry.as_ref(), ) .await?; @@ -328,8 +338,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 +365,15 @@ 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| WargClientError(e))?; + client .wait_for_publish(name, &record_id, Duration::from_secs(1)) - .await?; + .await + .map_err(|e| WargClientError(e))?; terminal.status( "Published", @@ -379,12 +393,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..0b68daee 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::WargError, + terminal::{Color, Terminal, Verbosity}, +}; use clap::{CommandFactory, Parser}; +use dialoguer::{theme::ColorfulTheme, Confirm}; +use warg_client::{with_interactive_retry, ClientError, Retry}; fn version() -> &'static str { option_env!("CARGO_VERSION_INFO").unwrap_or(env!("CARGO_PKG_VERSION")) @@ -109,19 +116,90 @@ 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() { - CargoComponent::Component(cmd) | CargoComponent::Command(cmd) => match cmd { - Command::Add(cmd) => cmd.exec().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, - }, - } { - let terminal = Terminal::new(Verbosity::Normal, Color::Auto); - terminal.error(format!("{e:?}"))?; - std::process::exit(1); - } + with_interactive_retry(|retry: Option| async { + if let Err(e) = match CargoComponent::parse() { + CargoComponent::Component(cmd) | CargoComponent::Command(cmd) => match cmd { + Command::Add(cmd) => cmd.exec(retry).await, + Command::Key(cmd) => cmd.exec().await, + Command::New(cmd) => cmd.exec(retry).await, + Command::Update(cmd) => cmd.exec(retry).await, + Command::Publish(cmd) => cmd.exec(retry).await, + }, + } { + match e { + WargError::General(e) => { + let terminal = Terminal::new(Verbosity::Normal, Color::Auto); + terminal.error(e)?; + exit(1); + } + WargError::WargClient(e) => { + let terminal = Terminal::new(Verbosity::Normal, Color::Auto); + terminal.error(e)?; + exit(1); + } + WargError::WargHint(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); + } + } + } + } + } + }; + } + Ok(()) + }).await?; } // Check for explicitly unsupported commands (e.g. those that deal with crates.io) @@ -146,8 +224,9 @@ async fn main() -> Result<()> { _ => { // Not a built-in command, run the cargo command - let cargo_args = CargoArguments::parse()?; - let config = Config::new(Terminal::new( + with_interactive_retry(|retry: Option| async move { + let cargo_args = CargoArguments::parse()?; + let config = Config::new(Terminal::new( if cargo_args.quiet { Verbosity::Quiet } else { @@ -157,36 +236,82 @@ async fn main() -> Result<()> { } }, cargo_args.color.unwrap_or_default(), - ))?; + ))?; - let metadata = load_metadata(cargo_args.manifest_path.as_deref())?; - let packages = load_component_metadata( - &metadata, - cargo_args.packages.iter(), - cargo_args.workspace, - )?; + let metadata = load_metadata(cargo_args.manifest_path.as_deref())?; + let packages = load_component_metadata( + &metadata, + cargo_args.packages.iter(), + cargo_args.workspace, + )?; - if packages.is_empty() { - bail!( - "manifest `{path}` contains no package or the workspace has no members", - path = metadata.workspace_root.join("Cargo.toml") - ); - } + if packages.is_empty() { + bail!( + "manifest `{path}` contains no package or the workspace has no members", + path = metadata.workspace_root.join("Cargo.toml") + ); + } let spawn_args: Vec<_> = std::env::args().skip(1).collect(); - if let Err(e) = run_cargo_command( - &config, - &metadata, - &packages, - subcommand.as_deref(), - &cargo_args, - &spawn_args, - ) - .await - { - config.terminal().error(format!("{e:?}"))?; - std::process::exit(1); - } + if let Err(e) = run_cargo_command( + &config, + &metadata, + &packages, + detect_subcommand().as_deref(), + &cargo_args, + &spawn_args, + retry.as_ref(), + ) + .await + { + match e { + WargError::General(e) => { + let terminal = Terminal::new(Verbosity::Normal, Color::Auto); + terminal.error(e)?; + exit(1); + } + WargError::WargClient(e) => { + let terminal = Terminal::new(Verbosity::Normal, Color::Auto); + terminal.error(e)?; + exit(1); + } + WargError::WargHint(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?; + } + } + } + } + }; + } + Ok(()) + }).await?; } } diff --git a/src/commands/add.rs b/src/commands/add.rs index 27f9945b..f4de6f8f 100644 --- a/src/commands/add.rs +++ b/src/commands/add.rs @@ -7,7 +7,7 @@ use crate::{ use anyhow::{bail, Context, Result}; use cargo_component_core::{ command::CommonOptions, - registry::{Dependency, DependencyResolution, DependencyResolver, RegistryPackage}, + registry::{Dependency, DependencyResolution, DependencyResolver, RegistryPackage, WargError}, VersionedPackageName, }; use cargo_metadata::Package; @@ -18,6 +18,7 @@ use std::{ path::{Path, PathBuf}, }; use toml_edit::{value, Document, InlineTable, Item, Table, Value}; +use warg_client::Retry; use warg_protocol::registry::PackageName; /// Add a dependency for a WebAssembly component @@ -63,7 +64,7 @@ pub struct AddCommand { impl AddCommand { /// Executes the command - pub async fn exec(self) -> Result<()> { + pub async fn exec(self, retry: Option) -> Result<(), WargError> { let config = Config::new(self.common.new_terminal())?; let metadata = load_metadata(self.manifest_path.as_deref())?; @@ -104,7 +105,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 +121,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 +138,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..2650329c 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::WargError, 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<(), WargError> { 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<(), WargError> { 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<(), WargError> { 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<(), WargError> { 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<(), WargError> { 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 d74e37da..1f476e6c 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::{ + Dependency, DependencyResolution, DependencyResolver, RegistryResolution, WargError, + }, }; use clap::Args; use heck::ToKebabCase; @@ -16,6 +18,7 @@ use std::{ }; use toml_edit::{table, value, Document, 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<(), WargError> { 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)>, WargError> { 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..0715758b 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, WargError}, +}; 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<(), WargError> { 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..bac54cf4 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::WargError}; 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<(), WargError> { 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 91ad082f..dcfcfcc9 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, WargClientError, WargError}, terminal::Colors, }; use cargo_config2::{PathAndArgs, TargetTripleRef}; @@ -30,7 +30,10 @@ use std::{ process::{Command, Stdio}, time::{Duration, SystemTime}, }; -use warg_client::storage::{ContentStorage, PublishEntry, PublishInfo}; +use warg_client::{ + storage::{ContentStorage, PublishEntry, PublishInfo}, + Retry, +}; use warg_crypto::signing::PrivateKey; use warg_protocol::registry::PackageName; use wasm_metadata::{Link, LinkType, RegistryMetadata}; @@ -134,8 +137,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, WargError> { + let import_name_map = generate_bindings(config, metadata, packages, cargo_args, retry).await?; let cargo_path = std::env::var("CARGO") .map(PathBuf::from) @@ -194,7 +198,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()); } } @@ -636,7 +640,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}`", @@ -649,7 +654,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. @@ -714,8 +719,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>, WargError> { + 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() @@ -733,8 +739,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"); @@ -774,12 +786,14 @@ async fn create_resolution_map<'a>( packages: &'a [PackageComponentMetadata<'_>], lock_file: Option>, network_allowed: bool, -) -> Result> { + retry: Option<&Retry>, +) -> Result, WargError> { 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); } @@ -1065,7 +1079,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<(), WargError> { if options.dry_run { config .terminal() @@ -1073,8 +1091,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!( @@ -1116,10 +1133,14 @@ 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| WargClientError(e))?; client .wait_for_publish(options.name, &record_id, Duration::from_secs(1)) - .await?; + .await + .map_err(|e| WargClientError(e))?; config.terminal().status( "Published", @@ -1144,9 +1165,10 @@ pub async fn update_lockfile( lock_update_allowed: bool, locked: bool, dry_run: bool, -) -> Result<()> { + retry: Option<&Retry>, +) -> Result<(), WargError> { // 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..772682eb 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::{DependencyResolution, DependencyResolutionMap, DependencyResolver, WargError}, }; 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; @@ -32,7 +33,8 @@ impl<'a> PackageDependencyResolution<'a> { metadata: &'a ComponentMetadata, lock_file: Option>, network_allowed: bool, - ) -> Result> { + retry: Option<&Retry>, + ) -> Result, WargError> { Ok(Self { metadata, target_resolutions: Self::resolve_target_deps( @@ -40,9 +42,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 +62,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 +84,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..15adf1e4 100644 --- a/tests/publish.rs +++ b/tests/publish.rs @@ -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 779510df..26976f87 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()