From fc6471e46c8a07fa226753b6eee202048efe9f49 Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Wed, 15 May 2024 19:24:05 +0200 Subject: [PATCH 01/14] feat(cli): Use registry templates to create new apps --- Cargo.lock | 193 +++++- lib/backend-api/src/query.rs | 33 +- lib/backend-api/src/types.rs | 58 ++ lib/cli/Cargo.toml | 3 +- lib/cli/src/commands/app/create.rs | 646 +++++++----------- lib/cli/src/commands/app/deploy.rs | 34 +- lib/cli/src/utils/mod.rs | 147 ++-- lib/cli/src/utils/package_wizard/mod.rs | 852 ++++++++++++------------ lib/cli/src/utils/prompts.rs | 155 +++-- 9 files changed, 1136 insertions(+), 985 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7c505068ff1..6fdcbb8b63f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if 1.0.0", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.7.8" @@ -450,6 +461,27 @@ dependencies = [ "serde", ] +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "camino" version = "1.1.6" @@ -654,6 +686,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.1" @@ -921,6 +962,21 @@ dependencies = [ "build_const", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.4.0" @@ -1254,6 +1310,12 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +[[package]] +name = "deflate64" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83ace6c86376be0b6cdcf3fb41882e81d94b31587573d1cfa9d01cd06bba210d" + [[package]] name = "deranged" version = "0.3.11" @@ -1407,6 +1469,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.61", +] + [[package]] name = "distance" version = "0.4.0" @@ -1695,6 +1768,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", + "libz-ng-sys", "miniz_oxide", ] @@ -2708,6 +2782,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libz-ng-sys" +version = "1.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6409efc61b12687963e602df8ecf70e8ddacf95bc6576bcf16e3ac6328083c5" +dependencies = [ + "cmake", + "libc", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -2830,7 +2914,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aba8ecb0450dfabce4ad72085eed0a75dffe8f21f7ada05638564ea9db2d7fb1" dependencies = [ "byteorder", - "crc", + "crc 1.8.1", +] + +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc 3.2.1", ] [[package]] @@ -4669,6 +4763,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "simdutf8" version = "0.1.4" @@ -5707,6 +5807,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + [[package]] name = "typenum" version = "1.17.0" @@ -6573,6 +6679,7 @@ dependencies = [ "wasmer-wasix", "wasmer-wast", "webc", + "zip", ] [[package]] @@ -6882,7 +6989,7 @@ dependencies = [ "indicatif", "lazy_static", "log 0.4.21", - "lzma-rs", + "lzma-rs 0.2.0", "minisign", "pretty_assertions", "rand", @@ -7665,3 +7772,85 @@ name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.61", +] + +[[package]] +name = "zip" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c700ea425e148de30c29c580c1f9508b93ca57ad31c9f4e96b83c194c37a7a8f" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils 0.8.19", + "deflate64", + "displaydoc", + "flate2", + "hmac", + "indexmap 2.2.6", + "lzma-rs 0.3.0", + "pbkdf2", + "rand", + "sha1", + "thiserror", + "time 0.3.36", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zopfli" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1f48f3508a3a3f2faee01629564400bc12260f6214a056d06a3aaaa6ef0736" +dependencies = [ + "crc32fast", + "log 0.4.21", + "simd-adler32", + "typed-arena", +] + +[[package]] +name = "zstd" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.10+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/lib/backend-api/src/query.rs b/lib/backend-api/src/query.rs index c5867b2c772..7cf09ca9f25 100644 --- a/lib/backend-api/src/query.rs +++ b/lib/backend-api/src/query.rs @@ -12,7 +12,8 @@ use wasmer_config::package::PackageIdent; use crate::{ types::{ self, CreateNamespaceVars, DeployApp, DeployAppConnection, DeployAppVersion, - DeployAppVersionConnection, DnsDomain, GetCurrentUserWithAppsVars, GetDeployAppAndVersion, + DeployAppVersionConnection, DnsDomain, GetAppTemplateFromSlugVariables, + GetAppTemplatesQueryVariables, GetCurrentUserWithAppsVars, GetDeployAppAndVersion, GetDeployAppVersionsVars, GetNamespaceAppsVars, GetSignedUrlForPackageUploadVariables, Log, LogStream, PackageVersionConnection, PublishDeployAppVars, PushPackageReleasePayload, SignedUrl, TagPackageReleasePayload, UpsertDomainFromZoneFileVars, @@ -55,6 +56,36 @@ pub async fn fetch_webc_package( webc::compat::Container::from_bytes(data).context("failed to parse webc package") } +/// Fetch app templates. +pub async fn fetch_app_template_from_slug( + client: &WasmerClient, + slug: String, +) -> Result, anyhow::Error> { + client + .run_graphql_strict(types::GetAppTemplateFromSlug::build( + GetAppTemplateFromSlugVariables { slug }, + )) + .await + .map(|v| v.get_app_template) +} + +/// Fetch app templates. +pub async fn fetch_app_templates( + client: &WasmerClient, + category_slug: String, + first: i32, +) -> Result, anyhow::Error> { + client + .run_graphql_strict(types::GetAppTemplatesQuery::build( + GetAppTemplatesQueryVariables { + category_slug, + first, + }, + )) + .await + .map(|r| r.get_app_templates) +} + /// Get a signed URL to upload packages. pub async fn get_signed_url_for_package_upload( client: &WasmerClient, diff --git a/lib/backend-api/src/types.rs b/lib/backend-api/src/types.rs index ca6d8800c19..83af3ae8c1d 100644 --- a/lib/backend-api/src/types.rs +++ b/lib/backend-api/src/types.rs @@ -126,6 +126,64 @@ mod queries { pub package: Package, } + #[derive(cynic::QueryVariables, Debug)] + pub struct GetAppTemplateFromSlugVariables { + pub slug: String, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "Query", variables = "GetAppTemplateFromSlugVariables")] + pub struct GetAppTemplateFromSlug { + #[arguments(slug: $slug)] + pub get_app_template: Option, + } + #[derive(cynic::QueryVariables, Debug)] + pub struct GetAppTemplatesQueryVariables { + pub category_slug: String, + pub first: i32, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "Query", variables = "GetAppTemplatesQueryVariables")] + pub struct GetAppTemplatesQuery { + #[arguments(categorySlug: $category_slug, first: $first)] + pub get_app_templates: Option, + } + + #[derive(cynic::QueryFragment, Debug)] + pub struct AppTemplateConnection { + pub edges: Vec>, + pub page_info: PageInfo, + } + + #[derive(cynic::QueryFragment, Debug)] + pub struct AppTemplateEdge { + pub node: Option, + pub cursor: String, + } + + #[derive(cynic::QueryFragment, Debug)] + pub struct AppTemplate { + pub demo_url: String, + pub language: String, + pub name: String, + pub framework: String, + pub created_at: DateTime, + pub description: String, + pub id: cynic::Id, + pub is_public: bool, + pub repo_license: String, + pub readme: String, + pub repo_url: String, + pub slug: String, + pub updated_at: DateTime, + pub use_cases: Jsonstring, + } + + #[derive(cynic::Scalar, Debug, Clone)] + #[cynic(graphql_type = "JSONString")] + pub struct Jsonstring(pub String); + #[derive(cynic::QueryVariables, Debug)] pub struct GetPackageReleaseVars { pub hash: String, diff --git a/lib/cli/Cargo.toml b/lib/cli/Cargo.toml index b918d8018f4..4746ede9ccd 100644 --- a/lib/cli/Cargo.toml +++ b/lib/cli/Cargo.toml @@ -185,7 +185,7 @@ tldextract = "0.6.0" hex = "0.4.3" flate2 = "1.0.25" cargo_metadata = "0.15.2" -tar = "0.4.38" +tar = "0.4.40" bytes = "1" thiserror = "1.0.37" log = "0.4.17" @@ -229,6 +229,7 @@ tun-tap = { version = "0.1.3", features = ["tokio"], optional = true } clap_complete = "4.5.2" clap_mangen = "0.2.20" +zip = "1.2.3" # NOTE: Must use different features for clap because the "color" feature does not # work on wasi due to the anstream dependency not compiling. diff --git a/lib/cli/src/commands/app/create.rs b/lib/cli/src/commands/app/create.rs index 4a3299b1cf8..3f4a184d2d3 100644 --- a/lib/cli/src/commands/app/create.rs +++ b/lib/cli/src/commands/app/create.rs @@ -3,23 +3,17 @@ use crate::{ commands::AsyncCliCommand, opts::{ApiOpts, ItemFormatOpts, WasmerEnv}, - utils::{ - load_package_manifest, - package_wizard::{CreateMode, PackageType, PackageWizard}, - }, + utils::load_package_manifest, }; use anyhow::Context; use colored::Colorize; use dialoguer::{theme::ColorfulTheme, Confirm, Select}; use is_terminal::IsTerminal; -use std::{collections::HashMap, env, path::PathBuf, str::FromStr}; -use wasmer_api::{types::UserWithNamespaces, WasmerClient}; -use wasmer_config::{ - app::AppConfigV1, - package::{NamedPackageIdent, PackageSource, Tag}, -}; +use std::{collections::HashMap, env, io::Cursor, path::PathBuf, str::FromStr}; +use wasmer_api::{types::AppTemplate, WasmerClient}; +use wasmer_config::{app::AppConfigV1, package::PackageSource}; -use super::deploy::CmdAppDeploy; +use super::{deploy::CmdAppDeploy, util::login_user}; async fn write_app_config(app_config: &AppConfigV1, dir: Option) -> anyhow::Result<()> { let raw_app_config = app_config.clone().to_yaml()?; @@ -41,8 +35,30 @@ async fn write_app_config(app_config: &AppConfigV1, dir: Option) -> any /// Create a new Edge app. #[derive(clap::Parser, Debug)] pub struct CmdAppCreate { - #[clap(name = "type", short = 't', long)] - pub template: Option, + /// A reference to the template to use. + /// + /// It can be either an URL to a github repository - like + /// `https://github.com/wasmer-examples/php-wasmer-starter` - or the name of a template that + /// will be searched for in the selected registry, like `astro-starter`. + #[clap( + long, + conflicts_with = "package", + conflicts_with = "use_local_manifest" + )] + pub template: Option, + + /// Name of the package to use. + #[clap( + long, + conflicts_with = "template", + conflicts_with = "use_local_manifest" + )] + pub package: Option, + + /// Whether or not to search (and use) a local manifest. + #[clap(long, conflicts_with = "template", conflicts_with = "package")] + pub use_local_manifest: bool, + /// Whether or not to deploy the application once it is created. /// /// If selected, this might entail the step of publishing the package related to the @@ -90,17 +106,13 @@ pub struct CmdAppCreate { #[allow(missing_docs)] pub fmt: ItemFormatOpts, - /// Name of the package to use. - #[clap(long, short = 'p')] - pub package: Option, - - /// Whether or not to search (and use) a local manifest. - #[clap(long)] - pub use_local_manifest: bool, - /// Name to use when creating a new package from a template. #[clap(long)] pub new_package_name: Option, + + /// Don't print any message. + #[clap(long)] + pub quiet: bool, } impl CmdAppCreate { @@ -145,7 +157,7 @@ impl CmdAppCreate { ) } - async fn get_owner(&self) -> anyhow::Result { + async fn get_owner(&self, client: &WasmerClient) -> anyhow::Result { if let Some(owner) = &self.owner { return Ok(owner.clone()); } @@ -156,21 +168,12 @@ impl CmdAppCreate { } if !self.offline { - match self.api.client() { - Ok(client) => { - let user = - wasmer_api::query::current_user_with_namespaces(&client, None).await?; - crate::utils::prompts::prompt_for_namespace( - "Who should own this app?", - None, - Some(&user), - ) - } - Err(e) => anyhow::bail!( - "Can't determine user info: {e}. Please, user `wasmer login` before deploying an - app or use the --owner flag to specify the owner of the app to deploy." - ), - } + let user = wasmer_api::query::current_user_with_namespaces(&client, None).await?; + crate::utils::prompts::prompt_for_namespace( + "Who should own this app?", + None, + Some(&user), + ) } else { anyhow::bail!( "Please, user `wasmer login` before deploying an app or use the --owner @@ -213,10 +216,9 @@ impl CmdAppCreate { }; if self.use_local_manifest || ask_confirmation()? { - let app_config = - self.get_app_config(owner, app_name, manifest_path.to_string_lossy().as_ref()); + let app_config = self.get_app_config(owner, app_name, "."); write_app_config(&app_config, self.app_dir_path.clone()).await?; - self.try_deploy(owner).await?; + self.try_deploy(owner, app_name).await?; return Ok(true); } @@ -231,7 +233,7 @@ impl CmdAppCreate { if let Some(pkg) = &self.package { let app_config = self.get_app_config(owner, app_name, pkg); write_app_config(&app_config, self.app_dir_path.clone()).await?; - self.try_deploy(owner).await?; + self.try_deploy(owner, app_name).await?; return Ok(true); } else if !self.non_interactive { let theme = ColorfulTheme::default(); @@ -241,7 +243,7 @@ impl CmdAppCreate { let app_config = self.get_app_config(owner, app_name, &package_name); write_app_config(&app_config, self.app_dir_path.clone()).await?; - self.try_deploy(owner).await?; + self.try_deploy(owner, app_name).await?; return Ok(true); } else { eprintln!( @@ -253,109 +255,202 @@ impl CmdAppCreate { Ok(false) } - async fn create_from_template(&self, owner: &str, app_name: &str) -> anyhow::Result { - let template = match self.template { - Some(t) => t, - None => { - if !self.non_interactive { - let theme = ColorfulTheme::default(); - let index = dialoguer::Select::with_theme(&theme) - .with_prompt("App type") - .default(0) - .items(&[ - "Static website", - "HTTP server", - "Browser shell", - "JS Worker (WinterJS)", - "Python Application", - ]) - .interact()?; - match index { - 0 => AppType::StaticWebsite, - 1 => AppType::HttpServer, - 2 => AppType::BrowserShell, - 3 => AppType::JsWorker, - 4 => AppType::PyApplication, - x => panic!("unhandled app type index '{x}'"), - } - } else { - return Ok(false); - } + // A utility function used to fetch the URL of the template to use. + async fn get_template_url(&self, client: &WasmerClient) -> anyhow::Result { + let mut url = if let Some(template) = &self.template { + if let Ok(url) = url::Url::parse(&template) { + url + } else if let Some(template) = + wasmer_api::query::fetch_app_template_from_slug(client, template.clone()).await? + { + url::Url::parse(&template.repo_url)? + } else { + anyhow::bail!("Template '{}' not found in the registry", template) + } + } else { + if self.non_interactive { + anyhow::bail!("No template selected") } - }; - let allow_local_package = match template { - AppType::HttpServer => true, - AppType::StaticWebsite => true, - AppType::BrowserShell => false, - AppType::JsWorker => true, - AppType::PyApplication => true, - }; + let templates: Vec = + wasmer_api::query::fetch_app_templates(client, String::new(), 20) + .await? + .ok_or(anyhow::anyhow!("No template received from the backend"))? + .edges + .into_iter() + .filter(|v| v.is_some()) + .map(|v| v.unwrap()) + .filter(|v| v.node.is_some()) + .map(|v| v.node.unwrap()) + .collect(); - let app_dir_path = match &self.app_dir_path { - Some(dir) => dir.clone(), - None => std::env::current_dir()?, + let theme = ColorfulTheme::default(); + let items = templates + .iter() + .map(|t| { + format!( + "{}{}{}\n", + t.name.bold(), + if t.language.is_empty() { + String::new() + } else { + format!(" {}", t.language.dimmed()) + }, + format!( + "\n {} {}", + "demo url:".bold().dimmed(), + t.demo_url.dimmed() + ) + ) + }) + .collect::>(); + // items.sort(); + + let dialog = dialoguer::Select::with_theme(&theme) + .with_prompt(format!("Select a template ({} available)", items.len())) + .items(&items) + .max_length(3) + .clear(true) + .report(false) + .default(0); + + let selection = dialog.interact()?; + + let selected_template = templates + .get(selection) + .ok_or(anyhow::anyhow!("Invalid selection!"))?; + + if !self.quiet { + eprintln!( + "{} {} {} {} ({} {})", + "✔".green().bold(), + "Selected template".bold(), + "·".dimmed(), + selected_template.name.green().bold(), + "demo url".dimmed().bold(), + selected_template.demo_url.dimmed() + ) + } + + url::Url::parse(&selected_template.repo_url)? }; - let local_package = if allow_local_package { - match crate::utils::load_package_manifest(&app_dir_path) { - Ok(Some(p)) => Some(p), - Ok(None) => None, - Err(err) => { - eprintln!( - "{warning}: could not load package manifest: {err}", - warning = "Warning".yellow(), - ); - None - } - } + if url.path().contains("archive/refs/heads") { + return Ok(url); + } else if url.path().contains("/zipball/") { + return Ok(url); } else { - None - }; + let old_path = url.path(); + url.set_path(&format!("{old_path}/zipball/main")); + return Ok(url); + } + } - let user = if self.offline { - None - } else if let Ok(client) = &self.api.client() { - let u = wasmer_api::query::current_user_with_namespaces( - client, - Some(wasmer_api::types::GrapheneRole::Admin), - ) - .await?; - Some(u) + async fn create_from_template( + &self, + client: &WasmerClient, + owner: &str, + app_name: &str, + ) -> anyhow::Result { + let url = self.get_template_url(client).await?; + + tracing::info!("Downloading template from url {url}"); + + let output_path = if let Some(path) = &self.app_dir_path { + path.clone() } else { - None - }; - let creator = AppCreator { - app_name: String::from(app_name), - new_package_name: self.new_package_name.clone(), - package: self.package.clone(), - template, - interactive: !self.non_interactive, - app_dir_path, - owner: String::from(owner), - api: if self.offline { - None - } else { - self.api.client().ok() - }, - user, - local_package, + PathBuf::from(".").canonicalize()? }; - match template { - AppType::HttpServer - | AppType::StaticWebsite - | AppType::JsWorker - | AppType::PyApplication => creator.build_app().await?, - AppType::BrowserShell => creator.build_browser_shell_app().await?, - }; + let pb = indicatif::ProgressBar::new_spinner(); + + pb.enable_steady_tick(std::time::Duration::from_millis(500)); + pb.set_style( + indicatif::ProgressStyle::with_template("{spinner:.magenta} {msg}") + .unwrap() + .tick_strings(&["✶", "✸", "✹", "✺", "✹", "✷"]), + ); + + pb.set_message("Downloading package.."); + + let response = reqwest::get(url).await?; + let bytes = response.bytes().await?; + pb.set_message("Unpacking the template.."); + let cursor = Cursor::new(bytes); + let mut archive = zip::ZipArchive::new(cursor)?; + + // Extract the files to the output path + for entry in 0..archive.len() { + let mut entry = archive + .by_index(entry) + .context(format!("Getting the archive entry #{entry}"))?; + + let path = entry.mangled_name(); + + let path: PathBuf = { + let mut components = path.components(); + components.next(); + components.collect() + }; + + if path.to_str().unwrap_or_default().contains(".github") { + continue; + } + + let path = output_path.join(path); + + if let Some(parent) = path.parent() { + if !parent.exists() { + std::fs::create_dir_all(parent)?; + } + } + + if !path.exists() { + // AsyncRead not implemented for entry.. + if entry.is_file() { + let mut outfile = std::fs::File::create(&path)?; + std::io::copy(&mut entry, &mut outfile)?; + } else { + std::fs::create_dir(path)?; + } + } + } - self.try_deploy(owner).await?; + pb.finish(); + + let app_yaml_path = output_path.join(AppConfigV1::CANONICAL_FILE_NAME); + + if app_yaml_path.exists() && app_yaml_path.is_file() { + let contents = tokio::fs::read_to_string(&app_yaml_path).await?; + let contents = format!("{contents}\nname: {app_name}"); + let mut app_config = AppConfigV1::parse_yaml(&contents)?; + app_config.owner = Some(owner.to_string()); + let raw_app = serde_yaml::to_string(&app_config)?; + tokio::fs::write(&app_yaml_path, raw_app).await?; + } + + let build_md_path = output_path.join("BUILD.md"); + if build_md_path.exists() { + let contents = tokio::fs::read_to_string(build_md_path).await?; + eprintln!( + "{}: {} +{}", + "NOTE".bold(), + "The selected template has a `BUILD.md` file. +This means there are likely additional build +steps that you need to perform before deploying +the app:\n" + .bold(), + contents + ); + } else { + self.try_deploy(owner, app_name).await?; + } Ok(true) } - async fn try_deploy(&self, owner: &str) -> anyhow::Result<()> { + async fn try_deploy(&self, owner: &str, app_name: &str) -> anyhow::Result<()> { let interactive = !self.non_interactive; let theme = dialoguer::theme::ColorfulTheme::default(); @@ -380,8 +475,11 @@ impl CmdAppCreate { no_default: false, no_persist_id: false, owner: Some(String::from(owner)), - app_name: None, + app_name: Some(app_name.into()), bump: false, + template: self.template.clone(), + package: self.package.clone(), + use_local_manifest: self.use_local_manifest, }; cmd_deploy.run_async().await?; } @@ -395,15 +493,24 @@ impl AsyncCliCommand for CmdAppCreate { type Output = (); async fn run_async(self) -> Result { + let client = login_user( + &self.api, + &self.env, + !self.non_interactive, + "retrieve informations about the owner of the app", + ) + .await?; + // Get the future owner of the app. - let owner = self.get_owner().await?; + let owner = self.get_owner(&client).await?; // Get the name of the app. let app_name = self.get_app_name().await?; if !self.create_from_local_manifest(&owner, &app_name).await? { if self.template.is_some() { - self.create_from_template(&owner, &app_name).await?; + self.create_from_template(&client, &owner, &app_name) + .await?; } else if self.package.is_some() { self.create_from_package(&owner, &app_name).await?; } else if !self.non_interactive { @@ -414,7 +521,10 @@ impl AsyncCliCommand for CmdAppCreate { .default(0) .interact()?; match choice { - 0 => self.create_from_template(&owner, &app_name).await?, + 0 => { + self.create_from_template(&client, &owner, &app_name) + .await? + } 1 => self.create_from_package(&owner, &app_name).await?, x => panic!("unhandled selection {x}"), }; @@ -427,270 +537,6 @@ impl AsyncCliCommand for CmdAppCreate { } } -/// App type. -#[derive(clap::ValueEnum, Clone, Copy, Debug)] -pub enum AppType { - /// A HTTP server. - #[clap(name = "http")] - HttpServer, - /// A static website. - #[clap(name = "static-website")] - StaticWebsite, - /// Wraps another package to run in the browser. - #[clap(name = "browser-shell")] - BrowserShell, - /// Winter-js based JS-Worker - #[clap(name = "js-worker")] - JsWorker, - /// Python worker - #[clap(name = "py-application")] - PyApplication, -} - -struct AppCreator { - package: Option, - new_package_name: Option, - app_name: String, - template: AppType, - interactive: bool, - app_dir_path: PathBuf, - owner: String, - api: Option, - user: Option, - local_package: Option<(PathBuf, wasmer_config::package::Manifest)>, -} - -impl AppCreator { - async fn build_browser_shell_app(self) -> Result<(), anyhow::Error> { - const WASM_BROWSER_CONTAINER_PACKAGE: &str = "wasmer/wasmer-sh"; - const WASM_BROWSER_CONTAINER_VERSION: &str = "0.2"; - - eprintln!("A browser web shell wraps another package and runs it in the browser"); - eprintln!("Select the package to wrap."); - - let (inner_pkg, _inner_pkg_api) = crate::utils::prompt_for_package( - "Package", - None, - Some(crate::utils::PackageCheckMode::MustExist), - self.api.as_ref(), - ) - .await?; - - let app_name = self.app_name; - eprintln!("What should be the name of the package?"); - - let default_name = format!( - "{}-{}-webshell", - self.owner, - inner_pkg.to_string().replace('/', "-") - ); - - let outer_pkg_name = - crate::utils::prompts::prompt_for_ident("Package name", Some(&default_name))?; - let outer_pkg_full_name = format!("{}/{}", self.owner, outer_pkg_name); - - // Build the package. - - let public_dir = self.app_dir_path.join("public"); - if !public_dir.exists() { - std::fs::create_dir_all(&public_dir)?; - } - - let init = serde_json::json!({ - "init": format!("{}/{}", inner_pkg.namespace.as_ref().unwrap(), inner_pkg.name), - "prompt": inner_pkg.name, - "no_welcome": true, - "connect": format!("wss://{app_name}.wasmer.app/.well-known/edge-vpn"), - }); - let init_path = public_dir.join("init.json"); - std::fs::write(&init_path, init.to_string()) - .with_context(|| format!("Failed to write to '{}'", init_path.display()))?; - - let package = wasmer_config::package::PackageBuilder::new( - outer_pkg_full_name, - "0.1.0".parse().unwrap(), - format!("{} web shell", inner_pkg.name), - ) - .rename_commands_to_raw_command_name(false) - .build()?; - - let manifest = wasmer_config::package::ManifestBuilder::new(package) - .with_dependency( - WASM_BROWSER_CONTAINER_PACKAGE, - WASM_BROWSER_CONTAINER_VERSION.to_string().parse().unwrap(), - ) - .map_fs("public", PathBuf::from("public")) - .build()?; - - let manifest_path = self.app_dir_path.join("wasmer.toml"); - - let raw = manifest.to_string()?; - eprintln!( - "Writing wasmer.toml package to '{}'", - manifest_path.display() - ); - std::fs::write(&manifest_path, raw)?; - - let app_config = AppConfigV1 { - name: app_name, - app_id: None, - owner: Some(self.owner.clone()), - package: PackageSource::Path(".".into()), - domains: None, - env: Default::default(), - cli_args: None, - capabilities: None, - scheduled_tasks: None, - volumes: None, - health_checks: None, - debug: Some(false), - scaling: None, - extra: Default::default(), - }; - - write_app_config(&app_config, Some(self.app_dir_path.clone())).await?; - - Ok(()) - } - - async fn build_app(self) -> Result<(), anyhow::Error> { - let package_opt: Option = if let Some(package) = self.package { - Some(NamedPackageIdent::from_str(&package)?) - } else if let Some((_, local)) = self.local_package.as_ref() { - let pkg = match &local.package { - Some(pkg) => pkg.clone(), - None => anyhow::bail!( - "Error while building app: template manifest has no package field!" - ), - }; - - if let (Some(name), Some(version)) = (pkg.name, pkg.version) { - let full = format!("{}@{}", name, version); - let mut pkg_ident = NamedPackageIdent::from_str(&name).with_context(|| { - format!("local package manifest has invalid name: '{full}'") - })?; - - // Pin the version. - pkg_ident.tag = Some(Tag::from_str(&version.to_string()).unwrap()); - - if self.interactive { - eprintln!("Found local package: '{}'", full.green()); - - let msg = format!("Use package '{pkg_ident}'"); - - let theme = dialoguer::theme::ColorfulTheme::default(); - let should_use = Confirm::with_theme(&theme) - .with_prompt(&msg) - .interact_opt()? - .unwrap_or_default(); - - if should_use { - Some(pkg_ident) - } else { - None - } - } else { - Some(pkg_ident) - } - } else { - None - } - } else { - None - }; - - let (package, _api_pkg, _local_package) = if let Some(pkg) = package_opt { - if let Some(api) = &self.api { - let p2 = wasmer_api::query::get_package( - api, - format!("{}/{}", pkg.namespace.as_ref().unwrap(), pkg.name), - ) - .await?; - - ( - PackageSource::Ident(wasmer_config::package::PackageIdent::Named(pkg)), - p2, - self.local_package, - ) - } else { - ( - PackageSource::Ident(wasmer_config::package::PackageIdent::Named(pkg)), - None, - self.local_package, - ) - } - } else { - let ty = match self.template { - AppType::HttpServer => None, - AppType::StaticWebsite => Some(PackageType::StaticWebsite), - AppType::BrowserShell => None, - AppType::JsWorker => Some(PackageType::JsWorker), - AppType::PyApplication => Some(PackageType::PyApplication), - }; - - let create_mode = match ty { - Some(PackageType::StaticWebsite) - | Some(PackageType::JsWorker) - | Some(PackageType::PyApplication) => CreateMode::Create, - // Only static website creation is currently supported. - _ => CreateMode::SelectExisting, - }; - - let w = PackageWizard { - path: self.app_dir_path.clone(), - name: self.new_package_name.clone(), - type_: ty, - create_mode, - namespace: Some(self.owner.clone()), - namespace_default: self.user.as_ref().map(|u| u.username.clone()), - user: self.user.clone(), - }; - - let output = w.run(self.api.as_ref()).await?; - ( - PackageSource::Path(".".into()), - output.api, - output - .local_path - .and_then(move |x| Some((x, output.local_manifest?))), - ) - }; - - let name = self.app_name; - - let cli_args = match self.template { - AppType::PyApplication => Some(vec!["/src/main.py".to_string()]), - AppType::JsWorker => Some(vec!["/src/index.js".to_string()]), - _ => None, - }; - - // TODO: check if name already exists. - let app_config = AppConfigV1 { - name, - app_id: None, - owner: Some(self.owner.clone()), - package, - domains: None, - env: Default::default(), - // CLI args are only set for JS and Py workers for now. - cli_args, - // TODO: allow setting the description. - // description: Some("".to_string()), - capabilities: None, - scheduled_tasks: None, - volumes: None, - health_checks: None, - debug: Some(false), - scaling: None, - extra: Default::default(), - }; - - write_app_config(&app_config, Some(self.app_dir_path.clone())).await?; - - Ok(()) - } -} - #[cfg(test)] mod tests { use super::*; @@ -700,6 +546,7 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let cmd = CmdAppCreate { + quiet: true, template: Some(AppType::StaticWebsite), deploy_app: false, no_validate: false, @@ -735,6 +582,7 @@ debug: false let dir = tempfile::tempdir().unwrap(); let cmd = CmdAppCreate { + quiet: true, template: Some(AppType::HttpServer), deploy_app: false, no_validate: false, @@ -769,6 +617,7 @@ debug: false let dir = tempfile::tempdir().unwrap(); let cmd = CmdAppCreate { + quiet: true, template: Some(AppType::JsWorker), deploy_app: false, no_validate: false, @@ -806,6 +655,7 @@ debug: false let dir = tempfile::tempdir().unwrap(); let cmd = CmdAppCreate { + quiet: true, template: Some(AppType::PyApplication), deploy_app: false, no_validate: false, diff --git a/lib/cli/src/commands/app/deploy.rs b/lib/cli/src/commands/app/deploy.rs index 3e42f5a834b..7051a63c8bb 100644 --- a/lib/cli/src/commands/app/deploy.rs +++ b/lib/cli/src/commands/app/deploy.rs @@ -76,7 +76,7 @@ pub struct CmdAppDeploy { /// If specified via this flag, the app_name will be overridden. Otherwise, the `app.yaml` is /// inspected and, if there is no `name` field in the spec file, if running interactive the /// user will be prompted to insert an app name, otherwise the deployment will fail. - #[clap(long)] + #[clap(long, name = "name")] pub app_name: Option, /// Whether or not to automatically bump the package version if publishing. @@ -89,6 +89,31 @@ pub struct CmdAppDeploy { /// operation. #[clap(long)] pub quiet: bool, + + // - App creation - + /// A reference to the template to use when creating an app to deploy. + /// + /// It can be either an URL to a github repository - like + /// `https://github.com/wasmer-examples/php-wasmer-starter` - or the name of a template that + /// will be searched for in the selected registry, like `astro-starter`. + #[clap( + long, + conflicts_with = "package", + conflicts_with = "use_local_manifest" + )] + pub template: Option, + + /// Name of the package to use when creating an app to deploy. + #[clap( + long, + conflicts_with = "template", + conflicts_with = "use_local_manifest" + )] + pub package: Option, + + /// Whether or not to search (and use) a local manifest when creating an app to deploy. + #[clap(long, conflicts_with = "template", conflicts_with = "package")] + pub use_local_manifest: bool, } impl CmdAppDeploy { @@ -172,7 +197,7 @@ impl CmdAppDeploy { eprintln!("It seems you are trying to create a new app!"); let create_cmd = CmdAppCreate { - template: None, + quiet: self.quiet, deploy_app: false, no_validate: false, non_interactive: false, @@ -185,9 +210,10 @@ impl CmdAppDeploy { fmt: ItemFormatOpts { format: self.fmt.format, }, - package: None, + package: self.package.clone(), + template: self.template.clone(), app_dir_path: None, - use_local_manifest: false, + use_local_manifest: self.use_local_manifest, new_package_name: None, }; diff --git a/lib/cli/src/utils/mod.rs b/lib/cli/src/utils/mod.rs index 14c001e8717..44b9c44ed13 100644 --- a/lib/cli/src/utils/mod.rs +++ b/lib/cli/src/utils/mod.rs @@ -10,11 +10,8 @@ use std::{ }; use anyhow::{bail, Context as _, Result}; -use dialoguer::theme::ColorfulTheme; use once_cell::sync::Lazy; use regex::Regex; -use wasmer_api::WasmerClient; -use wasmer_config::package::NamedPackageIdent; use wasmer_wasix::runners::MappedDirectory; fn retrieve_alias_pathbuf(alias: &str, real_dir: &str) -> Result { @@ -111,79 +108,79 @@ pub fn load_package_manifest( Ok(Some((file_path, manifest))) } -/// Ask a user for a package name. -/// -/// Will continue looping until the user provides a valid name. -pub fn prompt_for_package_name( - message: &str, - default: Option<&str>, -) -> Result { - loop { - let theme = ColorfulTheme::default(); - let raw: String = dialoguer::Input::with_theme(&theme) - .with_prompt(message) - .with_initial_text(default.unwrap_or_default()) - .interact_text() - .context("could not read user input")?; - - match raw.parse::() { - Ok(p) => break Ok(p), - Err(err) => { - eprintln!("invalid package name: {err}"); - } - } - } -} - -/// Defines how to check for a package. -pub enum PackageCheckMode { - /// The package must exist in the registry. - MustExist, - /// The package must NOT exist in the registry. - #[allow(dead_code)] - MustNotExist, -} +///// Ask a user for a package name. +///// +///// Will continue looping until the user provides a valid name. +//pub fn prompt_for_package_name( +// message: &str, +// default: Option<&str>, +//) -> Result { +// loop { +// let theme = ColorfulTheme::default(); +// let raw: String = dialoguer::Input::with_theme(&theme) +// .with_prompt(message) +// .with_initial_text(default.unwrap_or_default()) +// .interact_text() +// .context("could not read user input")?; +// +// match raw.parse::() { +// Ok(p) => break Ok(p), +// Err(err) => { +// eprintln!("invalid package name: {err}"); +// } +// } +// } +//} -/// Ask for a package name. -/// -/// Will continue looping until the user provides a valid name. -/// -/// If an API is provided, will check if the package exists. -pub async fn prompt_for_package( - message: &str, - default: Option<&str>, - check: Option, - client: Option<&WasmerClient>, -) -> Result<(NamedPackageIdent, Option), anyhow::Error> { - loop { - let name = prompt_for_package_name(message, default)?; - - if let Some(check) = &check { - let api = client.expect("Check mode specified, but no API provided"); - - let pkg = wasmer_api::query::get_package(api, name.to_string()) - .await - .context("could not query backend for package")?; - - match check { - PackageCheckMode::MustExist => { - if let Some(pkg) = pkg { - break Ok((name, Some(pkg))); - } else { - eprintln!("Package '{name}' does not exist"); - } - } - PackageCheckMode::MustNotExist => { - if pkg.is_none() { - break Ok((name, None)); - } else { - eprintln!("Package '{name}' already exists"); - } - } - } - } - } -} +// /// Defines how to check for a package. +// pub enum PackageCheckMode { +// /// The package must exist in the registry. +// MustExist, +// /// The package must NOT exist in the registry. +// #[allow(dead_code)] +// MustNotExist, +// } +// +// /// Ask for a package name. +// /// +// /// Will continue looping until the user provides a valid name. +// /// +// /// If an API is provided, will check if the package exists. +// pub async fn prompt_for_package( +// message: &str, +// default: Option<&str>, +// check: Option, +// client: Option<&WasmerClient>, +// ) -> Result<(NamedPackageIdent, Option), anyhow::Error> { +// loop { +// let name = prompt_for_package_name(message, default)?; +// +// if let Some(check) = &check { +// let api = client.expect("Check mode specified, but no API provided"); +// +// let pkg = wasmer_api::query::get_package(api, name.to_string()) +// .await +// .context("could not query backend for package")?; +// +// match check { +// PackageCheckMode::MustExist => { +// if let Some(pkg) = pkg { +// break Ok((name, Some(pkg))); +// } else { +// eprintln!("Package '{name}' does not exist"); +// } +// } +// PackageCheckMode::MustNotExist => { +// if pkg.is_none() { +// break Ok((name, None)); +// } else { +// eprintln!("Package '{name}' already exists"); +// } +// } +// } +// } +// } +// } // /// Republish the package described by the [`wasmer_config::package::Manifest`] given as argument and return a // /// [`Result`]. diff --git a/lib/cli/src/utils/package_wizard/mod.rs b/lib/cli/src/utils/package_wizard/mod.rs index ddc55e18c0e..609ff37f606 100644 --- a/lib/cli/src/utils/package_wizard/mod.rs +++ b/lib/cli/src/utils/package_wizard/mod.rs @@ -1,426 +1,426 @@ -use std::path::{Path, PathBuf}; - -use anyhow::Context; -use dialoguer::{theme::ColorfulTheme, Select}; -use wasmer_api::{types::UserWithNamespaces, WasmerClient}; - -use super::prompts::PackageCheckMode; - -const WASM_STATIC_SERVER_PACKAGE: &str = "wasmer/static-web-server"; -const WASM_STATIC_SERVER_VERSION: &str = "1"; - -const WASMER_WINTER_JS_PACKAGE: &str = "wasmer/winterjs"; -const WASMER_WINTER_JS_VERSION: &str = "*"; - -const WASM_PYTHON_PACKAGE: &str = "wasmer/python"; -const WASM_PYTHON_VERSION: &str = "3.12.6"; - -const SAMPLE_INDEX_HTML: &str = include_str!("./templates/static-site/index.html"); -const SAMPLE_JS_WORKER: &str = include_str!("./templates/js-worker/index.js"); -const SAMPLE_PY_APPLICATION: &str = include_str!("./templates/py-application/main.py"); - -#[derive(clap::ValueEnum, Clone, Copy, Debug)] -pub enum PackageType { - #[clap(name = "regular")] - Regular, - /// A static website. - #[clap(name = "static-website")] - StaticWebsite, - /// A js-worker - #[clap(name = "js-worker")] - JsWorker, - /// A py-worker - #[clap(name = "py-application")] - PyApplication, -} - -#[derive(Clone, Copy, Debug)] -pub enum CreateMode { - Create, - SelectExisting, - #[allow(dead_code)] - CreateOrSelect, -} - -fn prompt_for_package_type() -> Result { - let theme = ColorfulTheme::default(); - Select::with_theme(&theme) - .with_prompt("What type of package do you want to create?") - .items(&["Basic pacakge", "Static website"]) - .interact() - .map(|idx| match idx { - 0 => PackageType::Regular, - 1 => PackageType::StaticWebsite, - _ => unreachable!(), - }) - .map_err(anyhow::Error::from) -} - -#[derive(Debug)] -pub struct PackageWizard { - pub path: PathBuf, - pub type_: Option, - - pub create_mode: CreateMode, - - /// Namespace to use. - pub namespace: Option, - /// Default namespace to use. - /// Will still show a prompt, with this as the default value. - /// Ignored if [`Self::namespace`] is set. - pub namespace_default: Option, - - /// Pre-configured package name. - pub name: Option, - - pub user: Option, -} - -pub struct PackageWizardOutput { - pub api: Option, - pub local_path: Option, - pub local_manifest: Option, -} - -impl PackageWizard { - fn build_new_package(&self) -> Result { - let ty = match self.type_ { - Some(t) => t, - None => prompt_for_package_type()?, - }; - - if !self.path.is_dir() { - std::fs::create_dir_all(&self.path).with_context(|| { - format!("Failed to create directory: '{}'", self.path.display()) - })?; - } - - let manifest = match ty { - PackageType::Regular => todo!(), - PackageType::StaticWebsite => initialize_static_site(&self.path)?, - PackageType::JsWorker => initialize_js_worker(&self.path)?, - PackageType::PyApplication => initialize_py_worker(&self.path)?, - }; - - let manifest_path = self.path.join("wasmer.toml"); - let manifest_raw = manifest - .to_string() - .context("could not serialize package manifest")?; - std::fs::write(manifest_path, manifest_raw) - .with_context(|| format!("Failed to write manifest to '{}'", self.path.display()))?; - - Ok(PackageWizardOutput { - api: None, - local_path: Some(self.path.clone()), - local_manifest: Some(manifest), - }) - } - - async fn prompt_existing_package( - &self, - api: Option<&WasmerClient>, - ) -> Result { - // Existing package - let check = if api.is_some() { - Some(PackageCheckMode::MustExist) - } else { - None - }; - - eprintln!("Enter the name of an existing package:"); - let (_ident, api) = super::prompts::prompt_for_package("Package", None, check, api).await?; - Ok(PackageWizardOutput { - api, - local_path: None, - local_manifest: None, - }) - } - - pub async fn run( - self, - api: Option<&WasmerClient>, - ) -> Result { - match self.create_mode { - CreateMode::Create => self.build_new_package(), - CreateMode::SelectExisting => self.prompt_existing_package(api).await, - CreateMode::CreateOrSelect => { - let theme = ColorfulTheme::default(); - let index = Select::with_theme(&theme) - .with_prompt("What package do you want to use?") - .items(&["Create new package", "Use existing package"]) - .default(0) - .interact()?; - - match index { - 0 => self.build_new_package(), - 1 => self.prompt_existing_package(api).await, - other => { - unreachable!("Unexpected index: {other}"); - } - } - } - } - } -} - -fn initialize_static_site(path: &Path) -> Result { - let pubdir_name = "public"; - let pubdir = path.join(pubdir_name); - if !pubdir.is_dir() { - std::fs::create_dir_all(&pubdir) - .with_context(|| format!("Failed to create directory: '{}'", pubdir.display()))?; - } - let index = pubdir.join("index.html"); - - let static_html = SAMPLE_INDEX_HTML.replace("{{title}}", "My static website"); - - if !index.is_file() { - std::fs::write(&index, static_html.as_str()) - .with_context(|| "Could not write index.html file".to_string())?; - } else { - // The index.js file already exists, so we can ask the user if they want to overwrite it - let theme = dialoguer::theme::ColorfulTheme::default(); - let should_overwrite = dialoguer::Confirm::with_theme(&theme) - .with_prompt("index.html already exists. Do you want to overwrite it?") - .interact() - .unwrap(); - if should_overwrite { - std::fs::write(&index, static_html.as_str()) - .with_context(|| "Could not write index.html file".to_string())?; - } - } - - let raw_static_site_toml = format!( - r#" -[dependencies] -"{}" = "{}" - -[fs] -public = "{}" -"#, - WASM_STATIC_SERVER_PACKAGE, WASM_STATIC_SERVER_VERSION, pubdir_name - ); - - let manifest = wasmer_config::package::Manifest::parse(raw_static_site_toml.as_str()) - .map_err(|e| anyhow::anyhow!("Could not parse js worker manifest: {}", e))?; - - Ok(manifest) -} - -fn initialize_js_worker(path: &Path) -> Result { - let srcdir_name = "src"; - let srcdir = path.join(srcdir_name); - if !srcdir.is_dir() { - std::fs::create_dir_all(&srcdir) - .with_context(|| format!("Failed to create directory: '{}'", srcdir.display()))?; - } - - let index_js = srcdir.join("index.js"); - - let sample_js = SAMPLE_JS_WORKER.replace("{{package}}", "My JS worker"); - - if !index_js.is_file() { - std::fs::write(&index_js, sample_js.as_str()) - .with_context(|| "Could not write index.js file".to_string())?; - } - - // get the remote repository if it exists - // Todo: add this to the manifest - // let remote_repo_url = Command::new("git") - // .arg("remote") - // .arg("get-url") - // .arg("origin") - // .output() - // .map_or("".to_string(), |f| String::from_utf8(f.stdout).unwrap()); - - let raw_js_worker_toml = format!( - r#" -[dependencies] -"{winterjs_pkg}" = "{winterjs_version}" - -[fs] -"/src" = "./src" - -[[command]] -name = "script" -module = "{winterjs_pkg}:winterjs" -runner = "https://webc.org/runner/wasi" - -[command.annotations.wasi] -main-args = ["/src/index.js"] -env = ["JS_PATH=/src/index.js"] -"#, - winterjs_pkg = WASMER_WINTER_JS_PACKAGE, - winterjs_version = WASMER_WINTER_JS_VERSION, - ); - - let manifest = wasmer_config::package::Manifest::parse(raw_js_worker_toml.as_str()) - .map_err(|e| anyhow::anyhow!("Could not parse js worker manifest: {}", e))?; - - Ok(manifest) -} - -fn initialize_py_worker(path: &Path) -> Result { - let appdir_name = "src"; - let appdir = path.join(appdir_name); - if !appdir.is_dir() { - std::fs::create_dir_all(&appdir) - .with_context(|| format!("Failed to create directory: '{}'", appdir.display()))?; - } - let main_py = appdir.join("main.py"); - - let sample_main = SAMPLE_PY_APPLICATION.replace("{{package}}", "My Python Worker"); - - if !main_py.is_file() { - std::fs::write(&main_py, sample_main.as_str()) - .with_context(|| "Could not write main.py file".to_string())?; - } - - // Todo: add this to the manifest - // let remote_repo_url = Command::new("git") - // .arg("remote") - // .arg("get-url") - // .arg("origin") - // .output() - // .map_or("".to_string(), |f| String::from_utf8(f.stdout).unwrap()); - - let raw_py_worker_toml = format!( - r#" -[dependencies] -"{}" = "{}" - -[fs] -"/src" = "./src" -# "/.env" = "./.env/" # Bundle the virtualenv - -[[command]] -name = "script" -module = "{}:python" # The "python" atom from "wasmer/python" -runner = "wasi" - -[command.annotations.wasi] -main-args = ["/src/main.py"] -# env = ["PYTHON_PATH=/app/.env:/etc/python3.12/site-packages"] # Make our virtualenv accessible -"#, - WASM_PYTHON_PACKAGE, WASM_PYTHON_VERSION, WASM_PYTHON_PACKAGE - ); - - let manifest = wasmer_config::package::Manifest::parse(raw_py_worker_toml.as_str()) - .map_err(|e| anyhow::anyhow!("Could not parse py worker manifest: {}", e))?; - - Ok(manifest) -} -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_package_wizard_create_static_site() { - let dir = tempfile::tempdir().unwrap(); - - PackageWizard { - path: dir.path().to_owned(), - type_: Some(PackageType::StaticWebsite), - create_mode: CreateMode::Create, - namespace: None, - namespace_default: None, - name: None, - user: None, - } - .run(None) - .await - .unwrap(); - - let manifest = std::fs::read_to_string(dir.path().join("wasmer.toml")).unwrap(); - pretty_assertions::assert_eq!( - manifest, - r#"[dependencies] -"wasmer/static-web-server" = "^1" - -[fs] -public = "public" -"#, - ); - - assert!(dir.path().join("public").join("index.html").is_file()); - } - - #[tokio::test] - async fn test_package_wizard_create_js_worker() { - let dir = tempfile::tempdir().unwrap(); - - PackageWizard { - path: dir.path().to_owned(), - type_: Some(PackageType::JsWorker), - create_mode: CreateMode::Create, - namespace: None, - namespace_default: None, - name: None, - user: None, - } - .run(None) - .await - .unwrap(); - let manifest = std::fs::read_to_string(dir.path().join("wasmer.toml")).unwrap(); - - pretty_assertions::assert_eq!( - manifest, - r#"[dependencies] -"wasmer/winterjs" = "*" - -[fs] -"/src" = "./src" - -[[command]] -name = "script" -module = "wasmer/winterjs:winterjs" -runner = "https://webc.org/runner/wasi" - -[command.annotations.wasi] -env = ["JS_PATH=/src/index.js"] -main-args = ["/src/index.js"] -"#, - ); - - assert!(dir.path().join("src").join("index.js").is_file()); - } - - #[tokio::test] - async fn test_package_wizard_create_py_worker() { - let dir = tempfile::tempdir().unwrap(); - - PackageWizard { - path: dir.path().to_owned(), - type_: Some(PackageType::PyApplication), - create_mode: CreateMode::Create, - namespace: None, - namespace_default: None, - name: None, - user: None, - } - .run(None) - .await - .unwrap(); - let manifest = std::fs::read_to_string(dir.path().join("wasmer.toml")).unwrap(); - - pretty_assertions::assert_eq!( - manifest, - r#"[dependencies] -"wasmer/python" = "^3.12.6" - -[fs] -"/src" = "./src" - -[[command]] -name = "script" -module = "wasmer/python:python" -runner = "wasi" - -[command.annotations.wasi] -main-args = ["/src/main.py"] -"#, - ); - - assert!(dir.path().join("src").join("main.py").is_file()); - } -} +// use std::path::{Path, PathBuf}; +// +// use anyhow::Context; +// use dialoguer::{theme::ColorfulTheme, Select}; +// use wasmer_api::{types::UserWithNamespaces, WasmerClient}; +// +// use super::prompts::PackageCheckMode; +// +// const WASM_STATIC_SERVER_PACKAGE: &str = "wasmer/static-web-server"; +// const WASM_STATIC_SERVER_VERSION: &str = "1"; +// +// const WASMER_WINTER_JS_PACKAGE: &str = "wasmer/winterjs"; +// const WASMER_WINTER_JS_VERSION: &str = "*"; +// +// const WASM_PYTHON_PACKAGE: &str = "wasmer/python"; +// const WASM_PYTHON_VERSION: &str = "3.12.6"; +// +// const SAMPLE_INDEX_HTML: &str = include_str!("./templates/static-site/index.html"); +// const SAMPLE_JS_WORKER: &str = include_str!("./templates/js-worker/index.js"); +// const SAMPLE_PY_APPLICATION: &str = include_str!("./templates/py-application/main.py"); +// +// #[derive(clap::ValueEnum, Clone, Copy, Debug)] +// pub enum PackageType { +// #[clap(name = "regular")] +// Regular, +// /// A static website. +// #[clap(name = "static-website")] +// StaticWebsite, +// /// A js-worker +// #[clap(name = "js-worker")] +// JsWorker, +// /// A py-worker +// #[clap(name = "py-application")] +// PyApplication, +// } +// +// #[derive(Clone, Copy, Debug)] +// pub enum CreateMode { +// Create, +// SelectExisting, +// #[allow(dead_code)] +// CreateOrSelect, +// } +// +// fn prompt_for_package_type() -> Result { +// let theme = ColorfulTheme::default(); +// Select::with_theme(&theme) +// .with_prompt("What type of package do you want to create?") +// .items(&["Basic pacakge", "Static website"]) +// .interact() +// .map(|idx| match idx { +// 0 => PackageType::Regular, +// 1 => PackageType::StaticWebsite, +// _ => unreachable!(), +// }) +// .map_err(anyhow::Error::from) +// } +// +// #[derive(Debug)] +// pub struct PackageWizard { +// pub path: PathBuf, +// pub type_: Option, +// +// pub create_mode: CreateMode, +// +// /// Namespace to use. +// pub namespace: Option, +// /// Default namespace to use. +// /// Will still show a prompt, with this as the default value. +// /// Ignored if [`Self::namespace`] is set. +// pub namespace_default: Option, +// +// /// Pre-configured package name. +// pub name: Option, +// +// pub user: Option, +// } +// +// pub struct PackageWizardOutput { +// pub api: Option, +// pub local_path: Option, +// pub local_manifest: Option, +// } +// +// impl PackageWizard { +// fn build_new_package(&self) -> Result { +// let ty = match self.type_ { +// Some(t) => t, +// None => prompt_for_package_type()?, +// }; +// +// if !self.path.is_dir() { +// std::fs::create_dir_all(&self.path).with_context(|| { +// format!("Failed to create directory: '{}'", self.path.display()) +// })?; +// } +// +// let manifest = match ty { +// PackageType::Regular => todo!(), +// PackageType::StaticWebsite => initialize_static_site(&self.path)?, +// PackageType::JsWorker => initialize_js_worker(&self.path)?, +// PackageType::PyApplication => initialize_py_worker(&self.path)?, +// }; +// +// let manifest_path = self.path.join("wasmer.toml"); +// let manifest_raw = manifest +// .to_string() +// .context("could not serialize package manifest")?; +// std::fs::write(manifest_path, manifest_raw) +// .with_context(|| format!("Failed to write manifest to '{}'", self.path.display()))?; +// +// Ok(PackageWizardOutput { +// api: None, +// local_path: Some(self.path.clone()), +// local_manifest: Some(manifest), +// }) +// } +// +// async fn prompt_existing_package( +// &self, +// api: Option<&WasmerClient>, +// ) -> Result { +// // Existing package +// let check = if api.is_some() { +// Some(PackageCheckMode::MustExist) +// } else { +// None +// }; +// +// eprintln!("Enter the name of an existing package:"); +// let (_ident, api) = super::prompts::prompt_for_package("Package", None, check, api).await?; +// Ok(PackageWizardOutput { +// api, +// local_path: None, +// local_manifest: None, +// }) +// } +// +// pub async fn run( +// self, +// api: Option<&WasmerClient>, +// ) -> Result { +// match self.create_mode { +// CreateMode::Create => self.build_new_package(), +// CreateMode::SelectExisting => self.prompt_existing_package(api).await, +// CreateMode::CreateOrSelect => { +// let theme = ColorfulTheme::default(); +// let index = Select::with_theme(&theme) +// .with_prompt("What package do you want to use?") +// .items(&["Create new package", "Use existing package"]) +// .default(0) +// .interact()?; +// +// match index { +// 0 => self.build_new_package(), +// 1 => self.prompt_existing_package(api).await, +// other => { +// unreachable!("Unexpected index: {other}"); +// } +// } +// } +// } +// } +// } +// +// fn initialize_static_site(path: &Path) -> Result { +// let pubdir_name = "public"; +// let pubdir = path.join(pubdir_name); +// if !pubdir.is_dir() { +// std::fs::create_dir_all(&pubdir) +// .with_context(|| format!("Failed to create directory: '{}'", pubdir.display()))?; +// } +// let index = pubdir.join("index.html"); +// +// let static_html = SAMPLE_INDEX_HTML.replace("{{title}}", "My static website"); +// +// if !index.is_file() { +// std::fs::write(&index, static_html.as_str()) +// .with_context(|| "Could not write index.html file".to_string())?; +// } else { +// // The index.js file already exists, so we can ask the user if they want to overwrite it +// let theme = dialoguer::theme::ColorfulTheme::default(); +// let should_overwrite = dialoguer::Confirm::with_theme(&theme) +// .with_prompt("index.html already exists. Do you want to overwrite it?") +// .interact() +// .unwrap(); +// if should_overwrite { +// std::fs::write(&index, static_html.as_str()) +// .with_context(|| "Could not write index.html file".to_string())?; +// } +// } +// +// let raw_static_site_toml = format!( +// r#" +// [dependencies] +// "{}" = "{}" +// +// [fs] +// public = "{}" +// "#, +// WASM_STATIC_SERVER_PACKAGE, WASM_STATIC_SERVER_VERSION, pubdir_name +// ); +// +// let manifest = wasmer_config::package::Manifest::parse(raw_static_site_toml.as_str()) +// .map_err(|e| anyhow::anyhow!("Could not parse js worker manifest: {}", e))?; +// +// Ok(manifest) +// } +// +// fn initialize_js_worker(path: &Path) -> Result { +// let srcdir_name = "src"; +// let srcdir = path.join(srcdir_name); +// if !srcdir.is_dir() { +// std::fs::create_dir_all(&srcdir) +// .with_context(|| format!("Failed to create directory: '{}'", srcdir.display()))?; +// } +// +// let index_js = srcdir.join("index.js"); +// +// let sample_js = SAMPLE_JS_WORKER.replace("{{package}}", "My JS worker"); +// +// if !index_js.is_file() { +// std::fs::write(&index_js, sample_js.as_str()) +// .with_context(|| "Could not write index.js file".to_string())?; +// } +// +// // get the remote repository if it exists +// // Todo: add this to the manifest +// // let remote_repo_url = Command::new("git") +// // .arg("remote") +// // .arg("get-url") +// // .arg("origin") +// // .output() +// // .map_or("".to_string(), |f| String::from_utf8(f.stdout).unwrap()); +// +// let raw_js_worker_toml = format!( +// r#" +// [dependencies] +// "{winterjs_pkg}" = "{winterjs_version}" +// +// [fs] +// "/src" = "./src" +// +// [[command]] +// name = "script" +// module = "{winterjs_pkg}:winterjs" +// runner = "https://webc.org/runner/wasi" +// +// [command.annotations.wasi] +// main-args = ["/src/index.js"] +// env = ["JS_PATH=/src/index.js"] +// "#, +// winterjs_pkg = WASMER_WINTER_JS_PACKAGE, +// winterjs_version = WASMER_WINTER_JS_VERSION, +// ); +// +// let manifest = wasmer_config::package::Manifest::parse(raw_js_worker_toml.as_str()) +// .map_err(|e| anyhow::anyhow!("Could not parse js worker manifest: {}", e))?; +// +// Ok(manifest) +// } +// +// fn initialize_py_worker(path: &Path) -> Result { +// let appdir_name = "src"; +// let appdir = path.join(appdir_name); +// if !appdir.is_dir() { +// std::fs::create_dir_all(&appdir) +// .with_context(|| format!("Failed to create directory: '{}'", appdir.display()))?; +// } +// let main_py = appdir.join("main.py"); +// +// let sample_main = SAMPLE_PY_APPLICATION.replace("{{package}}", "My Python Worker"); +// +// if !main_py.is_file() { +// std::fs::write(&main_py, sample_main.as_str()) +// .with_context(|| "Could not write main.py file".to_string())?; +// } +// +// // Todo: add this to the manifest +// // let remote_repo_url = Command::new("git") +// // .arg("remote") +// // .arg("get-url") +// // .arg("origin") +// // .output() +// // .map_or("".to_string(), |f| String::from_utf8(f.stdout).unwrap()); +// +// let raw_py_worker_toml = format!( +// r#" +// [dependencies] +// "{}" = "{}" +// +// [fs] +// "/src" = "./src" +// # "/.env" = "./.env/" # Bundle the virtualenv +// +// [[command]] +// name = "script" +// module = "{}:python" # The "python" atom from "wasmer/python" +// runner = "wasi" +// +// [command.annotations.wasi] +// main-args = ["/src/main.py"] +// # env = ["PYTHON_PATH=/app/.env:/etc/python3.12/site-packages"] # Make our virtualenv accessible +// "#, +// WASM_PYTHON_PACKAGE, WASM_PYTHON_VERSION, WASM_PYTHON_PACKAGE +// ); +// +// let manifest = wasmer_config::package::Manifest::parse(raw_py_worker_toml.as_str()) +// .map_err(|e| anyhow::anyhow!("Could not parse py worker manifest: {}", e))?; +// +// Ok(manifest) +// } +// #[cfg(test)] +// mod tests { +// use super::*; +// +// #[tokio::test] +// async fn test_package_wizard_create_static_site() { +// let dir = tempfile::tempdir().unwrap(); +// +// PackageWizard { +// path: dir.path().to_owned(), +// type_: Some(PackageType::StaticWebsite), +// create_mode: CreateMode::Create, +// namespace: None, +// namespace_default: None, +// name: None, +// user: None, +// } +// .run(None) +// .await +// .unwrap(); +// +// let manifest = std::fs::read_to_string(dir.path().join("wasmer.toml")).unwrap(); +// pretty_assertions::assert_eq!( +// manifest, +// r#"[dependencies] +// "wasmer/static-web-server" = "^1" +// +// [fs] +// public = "public" +// "#, +// ); +// +// assert!(dir.path().join("public").join("index.html").is_file()); +// } +// +// #[tokio::test] +// async fn test_package_wizard_create_js_worker() { +// let dir = tempfile::tempdir().unwrap(); +// +// PackageWizard { +// path: dir.path().to_owned(), +// type_: Some(PackageType::JsWorker), +// create_mode: CreateMode::Create, +// namespace: None, +// namespace_default: None, +// name: None, +// user: None, +// } +// .run(None) +// .await +// .unwrap(); +// let manifest = std::fs::read_to_string(dir.path().join("wasmer.toml")).unwrap(); +// +// pretty_assertions::assert_eq!( +// manifest, +// r#"[dependencies] +// "wasmer/winterjs" = "*" +// +// [fs] +// "/src" = "./src" +// +// [[command]] +// name = "script" +// module = "wasmer/winterjs:winterjs" +// runner = "https://webc.org/runner/wasi" +// +// [command.annotations.wasi] +// env = ["JS_PATH=/src/index.js"] +// main-args = ["/src/index.js"] +// "#, +// ); +// +// assert!(dir.path().join("src").join("index.js").is_file()); +// } +// +// #[tokio::test] +// async fn test_package_wizard_create_py_worker() { +// let dir = tempfile::tempdir().unwrap(); +// +// PackageWizard { +// path: dir.path().to_owned(), +// type_: Some(PackageType::PyApplication), +// create_mode: CreateMode::Create, +// namespace: None, +// namespace_default: None, +// name: None, +// user: None, +// } +// .run(None) +// .await +// .unwrap(); +// let manifest = std::fs::read_to_string(dir.path().join("wasmer.toml")).unwrap(); +// +// pretty_assertions::assert_eq!( +// manifest, +// r#"[dependencies] +// "wasmer/python" = "^3.12.6" +// +// [fs] +// "/src" = "./src" +// +// [[command]] +// name = "script" +// module = "wasmer/python:python" +// runner = "wasi" +// +// [command.annotations.wasi] +// main-args = ["/src/main.py"] +// "#, +// ); +// +// assert!(dir.path().join("src").join("main.py").is_file()); +// } +// } diff --git a/lib/cli/src/utils/prompts.rs b/lib/cli/src/utils/prompts.rs index ef3618a6880..b5c2c5c1c9e 100644 --- a/lib/cli/src/utils/prompts.rs +++ b/lib/cli/src/utils/prompts.rs @@ -2,7 +2,6 @@ use anyhow::Context; use colored::Colorize; use dialoguer::{theme::ColorfulTheme, Select}; use wasmer_api::WasmerClient; -use wasmer_config::package::NamedPackageIdent; pub fn prompt_for_ident(message: &str, default: Option<&str>) -> Result { loop { @@ -23,38 +22,38 @@ pub fn prompt_for_ident(message: &str, default: Option<&str>) -> Result, -) -> Result { - loop { - let theme = ColorfulTheme::default(); - let raw: String = dialoguer::Input::with_theme(&theme) - .with_prompt(message) - .with_initial_text(default.unwrap_or_default()) - .interact_text() - .context("could not read user input")?; - - match raw.parse::() { - Ok(p) => break Ok(p), - Err(err) => { - eprintln!("invalid package name: {err}"); - } - } - } -} - -/// Defines how to check for a package. -pub enum PackageCheckMode { - /// The package must exist in the registry. - MustExist, - /// The package must NOT exist in the registry. - #[allow(dead_code)] - MustNotExist, -} +// /// Ask a user for a package name. +// /// +// /// Will continue looping until the user provides a valid name. +// pub fn prompt_for_package_ident( +// message: &str, +// default: Option<&str>, +// ) -> Result { +// loop { +// let theme = ColorfulTheme::default(); +// let raw: String = dialoguer::Input::with_theme(&theme) +// .with_prompt(message) +// .with_initial_text(default.unwrap_or_default()) +// .interact_text() +// .context("could not read user input")?; +// +// match raw.parse::() { +// Ok(p) => break Ok(p), +// Err(err) => { +// eprintln!("invalid package name: {err}"); +// } +// } +// } +// } + +// /// Defines how to check for a package. +// pub enum PackageCheckMode { +// /// The package must exist in the registry. +// MustExist, +// /// The package must NOT exist in the registry. +// #[allow(dead_code)] +// MustNotExist, +// } /// Ask a user for a package version. /// @@ -80,51 +79,51 @@ pub fn prompt_for_package_version( } } -/// Ask for a package name. -/// -/// Will continue looping until the user provides a valid name. -/// -/// If an API is provided, will check if the package exists. -pub async fn prompt_for_package( - message: &str, - default: Option<&str>, - check: Option, - client: Option<&WasmerClient>, -) -> Result<(NamedPackageIdent, Option), anyhow::Error> { - loop { - let ident = prompt_for_package_ident(message, default)?; - - if let Some(check) = &check { - let api = client.expect("Check mode specified, but no API provided"); - - let pkg = wasmer_api::query::get_package(api, ident.to_string()) - .await - .context("could not query backend for package")?; - - match check { - PackageCheckMode::MustExist => { - if let Some(pkg) = pkg { - let mut ident = ident; - if let Some(v) = &pkg.last_version { - ident.tag = - Some(wasmer_config::package::Tag::VersionReq(v.version.parse()?)); - } - break Ok((ident, Some(pkg))); - } else { - eprintln!("Package '{ident}' does not exist"); - } - } - PackageCheckMode::MustNotExist => { - if pkg.is_none() { - break Ok((ident, None)); - } else { - eprintln!("Package '{ident}' already exists"); - } - } - } - } - } -} +// /// Ask for a package name. +// /// +// /// Will continue looping until the user provides a valid name. +// /// +// /// If an API is provided, will check if the package exists. +// pub async fn prompt_for_package( +// message: &str, +// default: Option<&str>, +// check: Option, +// client: Option<&WasmerClient>, +// ) -> Result<(NamedPackageIdent, Option), anyhow::Error> { +// loop { +// let ident = prompt_for_package_ident(message, default)?; +// +// if let Some(check) = &check { +// let api = client.expect("Check mode specified, but no API provided"); +// +// let pkg = wasmer_api::query::get_package(api, ident.to_string()) +// .await +// .context("could not query backend for package")?; +// +// match check { +// PackageCheckMode::MustExist => { +// if let Some(pkg) = pkg { +// let mut ident = ident; +// if let Some(v) = &pkg.last_version { +// ident.tag = +// Some(wasmer_config::package::Tag::VersionReq(v.version.parse()?)); +// } +// break Ok((ident, Some(pkg))); +// } else { +// eprintln!("Package '{ident}' does not exist"); +// } +// } +// PackageCheckMode::MustNotExist => { +// if pkg.is_none() { +// break Ok((ident, None)); +// } else { +// eprintln!("Package '{ident}' already exists"); +// } +// } +// } +// } +// } +// } /// Prompt for a namespace. /// From 1931172984db5ae0c2f6da0222b34191e908826d Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Wed, 15 May 2024 19:25:21 +0200 Subject: [PATCH 02/14] chore(cli): make linter happy --- lib/cli/src/utils/mod.rs | 8 +- lib/cli/src/utils/package_wizard/mod.rs | 144 ++++++++++++------------ lib/cli/src/utils/prompts.rs | 8 +- 3 files changed, 80 insertions(+), 80 deletions(-) diff --git a/lib/cli/src/utils/mod.rs b/lib/cli/src/utils/mod.rs index 44b9c44ed13..35eed381dd8 100644 --- a/lib/cli/src/utils/mod.rs +++ b/lib/cli/src/utils/mod.rs @@ -140,7 +140,7 @@ pub fn load_package_manifest( // #[allow(dead_code)] // MustNotExist, // } -// +// // /// Ask for a package name. // /// // /// Will continue looping until the user provides a valid name. @@ -154,14 +154,14 @@ pub fn load_package_manifest( // ) -> Result<(NamedPackageIdent, Option), anyhow::Error> { // loop { // let name = prompt_for_package_name(message, default)?; -// +// // if let Some(check) = &check { // let api = client.expect("Check mode specified, but no API provided"); -// +// // let pkg = wasmer_api::query::get_package(api, name.to_string()) // .await // .context("could not query backend for package")?; -// +// // match check { // PackageCheckMode::MustExist => { // if let Some(pkg) = pkg { diff --git a/lib/cli/src/utils/package_wizard/mod.rs b/lib/cli/src/utils/package_wizard/mod.rs index 609ff37f606..08383cf4584 100644 --- a/lib/cli/src/utils/package_wizard/mod.rs +++ b/lib/cli/src/utils/package_wizard/mod.rs @@ -1,24 +1,24 @@ // use std::path::{Path, PathBuf}; -// +// // use anyhow::Context; // use dialoguer::{theme::ColorfulTheme, Select}; // use wasmer_api::{types::UserWithNamespaces, WasmerClient}; -// +// // use super::prompts::PackageCheckMode; -// +// // const WASM_STATIC_SERVER_PACKAGE: &str = "wasmer/static-web-server"; // const WASM_STATIC_SERVER_VERSION: &str = "1"; -// +// // const WASMER_WINTER_JS_PACKAGE: &str = "wasmer/winterjs"; // const WASMER_WINTER_JS_VERSION: &str = "*"; -// +// // const WASM_PYTHON_PACKAGE: &str = "wasmer/python"; // const WASM_PYTHON_VERSION: &str = "3.12.6"; -// +// // const SAMPLE_INDEX_HTML: &str = include_str!("./templates/static-site/index.html"); // const SAMPLE_JS_WORKER: &str = include_str!("./templates/js-worker/index.js"); // const SAMPLE_PY_APPLICATION: &str = include_str!("./templates/py-application/main.py"); -// +// // #[derive(clap::ValueEnum, Clone, Copy, Debug)] // pub enum PackageType { // #[clap(name = "regular")] @@ -33,7 +33,7 @@ // #[clap(name = "py-application")] // PyApplication, // } -// +// // #[derive(Clone, Copy, Debug)] // pub enum CreateMode { // Create, @@ -41,7 +41,7 @@ // #[allow(dead_code)] // CreateOrSelect, // } -// +// // fn prompt_for_package_type() -> Result { // let theme = ColorfulTheme::default(); // Select::with_theme(&theme) @@ -55,67 +55,67 @@ // }) // .map_err(anyhow::Error::from) // } -// +// // #[derive(Debug)] // pub struct PackageWizard { // pub path: PathBuf, // pub type_: Option, -// +// // pub create_mode: CreateMode, -// +// // /// Namespace to use. // pub namespace: Option, // /// Default namespace to use. // /// Will still show a prompt, with this as the default value. // /// Ignored if [`Self::namespace`] is set. // pub namespace_default: Option, -// +// // /// Pre-configured package name. // pub name: Option, -// +// // pub user: Option, // } -// +// // pub struct PackageWizardOutput { // pub api: Option, // pub local_path: Option, // pub local_manifest: Option, // } -// +// // impl PackageWizard { // fn build_new_package(&self) -> Result { // let ty = match self.type_ { // Some(t) => t, // None => prompt_for_package_type()?, // }; -// +// // if !self.path.is_dir() { // std::fs::create_dir_all(&self.path).with_context(|| { // format!("Failed to create directory: '{}'", self.path.display()) // })?; // } -// +// // let manifest = match ty { // PackageType::Regular => todo!(), // PackageType::StaticWebsite => initialize_static_site(&self.path)?, // PackageType::JsWorker => initialize_js_worker(&self.path)?, // PackageType::PyApplication => initialize_py_worker(&self.path)?, // }; -// +// // let manifest_path = self.path.join("wasmer.toml"); // let manifest_raw = manifest // .to_string() // .context("could not serialize package manifest")?; // std::fs::write(manifest_path, manifest_raw) // .with_context(|| format!("Failed to write manifest to '{}'", self.path.display()))?; -// +// // Ok(PackageWizardOutput { // api: None, // local_path: Some(self.path.clone()), // local_manifest: Some(manifest), // }) // } -// +// // async fn prompt_existing_package( // &self, // api: Option<&WasmerClient>, @@ -126,7 +126,7 @@ // } else { // None // }; -// +// // eprintln!("Enter the name of an existing package:"); // let (_ident, api) = super::prompts::prompt_for_package("Package", None, check, api).await?; // Ok(PackageWizardOutput { @@ -135,7 +135,7 @@ // local_manifest: None, // }) // } -// +// // pub async fn run( // self, // api: Option<&WasmerClient>, @@ -150,7 +150,7 @@ // .items(&["Create new package", "Use existing package"]) // .default(0) // .interact()?; -// +// // match index { // 0 => self.build_new_package(), // 1 => self.prompt_existing_package(api).await, @@ -162,7 +162,7 @@ // } // } // } -// +// // fn initialize_static_site(path: &Path) -> Result { // let pubdir_name = "public"; // let pubdir = path.join(pubdir_name); @@ -171,9 +171,9 @@ // .with_context(|| format!("Failed to create directory: '{}'", pubdir.display()))?; // } // let index = pubdir.join("index.html"); -// +// // let static_html = SAMPLE_INDEX_HTML.replace("{{title}}", "My static website"); -// +// // if !index.is_file() { // std::fs::write(&index, static_html.as_str()) // .with_context(|| "Could not write index.html file".to_string())?; @@ -189,24 +189,24 @@ // .with_context(|| "Could not write index.html file".to_string())?; // } // } -// +// // let raw_static_site_toml = format!( // r#" // [dependencies] // "{}" = "{}" -// +// // [fs] // public = "{}" // "#, // WASM_STATIC_SERVER_PACKAGE, WASM_STATIC_SERVER_VERSION, pubdir_name // ); -// +// // let manifest = wasmer_config::package::Manifest::parse(raw_static_site_toml.as_str()) // .map_err(|e| anyhow::anyhow!("Could not parse js worker manifest: {}", e))?; -// +// // Ok(manifest) // } -// +// // fn initialize_js_worker(path: &Path) -> Result { // let srcdir_name = "src"; // let srcdir = path.join(srcdir_name); @@ -214,16 +214,16 @@ // std::fs::create_dir_all(&srcdir) // .with_context(|| format!("Failed to create directory: '{}'", srcdir.display()))?; // } -// +// // let index_js = srcdir.join("index.js"); -// +// // let sample_js = SAMPLE_JS_WORKER.replace("{{package}}", "My JS worker"); -// +// // if !index_js.is_file() { // std::fs::write(&index_js, sample_js.as_str()) // .with_context(|| "Could not write index.js file".to_string())?; // } -// +// // // get the remote repository if it exists // // Todo: add this to the manifest // // let remote_repo_url = Command::new("git") @@ -232,20 +232,20 @@ // // .arg("origin") // // .output() // // .map_or("".to_string(), |f| String::from_utf8(f.stdout).unwrap()); -// +// // let raw_js_worker_toml = format!( // r#" // [dependencies] // "{winterjs_pkg}" = "{winterjs_version}" -// +// // [fs] // "/src" = "./src" -// +// // [[command]] // name = "script" // module = "{winterjs_pkg}:winterjs" // runner = "https://webc.org/runner/wasi" -// +// // [command.annotations.wasi] // main-args = ["/src/index.js"] // env = ["JS_PATH=/src/index.js"] @@ -253,13 +253,13 @@ // winterjs_pkg = WASMER_WINTER_JS_PACKAGE, // winterjs_version = WASMER_WINTER_JS_VERSION, // ); -// +// // let manifest = wasmer_config::package::Manifest::parse(raw_js_worker_toml.as_str()) // .map_err(|e| anyhow::anyhow!("Could not parse js worker manifest: {}", e))?; -// +// // Ok(manifest) // } -// +// // fn initialize_py_worker(path: &Path) -> Result { // let appdir_name = "src"; // let appdir = path.join(appdir_name); @@ -268,14 +268,14 @@ // .with_context(|| format!("Failed to create directory: '{}'", appdir.display()))?; // } // let main_py = appdir.join("main.py"); -// +// // let sample_main = SAMPLE_PY_APPLICATION.replace("{{package}}", "My Python Worker"); -// +// // if !main_py.is_file() { // std::fs::write(&main_py, sample_main.as_str()) // .with_context(|| "Could not write main.py file".to_string())?; // } -// +// // // Todo: add this to the manifest // // let remote_repo_url = Command::new("git") // // .arg("remote") @@ -283,41 +283,41 @@ // // .arg("origin") // // .output() // // .map_or("".to_string(), |f| String::from_utf8(f.stdout).unwrap()); -// +// // let raw_py_worker_toml = format!( // r#" // [dependencies] // "{}" = "{}" -// +// // [fs] // "/src" = "./src" // # "/.env" = "./.env/" # Bundle the virtualenv -// +// // [[command]] // name = "script" // module = "{}:python" # The "python" atom from "wasmer/python" // runner = "wasi" -// +// // [command.annotations.wasi] // main-args = ["/src/main.py"] -// # env = ["PYTHON_PATH=/app/.env:/etc/python3.12/site-packages"] # Make our virtualenv accessible +// # env = ["PYTHON_PATH=/app/.env:/etc/python3.12/site-packages"] # Make our virtualenv accessible // "#, // WASM_PYTHON_PACKAGE, WASM_PYTHON_VERSION, WASM_PYTHON_PACKAGE // ); -// +// // let manifest = wasmer_config::package::Manifest::parse(raw_py_worker_toml.as_str()) // .map_err(|e| anyhow::anyhow!("Could not parse py worker manifest: {}", e))?; -// +// // Ok(manifest) // } // #[cfg(test)] // mod tests { // use super::*; -// +// // #[tokio::test] // async fn test_package_wizard_create_static_site() { // let dir = tempfile::tempdir().unwrap(); -// +// // PackageWizard { // path: dir.path().to_owned(), // type_: Some(PackageType::StaticWebsite), @@ -330,25 +330,25 @@ // .run(None) // .await // .unwrap(); -// +// // let manifest = std::fs::read_to_string(dir.path().join("wasmer.toml")).unwrap(); // pretty_assertions::assert_eq!( // manifest, // r#"[dependencies] // "wasmer/static-web-server" = "^1" -// +// // [fs] // public = "public" // "#, // ); -// +// // assert!(dir.path().join("public").join("index.html").is_file()); // } -// +// // #[tokio::test] // async fn test_package_wizard_create_js_worker() { // let dir = tempfile::tempdir().unwrap(); -// +// // PackageWizard { // path: dir.path().to_owned(), // type_: Some(PackageType::JsWorker), @@ -362,33 +362,33 @@ // .await // .unwrap(); // let manifest = std::fs::read_to_string(dir.path().join("wasmer.toml")).unwrap(); -// +// // pretty_assertions::assert_eq!( // manifest, // r#"[dependencies] // "wasmer/winterjs" = "*" -// +// // [fs] // "/src" = "./src" -// +// // [[command]] // name = "script" // module = "wasmer/winterjs:winterjs" // runner = "https://webc.org/runner/wasi" -// +// // [command.annotations.wasi] // env = ["JS_PATH=/src/index.js"] // main-args = ["/src/index.js"] // "#, // ); -// +// // assert!(dir.path().join("src").join("index.js").is_file()); // } -// +// // #[tokio::test] // async fn test_package_wizard_create_py_worker() { // let dir = tempfile::tempdir().unwrap(); -// +// // PackageWizard { // path: dir.path().to_owned(), // type_: Some(PackageType::PyApplication), @@ -402,25 +402,25 @@ // .await // .unwrap(); // let manifest = std::fs::read_to_string(dir.path().join("wasmer.toml")).unwrap(); -// +// // pretty_assertions::assert_eq!( // manifest, // r#"[dependencies] // "wasmer/python" = "^3.12.6" -// +// // [fs] // "/src" = "./src" -// +// // [[command]] // name = "script" // module = "wasmer/python:python" // runner = "wasi" -// +// // [command.annotations.wasi] // main-args = ["/src/main.py"] // "#, // ); -// +// // assert!(dir.path().join("src").join("main.py").is_file()); // } // } diff --git a/lib/cli/src/utils/prompts.rs b/lib/cli/src/utils/prompts.rs index b5c2c5c1c9e..eed2aedac11 100644 --- a/lib/cli/src/utils/prompts.rs +++ b/lib/cli/src/utils/prompts.rs @@ -36,7 +36,7 @@ pub fn prompt_for_ident(message: &str, default: Option<&str>) -> Result() { // Ok(p) => break Ok(p), // Err(err) => { @@ -92,14 +92,14 @@ pub fn prompt_for_package_version( // ) -> Result<(NamedPackageIdent, Option), anyhow::Error> { // loop { // let ident = prompt_for_package_ident(message, default)?; -// +// // if let Some(check) = &check { // let api = client.expect("Check mode specified, but no API provided"); -// +// // let pkg = wasmer_api::query::get_package(api, ident.to_string()) // .await // .context("could not query backend for package")?; -// +// // match check { // PackageCheckMode::MustExist => { // if let Some(pkg) = pkg { From b392c32d3e319a0cdc3ff629052f13bab9bffe73 Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Thu, 16 May 2024 13:54:40 +0200 Subject: [PATCH 03/14] feat(cli): Minor changes, changed upload chunk size and making linter happy --- lib/cli/src/commands/app/create.rs | 268 ++++++--------------- lib/cli/src/commands/app/deploy.rs | 29 +-- lib/cli/src/commands/login.rs | 3 +- lib/cli/src/commands/package/build.rs | 9 +- lib/cli/src/commands/package/common/mod.rs | 6 +- lib/cli/src/commands/package/push.rs | 5 +- lib/cli/src/utils/prompts.rs | 162 +++++++------ 7 files changed, 189 insertions(+), 293 deletions(-) diff --git a/lib/cli/src/commands/app/create.rs b/lib/cli/src/commands/app/create.rs index 3f4a184d2d3..02a8b757baa 100644 --- a/lib/cli/src/commands/app/create.rs +++ b/lib/cli/src/commands/app/create.rs @@ -3,7 +3,10 @@ use crate::{ commands::AsyncCliCommand, opts::{ApiOpts, ItemFormatOpts, WasmerEnv}, - utils::load_package_manifest, + utils::{ + load_package_manifest, + prompts::{prompt_for_package, PackageCheckMode}, + }, }; use anyhow::Context; use colored::Colorize; @@ -87,7 +90,7 @@ pub struct CmdAppCreate { pub app_name: Option, /// The path to the directory where the config file for the application will be written to. - #[clap(long = "path")] + #[clap(long = "dir")] pub app_dir_path: Option, /// Do not wait for the app to become reachable if deployed. @@ -146,11 +149,18 @@ impl CmdAppCreate { anyhow::bail!("No app name specified: use --name "); } - let default_name = env::current_dir().ok().and_then(|dir| { - dir.file_name() + let default_name = match &self.app_dir_path { + Some(path) => path + .file_name() .and_then(|f| f.to_str()) - .map(|s| s.to_owned()) - }); + .map(|s| s.to_owned()), + None => env::current_dir().ok().and_then(|dir| { + dir.file_name() + .and_then(|f| f.to_str()) + .map(|s| s.to_owned()) + }), + }; + crate::utils::prompts::prompt_for_ident( "What should be the name of the app?", default_name.as_deref(), @@ -168,7 +178,7 @@ impl CmdAppCreate { } if !self.offline { - let user = wasmer_api::query::current_user_with_namespaces(&client, None).await?; + let user = wasmer_api::query::current_user_with_namespaces(client, None).await?; crate::utils::prompts::prompt_for_namespace( "Who should own this app?", None, @@ -187,7 +197,10 @@ impl CmdAppCreate { owner: &str, app_name: &str, ) -> anyhow::Result { - if !self.use_local_manifest && self.non_interactive { + if (!self.use_local_manifest && self.non_interactive) + || self.template.is_some() + || self.package.is_some() + { return Ok(false); } @@ -225,7 +238,12 @@ impl CmdAppCreate { Ok(false) } - async fn create_from_package(&self, owner: &str, app_name: &str) -> anyhow::Result { + async fn create_from_package( + &self, + client: &WasmerClient, + owner: &str, + app_name: &str, + ) -> anyhow::Result { if self.template.is_some() { return Ok(false); } @@ -236,12 +254,15 @@ impl CmdAppCreate { self.try_deploy(owner, app_name).await?; return Ok(true); } else if !self.non_interactive { - let theme = ColorfulTheme::default(); - let package_name: String = dialoguer::Input::with_theme(&theme) - .with_prompt("What is the name of the package?") - .interact()?; + let (package_id, _) = prompt_for_package( + "Enter the name of the package", + Some("wasmer/hello"), + Some(PackageCheckMode::MustExist), + Some(client), + ) + .await?; - let app_config = self.get_app_config(owner, app_name, &package_name); + let app_config = self.get_app_config(owner, app_name, &package_id.to_string()); write_app_config(&app_config, self.app_dir_path.clone()).await?; self.try_deploy(owner, app_name).await?; return Ok(true); @@ -258,7 +279,7 @@ impl CmdAppCreate { // A utility function used to fetch the URL of the template to use. async fn get_template_url(&self, client: &WasmerClient) -> anyhow::Result { let mut url = if let Some(template) = &self.template { - if let Ok(url) = url::Url::parse(&template) { + if let Ok(url) = url::Url::parse(template) { url } else if let Some(template) = wasmer_api::query::fetch_app_template_from_slug(client, template.clone()).await? @@ -273,15 +294,13 @@ impl CmdAppCreate { } let templates: Vec = - wasmer_api::query::fetch_app_templates(client, String::new(), 20) + wasmer_api::query::fetch_app_templates(client, String::new(), 10) .await? .ok_or(anyhow::anyhow!("No template received from the backend"))? .edges .into_iter() - .filter(|v| v.is_some()) - .map(|v| v.unwrap()) - .filter(|v| v.node.is_some()) - .map(|v| v.node.unwrap()) + .flatten() + .filter_map(|v| v.node) .collect(); let theme = ColorfulTheme::default(); @@ -289,27 +308,23 @@ impl CmdAppCreate { .iter() .map(|t| { format!( - "{}{}{}\n", + "{}{}\n {} {}", t.name.bold(), if t.language.is_empty() { String::new() } else { format!(" {}", t.language.dimmed()) }, - format!( - "\n {} {}", - "demo url:".bold().dimmed(), - t.demo_url.dimmed() - ) + "demo:".bold().dimmed(), + t.demo_url.dimmed() ) }) .collect::>(); - // items.sort(); let dialog = dialoguer::Select::with_theme(&theme) .with_prompt(format!("Select a template ({} available)", items.len())) .items(&items) - .max_length(3) + .max_length(6) .clear(true) .report(false) .default(0); @@ -335,15 +350,15 @@ impl CmdAppCreate { url::Url::parse(&selected_template.repo_url)? }; - if url.path().contains("archive/refs/heads") { - return Ok(url); - } else if url.path().contains("/zipball/") { - return Ok(url); + let url = if url.path().contains("archive/refs/heads") || url.path().contains("/zipball/") { + url } else { let old_path = url.path(); url.set_path(&format!("{old_path}/zipball/main")); - return Ok(url); - } + url + }; + + Ok(url) } async fn create_from_template( @@ -362,6 +377,14 @@ impl CmdAppCreate { PathBuf::from(".").canonicalize()? }; + if output_path.is_dir() && output_path.read_dir()?.next().is_some() { + if !self.quiet { + eprintln!("The current directory is not empty."); + eprintln!("Use the `--dir` flag to specify another directory, or remove files from the currently selected one.") + } + anyhow::bail!("Stopping as the directory is not empty") + } + let pb = indicatif::ProgressBar::new_spinner(); pb.enable_steady_tick(std::time::Duration::from_millis(500)); @@ -376,6 +399,7 @@ impl CmdAppCreate { let response = reqwest::get(url).await?; let bytes = response.bytes().await?; pb.set_message("Unpacking the template.."); + let cursor = Cursor::new(bytes); let mut archive = zip::ZipArchive::new(cursor)?; @@ -408,7 +432,11 @@ impl CmdAppCreate { if !path.exists() { // AsyncRead not implemented for entry.. if entry.is_file() { - let mut outfile = std::fs::File::create(&path)?; + let mut outfile = std::fs::OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(&path)?; std::io::copy(&mut entry, &mut outfile)?; } else { std::fs::create_dir(path)?; @@ -443,6 +471,14 @@ the app:\n" .bold(), contents ); + let bin_name = match std::env::args().nth(0) { + Some(n) => n, + None => String::from("wasmer"), + }; + eprintln!( + "After taking the necessary steps to build your application, re-run `{}`", + format!("{bin_name} deploy").bold() + ) } else { self.try_deploy(owner, app_name).await?; } @@ -470,15 +506,15 @@ the app:\n" no_validate: false, non_interactive: self.non_interactive, publish_package: true, - path: self.app_dir_path.clone(), + dir: self.app_dir_path.clone(), no_wait: self.no_wait, no_default: false, no_persist_id: false, owner: Some(String::from(owner)), app_name: Some(app_name.into()), bump: false, - template: self.template.clone(), - package: self.package.clone(), + template: None, + package: None, use_local_manifest: self.use_local_manifest, }; cmd_deploy.run_async().await?; @@ -512,7 +548,7 @@ impl AsyncCliCommand for CmdAppCreate { self.create_from_template(&client, &owner, &app_name) .await?; } else if self.package.is_some() { - self.create_from_package(&owner, &app_name).await?; + self.create_from_package(&client, &owner, &app_name).await?; } else if !self.non_interactive { let theme = ColorfulTheme::default(); let choice = Select::with_theme(&theme) @@ -525,7 +561,7 @@ impl AsyncCliCommand for CmdAppCreate { self.create_from_template(&client, &owner, &app_name) .await? } - 1 => self.create_from_package(&owner, &app_name).await?, + 1 => self.create_from_package(&client, &owner, &app_name).await?, x => panic!("unhandled selection {x}"), }; } else { @@ -536,155 +572,3 @@ impl AsyncCliCommand for CmdAppCreate { Ok(()) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_app_create_static_site_offline() { - let dir = tempfile::tempdir().unwrap(); - - let cmd = CmdAppCreate { - quiet: true, - template: Some(AppType::StaticWebsite), - deploy_app: false, - no_validate: false, - non_interactive: true, - offline: true, - owner: Some("testuser".to_string()), - app_name: Some("static-site-1".to_string()), - app_dir_path: Some(dir.path().to_owned()), - no_wait: true, - api: ApiOpts::default(), - fmt: ItemFormatOpts::default(), - package: Some("testuser/static-site-1@0.1.0".to_string()), - use_local_manifest: false, - new_package_name: None, - env: WasmerEnv::default(), - }; - cmd.run_async().await.unwrap(); - - let app = std::fs::read_to_string(dir.path().join("app.yaml")).unwrap(); - assert_eq!( - app, - r#"kind: wasmer.io/App.v0 -name: static-site-1 -owner: testuser -package: testuser/static-site-1@^0.1.0 -debug: false -"#, - ); - } - - #[tokio::test] - async fn test_app_create_offline_with_package() { - let dir = tempfile::tempdir().unwrap(); - - let cmd = CmdAppCreate { - quiet: true, - template: Some(AppType::HttpServer), - deploy_app: false, - no_validate: false, - non_interactive: true, - offline: true, - owner: Some("wasmer".to_string()), - app_name: Some("testapp".to_string()), - app_dir_path: Some(dir.path().to_owned()), - no_wait: true, - api: ApiOpts::default(), - fmt: ItemFormatOpts::default(), - package: Some("wasmer/testpkg".to_string()), - use_local_manifest: false, - new_package_name: None, - env: WasmerEnv::default(), - }; - cmd.run_async().await.unwrap(); - - let app = std::fs::read_to_string(dir.path().join("app.yaml")).unwrap(); - assert_eq!( - app, - r#"kind: wasmer.io/App.v0 -name: testapp -owner: wasmer -package: wasmer/testpkg -debug: false -"#, - ); - } - #[tokio::test] - async fn test_app_create_js_worker() { - let dir = tempfile::tempdir().unwrap(); - - let cmd = CmdAppCreate { - quiet: true, - template: Some(AppType::JsWorker), - deploy_app: false, - no_validate: false, - non_interactive: true, - offline: true, - owner: Some("wasmer".to_string()), - app_name: Some("test-js-worker".to_string()), - app_dir_path: Some(dir.path().to_owned()), - no_wait: true, - api: ApiOpts::default(), - fmt: ItemFormatOpts::default(), - package: Some("wasmer/test-js-worker".to_string()), - use_local_manifest: false, - new_package_name: None, - env: WasmerEnv::default(), - }; - cmd.run_async().await.unwrap(); - - let app = std::fs::read_to_string(dir.path().join("app.yaml")).unwrap(); - assert_eq!( - app, - r#"kind: wasmer.io/App.v0 -name: test-js-worker -owner: wasmer -package: wasmer/test-js-worker -cli_args: -- /src/index.js -debug: false -"#, - ); - } - - #[tokio::test] - async fn test_app_create_py_worker() { - let dir = tempfile::tempdir().unwrap(); - - let cmd = CmdAppCreate { - quiet: true, - template: Some(AppType::PyApplication), - deploy_app: false, - no_validate: false, - non_interactive: true, - offline: true, - owner: Some("wasmer".to_string()), - app_name: Some("test-py-worker".to_string()), - app_dir_path: Some(dir.path().to_owned()), - no_wait: true, - api: ApiOpts::default(), - fmt: ItemFormatOpts::default(), - package: Some("wasmer/test-py-worker".to_string()), - use_local_manifest: false, - new_package_name: None, - env: WasmerEnv::default(), - }; - cmd.run_async().await.unwrap(); - - let app = std::fs::read_to_string(dir.path().join("app.yaml")).unwrap(); - assert_eq!( - app, - r#"kind: wasmer.io/App.v0 -name: test-py-worker -owner: wasmer -package: wasmer/test-py-worker -cli_args: -- /src/main.py -debug: false -"#, - ); - } -} diff --git a/lib/cli/src/commands/app/deploy.rs b/lib/cli/src/commands/app/deploy.rs index d5dd498e2c6..b54f080a9c4 100644 --- a/lib/cli/src/commands/app/deploy.rs +++ b/lib/cli/src/commands/app/deploy.rs @@ -45,9 +45,9 @@ pub struct CmdAppDeploy { #[clap(long)] pub publish_package: bool, - /// The path to the app.yaml file. + /// The path to the directory containing the `app.yaml` file. #[clap(long)] - pub path: Option, + pub dir: Option, /// Do not wait for the app to become reachable. #[clap(long)] @@ -199,8 +199,8 @@ impl CmdAppDeploy { no_validate: false, non_interactive: false, offline: false, - owner: None, - app_name: None, + owner: self.owner.clone(), + app_name: self.app_name.clone(), no_wait: self.no_wait, api: self.api.clone(), env: self.env.clone(), @@ -209,7 +209,7 @@ impl CmdAppDeploy { }, package: self.package.clone(), template: self.template.clone(), - app_dir_path: None, + app_dir_path: self.dir.clone(), use_local_manifest: self.use_local_manifest, new_package_name: None, }; @@ -226,7 +226,7 @@ impl AsyncCliCommand for CmdAppDeploy { let client = login_user(&self.api, &self.env, !self.non_interactive, "deploy an app").await?; - let base_dir_path = self.path.clone().unwrap_or(std::env::current_dir()?); + let base_dir_path = self.dir.clone().unwrap_or(std::env::current_dir()?); let (app_config_path, base_dir_path) = { if base_dir_path.is_file() { ( @@ -242,16 +242,13 @@ impl AsyncCliCommand for CmdAppDeploy { } }; - if !app_config_path.is_file() { - if !self.non_interactive { - // Create already points back to deploy. - return self.create().await; - } else { - anyhow::bail!( - "Cannot deploy app as no app.yaml was found in path '{}'", - app_config_path.display() - ) - } + if !app_config_path.is_file() + || self.template.is_some() + || self.package.is_some() + || self.use_local_manifest + { + // Create already points back to deploy. + return self.create().await; } assert!(app_config_path.is_file()); diff --git a/lib/cli/src/commands/login.rs b/lib/cli/src/commands/login.rs index 7986dbd2a9f..a917de52304 100644 --- a/lib/cli/src/commands/login.rs +++ b/lib/cli/src/commands/login.rs @@ -2,6 +2,7 @@ use std::{net::TcpListener, path::PathBuf, str::FromStr, time::Duration}; use anyhow::Ok; use clap::Parser; +use colored::Colorize; #[cfg(not(test))] use dialoguer::{console::style, Input}; use hyper::{ @@ -302,7 +303,7 @@ impl Login { match res { Some(s) => { print!("Done!"); - println!("\n✅ Login for Wasmer user {:?} saved", s) + println!("\n{} Login for Wasmer user {:?} saved","✔".green().bold(), s) } None => print!( "Warning: no user found on {:?} with the provided token.\nToken saved regardless.", diff --git a/lib/cli/src/commands/package/build.rs b/lib/cli/src/commands/package/build.rs index d87d25a469f..28860967e07 100644 --- a/lib/cli/src/commands/package/build.rs +++ b/lib/cli/src/commands/package/build.rs @@ -54,8 +54,13 @@ impl PackageBuild { manifest_path.display() ) }; - let pkg = webc::wasmer_package::Package::from_manifest(manifest_path)?; - let data = pkg.serialize()?; + let pkg = webc::wasmer_package::Package::from_manifest(manifest_path.clone()).context( + format!( + "While parsing the manifest (loaded from {})", + manifest_path.canonicalize()?.display() + ), + )?; + let data = pkg.serialize().context("While validating the package")?; let hash = sha2::Sha256::digest(&data).into(); let pkg_hash = PackageHash::from_sha256_bytes(hash); diff --git a/lib/cli/src/commands/package/common/mod.rs b/lib/cli/src/commands/package/common/mod.rs index 16254b7b91a..38f8b1ce755 100644 --- a/lib/cli/src/commands/package/common/mod.rs +++ b/lib/cli/src/commands/package/common/mod.rs @@ -117,18 +117,16 @@ pub(super) async fn upload( let total_bytes = bytes.len(); pb.set_length(total_bytes.try_into().unwrap()); - pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})") + pb.set_style(ProgressStyle::with_template("{spinner:.yellow} [{elapsed_precise}] [{bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})") .unwrap() .progress_chars("█▉▊▋▌▍▎▏ ") .tick_strings(&["✶", "✸", "✹", "✺", "✹", "✷"])); tracing::info!("webc is {total_bytes} bytes long"); - let chunk_size = 1_048_576; // 1MB - 315s / 100MB + let chunk_size = (total_bytes / 20).min(1_048_576); let chunks = bytes.chunks(chunk_size); let mut total_bytes_sent = 0; - let client = reqwest::Client::builder().build().unwrap(); - for chunk in chunks { let n = chunk.len(); diff --git a/lib/cli/src/commands/package/push.rs b/lib/cli/src/commands/package/push.rs index 9ac40a345f3..71e19260a06 100644 --- a/lib/cli/src/commands/package/push.rs +++ b/lib/cli/src/commands/package/push.rs @@ -3,6 +3,7 @@ use crate::{ commands::{AsyncCliCommand, PackageBuild}, opts::{ApiOpts, WasmerEnv}, }; +use anyhow::Context; use colored::Colorize; use is_terminal::IsTerminal; use std::path::{Path, PathBuf}; @@ -156,7 +157,9 @@ impl PackagePush { ) -> anyhow::Result<(String, PackageHash)> { tracing::info!("Building package"); let pb = make_spinner!(self.quiet, "Creating the package locally..."); - let (package, hash) = PackageBuild::check(manifest_path.to_path_buf()).execute()?; + let (package, hash) = PackageBuild::check(manifest_path.to_path_buf()) + .execute() + .context("While trying to build the package locally")?; spinner_ok!(pb, "Correctly built package locally"); tracing::info!("Package has hash: {hash}"); diff --git a/lib/cli/src/utils/prompts.rs b/lib/cli/src/utils/prompts.rs index eed2aedac11..7c2b9edfe7c 100644 --- a/lib/cli/src/utils/prompts.rs +++ b/lib/cli/src/utils/prompts.rs @@ -2,6 +2,7 @@ use anyhow::Context; use colored::Colorize; use dialoguer::{theme::ColorfulTheme, Select}; use wasmer_api::WasmerClient; +use wasmer_config::package::NamedPackageIdent; pub fn prompt_for_ident(message: &str, default: Option<&str>) -> Result { loop { @@ -22,38 +23,38 @@ pub fn prompt_for_ident(message: &str, default: Option<&str>) -> Result, -// ) -> Result { -// loop { -// let theme = ColorfulTheme::default(); -// let raw: String = dialoguer::Input::with_theme(&theme) -// .with_prompt(message) -// .with_initial_text(default.unwrap_or_default()) -// .interact_text() -// .context("could not read user input")?; -// -// match raw.parse::() { -// Ok(p) => break Ok(p), -// Err(err) => { -// eprintln!("invalid package name: {err}"); -// } -// } -// } -// } - -// /// Defines how to check for a package. -// pub enum PackageCheckMode { -// /// The package must exist in the registry. -// MustExist, -// /// The package must NOT exist in the registry. -// #[allow(dead_code)] -// MustNotExist, -// } +/// Ask a user for a package name. +/// +/// Will continue looping until the user provides a valid name. +pub fn prompt_for_package_ident( + message: &str, + default: Option<&str>, +) -> Result { + loop { + let theme = ColorfulTheme::default(); + let raw: String = dialoguer::Input::with_theme(&theme) + .with_prompt(message) + .with_initial_text(default.unwrap_or_default()) + .interact_text() + .context("could not read user input")?; + + match raw.parse::() { + Ok(p) => break Ok(p), + Err(err) => { + eprintln!("invalid package name: {err}"); + } + } + } +} + +/// Defines how to check for a package. +pub enum PackageCheckMode { + /// The package must exist in the registry. + MustExist, + /// The package must NOT exist in the registry. + #[allow(dead_code)] + MustNotExist, +} /// Ask a user for a package version. /// @@ -79,51 +80,58 @@ pub fn prompt_for_package_version( } } -// /// Ask for a package name. -// /// -// /// Will continue looping until the user provides a valid name. -// /// -// /// If an API is provided, will check if the package exists. -// pub async fn prompt_for_package( -// message: &str, -// default: Option<&str>, -// check: Option, -// client: Option<&WasmerClient>, -// ) -> Result<(NamedPackageIdent, Option), anyhow::Error> { -// loop { -// let ident = prompt_for_package_ident(message, default)?; -// -// if let Some(check) = &check { -// let api = client.expect("Check mode specified, but no API provided"); -// -// let pkg = wasmer_api::query::get_package(api, ident.to_string()) -// .await -// .context("could not query backend for package")?; -// -// match check { -// PackageCheckMode::MustExist => { -// if let Some(pkg) = pkg { -// let mut ident = ident; -// if let Some(v) = &pkg.last_version { -// ident.tag = -// Some(wasmer_config::package::Tag::VersionReq(v.version.parse()?)); -// } -// break Ok((ident, Some(pkg))); -// } else { -// eprintln!("Package '{ident}' does not exist"); -// } -// } -// PackageCheckMode::MustNotExist => { -// if pkg.is_none() { -// break Ok((ident, None)); -// } else { -// eprintln!("Package '{ident}' already exists"); -// } -// } -// } -// } -// } -// } +/// Ask for a package name. +/// +/// Will continue looping until the user provides a valid name. +/// +/// If an API is provided, will check if the package exists. +pub async fn prompt_for_package( + message: &str, + default: Option<&str>, + check: Option, + client: Option<&WasmerClient>, +) -> Result<(NamedPackageIdent, Option), anyhow::Error> { + loop { + let ident = prompt_for_package_ident(message, default)?; + + if let Some(check) = &check { + let api = client.expect("Check mode specified, but no API provided"); + + let pkg = if let Some(v) = ident.version_opt() { + wasmer_api::query::get_package_version(api, ident.full_name(), v.to_string()) + .await + .context("could not query backend for package")? + .map(|p| p.package) + } else { + wasmer_api::query::get_package(api, ident.to_string()) + .await + .context("could not query backend for package")? + }; + + match check { + PackageCheckMode::MustExist => { + if let Some(pkg) = pkg { + let mut ident = ident; + if let Some(v) = &pkg.last_version { + ident.tag = + Some(wasmer_config::package::Tag::VersionReq(v.version.parse()?)); + } + break Ok((ident, Some(pkg))); + } else { + eprintln!("Package '{ident}' does not exist"); + } + } + PackageCheckMode::MustNotExist => { + if pkg.is_none() { + break Ok((ident, None)); + } else { + eprintln!("Package '{ident}' already exists"); + } + } + } + } + } +} /// Prompt for a namespace. /// From 5de36c597db3b66f67b43c783b883941b38981dd Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Thu, 16 May 2024 16:18:43 +0200 Subject: [PATCH 04/14] fix(cli): Remove deflate-zlib-ng dependency ..and fix the cmake issue in CI --- Cargo.lock | 166 +-------------------------------------------- lib/cli/Cargo.toml | 4 +- 2 files changed, 4 insertions(+), 166 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6fdcbb8b63f..cc8395eb015 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,17 +17,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if 1.0.0", - "cipher", - "cpufeatures", -] - [[package]] name = "ahash" version = "0.7.8" @@ -461,27 +450,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bzip2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" -dependencies = [ - "bzip2-sys", - "libc", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.11+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "camino" version = "1.1.6" @@ -686,15 +654,6 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "cmake" -version = "0.1.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" -dependencies = [ - "cc", -] - [[package]] name = "colorchoice" version = "1.0.1" @@ -962,21 +921,6 @@ dependencies = [ "build_const", ] -[[package]] -name = "crc" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - [[package]] name = "crc32fast" version = "1.4.0" @@ -1310,12 +1254,6 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" -[[package]] -name = "deflate64" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83ace6c86376be0b6cdcf3fb41882e81d94b31587573d1cfa9d01cd06bba210d" - [[package]] name = "deranged" version = "0.3.11" @@ -1768,7 +1706,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", - "libz-ng-sys", "miniz_oxide", ] @@ -2782,16 +2719,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "libz-ng-sys" -version = "1.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6409efc61b12687963e602df8ecf70e8ddacf95bc6576bcf16e3ac6328083c5" -dependencies = [ - "cmake", - "libc", -] - [[package]] name = "linked-hash-map" version = "0.5.6" @@ -2914,17 +2841,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aba8ecb0450dfabce4ad72085eed0a75dffe8f21f7ada05638564ea9db2d7fb1" dependencies = [ "byteorder", - "crc 1.8.1", -] - -[[package]] -name = "lzma-rs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" -dependencies = [ - "byteorder", - "crc 3.2.1", + "crc", ] [[package]] @@ -4763,12 +4680,6 @@ dependencies = [ "libc", ] -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - [[package]] name = "simdutf8" version = "0.1.4" @@ -5807,12 +5718,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "typed-arena" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" - [[package]] name = "typenum" version = "1.17.0" @@ -6989,7 +6894,7 @@ dependencies = [ "indicatif", "lazy_static", "log 0.4.21", - "lzma-rs 0.2.0", + "lzma-rs", "minisign", "pretty_assertions", "rand", @@ -7772,20 +7677,6 @@ name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.61", -] [[package]] name = "zip" @@ -7793,64 +7684,11 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c700ea425e148de30c29c580c1f9508b93ca57ad31c9f4e96b83c194c37a7a8f" dependencies = [ - "aes", "arbitrary", - "bzip2", - "constant_time_eq", "crc32fast", "crossbeam-utils 0.8.19", - "deflate64", "displaydoc", "flate2", - "hmac", "indexmap 2.2.6", - "lzma-rs 0.3.0", - "pbkdf2", - "rand", - "sha1", "thiserror", - "time 0.3.36", - "zeroize", - "zopfli", - "zstd", -] - -[[package]] -name = "zopfli" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1f48f3508a3a3f2faee01629564400bc12260f6214a056d06a3aaaa6ef0736" -dependencies = [ - "crc32fast", - "log 0.4.21", - "simd-adler32", - "typed-arena", -] - -[[package]] -name = "zstd" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.10+zstd.1.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" -dependencies = [ - "cc", - "pkg-config", ] diff --git a/lib/cli/Cargo.toml b/lib/cli/Cargo.toml index 4746ede9ccd..ac646394ee0 100644 --- a/lib/cli/Cargo.toml +++ b/lib/cli/Cargo.toml @@ -118,7 +118,7 @@ wasmer-compiler-singlepass = { version = "=4.3.0", path = "../compiler-singlepas wasmer-compiler-llvm = { version = "=4.3.0", path = "../compiler-llvm", optional = true } wasmer-emscripten = { version = "=4.3.0", path = "../emscripten" } wasmer-vm = { version = "=4.3.0", path = "../vm", optional = true } -wasmer-wasix = { path = "../wasix", version="=0.20.0" , features = [ +wasmer-wasix = { path = "../wasix", version = "=0.20.0", features = [ "logging", "webc_runner_rt_wcgi", "webc_runner_rt_dcgi", @@ -229,7 +229,7 @@ tun-tap = { version = "0.1.3", features = ["tokio"], optional = true } clap_complete = "4.5.2" clap_mangen = "0.2.20" -zip = "1.2.3" +zip = { version = "1.2.3", default-features = false, features = ["deflate"] } # NOTE: Must use different features for clap because the "color" feature does not # work on wasi due to the anstream dependency not compiling. From 93319d64d0709d4421cd7d08171558893501b930 Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Thu, 16 May 2024 18:33:38 +0200 Subject: [PATCH 05/14] chore(cli): remove unused functions --- lib/cli/src/commands/app/create.rs | 7 +- lib/cli/src/utils/mod.rs | 288 ----------------------------- 2 files changed, 2 insertions(+), 293 deletions(-) diff --git a/lib/cli/src/commands/app/create.rs b/lib/cli/src/commands/app/create.rs index 02a8b757baa..c941bf1b580 100644 --- a/lib/cli/src/commands/app/create.rs +++ b/lib/cli/src/commands/app/create.rs @@ -3,10 +3,7 @@ use crate::{ commands::AsyncCliCommand, opts::{ApiOpts, ItemFormatOpts, WasmerEnv}, - utils::{ - load_package_manifest, - prompts::{prompt_for_package, PackageCheckMode}, - }, + utils::{load_package_manifest, prompts::PackageCheckMode}, }; use anyhow::Context; use colored::Colorize; @@ -254,7 +251,7 @@ impl CmdAppCreate { self.try_deploy(owner, app_name).await?; return Ok(true); } else if !self.non_interactive { - let (package_id, _) = prompt_for_package( + let (package_id, _) = crate::utils::prompts::prompt_for_package( "Enter the name of the package", Some("wasmer/hello"), Some(PackageCheckMode::MustExist), diff --git a/lib/cli/src/utils/mod.rs b/lib/cli/src/utils/mod.rs index 35eed381dd8..7cd7550963d 100644 --- a/lib/cli/src/utils/mod.rs +++ b/lib/cli/src/utils/mod.rs @@ -108,294 +108,6 @@ pub fn load_package_manifest( Ok(Some((file_path, manifest))) } -///// Ask a user for a package name. -///// -///// Will continue looping until the user provides a valid name. -//pub fn prompt_for_package_name( -// message: &str, -// default: Option<&str>, -//) -> Result { -// loop { -// let theme = ColorfulTheme::default(); -// let raw: String = dialoguer::Input::with_theme(&theme) -// .with_prompt(message) -// .with_initial_text(default.unwrap_or_default()) -// .interact_text() -// .context("could not read user input")?; -// -// match raw.parse::() { -// Ok(p) => break Ok(p), -// Err(err) => { -// eprintln!("invalid package name: {err}"); -// } -// } -// } -//} - -// /// Defines how to check for a package. -// pub enum PackageCheckMode { -// /// The package must exist in the registry. -// MustExist, -// /// The package must NOT exist in the registry. -// #[allow(dead_code)] -// MustNotExist, -// } -// -// /// Ask for a package name. -// /// -// /// Will continue looping until the user provides a valid name. -// /// -// /// If an API is provided, will check if the package exists. -// pub async fn prompt_for_package( -// message: &str, -// default: Option<&str>, -// check: Option, -// client: Option<&WasmerClient>, -// ) -> Result<(NamedPackageIdent, Option), anyhow::Error> { -// loop { -// let name = prompt_for_package_name(message, default)?; -// -// if let Some(check) = &check { -// let api = client.expect("Check mode specified, but no API provided"); -// -// let pkg = wasmer_api::query::get_package(api, name.to_string()) -// .await -// .context("could not query backend for package")?; -// -// match check { -// PackageCheckMode::MustExist => { -// if let Some(pkg) = pkg { -// break Ok((name, Some(pkg))); -// } else { -// eprintln!("Package '{name}' does not exist"); -// } -// } -// PackageCheckMode::MustNotExist => { -// if pkg.is_none() { -// break Ok((name, None)); -// } else { -// eprintln!("Package '{name}' already exists"); -// } -// } -// } -// } -// } -// } - -// /// Republish the package described by the [`wasmer_config::package::Manifest`] given as argument and return a -// /// [`Result`]. -// /// -// /// If the package described is named (i.e. has name, namespace and version), the returned manifest -// /// will have its minor version bumped. If the package is unnamed, the returned manifest will be -// /// equal to the one given as input. -// pub async fn republish_package( -// client: &WasmerClient, -// manifest_path: &Path, -// manifest: wasmer_config::package::Manifest, -// patch_owner: Option, -// ) -> Result<(wasmer_config::package::Manifest, Option), anyhow::Error> { -// let manifest_path = if manifest_path.is_file() { -// manifest_path.to_owned() -// } else { -// manifest_path.join(DEFAULT_PACKAGE_MANIFEST_FILE) -// }; -// -// let dir = manifest_path -// .parent() -// .context("could not determine wasmer.toml parent directory")? -// .to_owned(); -// -// let new_manifest = match &manifest.package { -// None => manifest.clone(), -// Some(pkg) => { -// let mut pkg = pkg.clone(); -// let name = pkg.name.clone(); -// -// let current_opt = wasmer_api::query::get_package(client, pkg.name.clone()) -// .await -// .context("could not load package info from backend")? -// .and_then(|x| x.last_version); -// -// let new_version = if let Some(current) = ¤t_opt { -// let mut v = semver::Version::parse(¤t.version).with_context(|| { -// format!("Could not parse package version: '{}'", current.version) -// })?; -// -// v.patch += 1; -// -// // The backend does not have a reliable way to return the latest version, -// // so we have to check each version in a loop. -// loop { -// let version = format!("={}", v); -// let version = wasmer_api::query::get_package_version( -// client, -// name.clone(), -// version.clone(), -// ) -// .await -// .context("could not load package info from backend")?; -// -// if version.is_some() { -// v.patch += 1; -// } else { -// break; -// } -// } -// -// v -// } else { -// pkg.version -// }; -// -// pkg.version = new_version; -// -// let mut manifest = manifest.clone(); -// manifest.package = Some(pkg); -// -// let contents = toml::to_string(&manifest).with_context(|| { -// format!( -// "could not persist manifest to '{}'", -// manifest_path.display() -// ) -// })?; -// -// std::fs::write(manifest_path.clone(), contents).with_context(|| { -// format!("could not write manifest to '{}'", manifest_path.display()) -// })?; -// -// manifest -// } -// }; -// -// let registry = client.graphql_endpoint().to_string(); -// let token = client -// .auth_token() -// .context("no auth token configured - run 'wasmer login'")? -// .to_string(); -// -// let publish = wasmer_registry::package::builder::Publish { -// registry: Some(registry), -// dry_run: false, -// quiet: false, -// package_name: None, -// version: None, -// wait: wasmer_registry::publish::PublishWait::new_none(), -// token, -// no_validate: true, -// package_path: Some(dir.to_str().unwrap().to_string()), -// // Use a high timeout to prevent interrupting uploads of -// // large packages. -// timeout: std::time::Duration::from_secs(60 * 60 * 12), -// package_namespace: patch_owner, -// }; -// -// // Publish uses a blocking http client internally, which leads to a -// // "can't drop a runtime within an async context" error, so this has -// // to be run in a separate thread. -// let maybe_hash = std::thread::spawn(move || publish.execute()) -// .join() -// .map_err(|e| anyhow::format_err!("failed to publish package: {:?}", e))??; -// -// Ok((new_manifest.clone(), maybe_hash)) -// } - -///// Re-publish a package with an increased minor version. -//pub async fn republish_package_with_bumped_version( -// client: &WasmerClient, -// manifest_path: &Path, -// mut manifest: wasmer_config::package::Manifest, -//) -> Result { -// // Try to load existing version. -// // If it does not exist yet, we don't need to increment. -// -// let current_opt = wasmer_api::query::get_package(client, manifest.package.name.clone()) -// .await -// .context("could not load package info from backend")? -// .and_then(|x| x.last_version); -// -// let new_version = if let Some(current) = ¤t_opt { -// let mut v = semver::Version::parse(¤t.version) -// .with_context(|| format!("Could not parse package version: '{}'", current.version))?; -// -// v.patch += 1; -// -// // The backend does not have a reliable way to return the latest version, -// // so we have to check each version in a loop. -// loop { -// let version = format!("={}", v); -// let version = wasmer_api::query::get_package_version( -// client, -// manifest.package.name.clone(), -// version.clone(), -// ) -// .await -// .context("could not load package info from backend")?; -// -// if version.is_some() { -// v.patch += 1; -// } else { -// break; -// } -// } -// -// v -// } else { -// manifest.package.version -// }; -// -// manifest.package.version = new_version; -// let contents = toml::to_string(&manifest).with_context(|| { -// format!( -// "could not persist manifest to '{}'", -// manifest_path.display() -// ) -// })?; -// -// let manifest_path = if manifest_path.is_file() { -// manifest_path.to_owned() -// } else { -// manifest_path.join(DEFAULT_PACKAGE_MANIFEST_FILE) -// }; -// -// std::fs::write(manifest_path.clone(), contents) -// .with_context(|| format!("could not write manifest to '{}'", manifest_path.display()))?; -// -// let dir = manifest_path -// .parent() -// .context("could not determine wasmer.toml parent directory")? -// .to_owned(); -// -// let registry = client.graphql_endpoint().to_string(); -// let token = client -// .auth_token() -// .context("no auth token configured - run 'wasmer login'")? -// .to_string(); -// -// let publish = wasmer_registry::package::builder::Publish { -// registry: Some(registry), -// dry_run: false, -// quiet: false, -// package_name: None, -// version: None, -// wait: wasmer_registry::publish::PublishWait::new_none(), -// token, -// no_validate: true, -// package_path: Some(dir.to_str().unwrap().to_string()), -// // Use a high timeout to prevent interrupting uploads of -// // large packages. -// timeout: std::time::Duration::from_secs(60 * 60 * 12), -// }; -// -// // Publish uses a blocking http client internally, which leads to a -// // "can't drop a runtime within an async context" error, so this has -// // to be run in a separate thread. -// std::thread::spawn(move || publish.execute()) -// .join() -// .map_err(|e| anyhow::format_err!("failed to publish package: {:?}", e))??; -// -// Ok(manifest) -//} - /// The identifier for an app or package in the form, `owner/package@version`, /// where the `owner` and `version` are optional. #[derive(Debug, Clone, PartialEq)] From a64745d23415340088b7bf2fb9b256cfedee3f06 Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Thu, 16 May 2024 18:36:18 +0200 Subject: [PATCH 06/14] feat(cli): use streams to upload packages --- lib/cli/src/commands/package/common/mod.rs | 75 ++++++++++++---------- lib/cli/src/commands/package/push.rs | 2 +- 2 files changed, 43 insertions(+), 34 deletions(-) diff --git a/lib/cli/src/commands/package/common/mod.rs b/lib/cli/src/commands/package/common/mod.rs index 38f8b1ce755..612c1fe3b49 100644 --- a/lib/cli/src/commands/package/common/mod.rs +++ b/lib/cli/src/commands/package/common/mod.rs @@ -5,6 +5,7 @@ use crate::{ }; use colored::Colorize; use dialoguer::Confirm; +use hyper::Body; use indicatif::{ProgressBar, ProgressStyle}; use std::{ collections::BTreeMap, @@ -42,12 +43,12 @@ pub(super) async fn upload( hash: &PackageHash, timeout: humantime::Duration, package: &Package, - pb: &ProgressBar, + pb: ProgressBar, ) -> anyhow::Result { let hash_str = hash.to_string(); let hash_str = hash_str.trim_start_matches("sha256:"); - let url = { + let session_uri = { let default_timeout_secs = Some(60 * 30); let q = wasmer_api::query::get_signed_url_for_package_upload( client, @@ -65,7 +66,7 @@ pub(super) async fn upload( } }; - tracing::info!("signed url is: {url}"); + tracing::info!("signed url is: {session_uri}"); let client = reqwest::Client::builder() .default_headers(reqwest::header::HeaderMap::default()) @@ -74,7 +75,7 @@ pub(super) async fn upload( .unwrap(); let res = client - .post(&url) + .post(&session_uri) .header(reqwest::header::CONTENT_LENGTH, "0") .header(reqwest::header::CONTENT_TYPE, "application/octet-stream") .header("x-goog-resumable", "start"); @@ -123,36 +124,44 @@ pub(super) async fn upload( .tick_strings(&["✶", "✸", "✹", "✺", "✹", "✷"])); tracing::info!("webc is {total_bytes} bytes long"); - let chunk_size = (total_bytes / 20).min(1_048_576); - let chunks = bytes.chunks(chunk_size); - let mut total_bytes_sent = 0; - - for chunk in chunks { - let n = chunk.len(); - - let start = total_bytes_sent; - let end = start + chunk.len().saturating_sub(1); - let content_range = format!("bytes {start}-{end}/{total_bytes}"); - - let res = client - .put(&session_uri) - .header(reqwest::header::CONTENT_TYPE, "application/octet-stream") - .header(reqwest::header::CONTENT_LENGTH, format!("{}", chunk.len())) - .header("Content-Range".to_string(), content_range) - .body(chunk.to_vec()); - - res.send() - .await - .map(|response| response.error_for_status()) - .map_err(|e| { - anyhow::anyhow!("cannot send request to {session_uri} (chunk {start}..{end}): {e}",) - })??; - - total_bytes_sent += n; - pb.set_position(total_bytes_sent.try_into().unwrap()); - } + let chunk_size = (total_bytes / 20).min(10485760); + + let stream = futures::stream::unfold(0, move |offset| { + let pb = pb.clone(); + let bytes = bytes.clone(); + async move { + if offset >= total_bytes { + return None; + } + + let start = offset; + + let end = if (start + chunk_size) >= total_bytes { + total_bytes + } else { + start + chunk_size + }; + + let n = end - start; + let next_chunk = bytes.slice(start..end); + pb.inc(n as u64); + + Some((Ok::<_, std::io::Error>(next_chunk), offset + n)) + } + }); + + let res = client + .put(&session_uri) + .header(reqwest::header::CONTENT_TYPE, "application/octet-stream") + .header(reqwest::header::CONTENT_LENGTH, format!("{}", total_bytes)) + .body(Body::wrap_stream(stream)); + + res.send() + .await + .map(|response| response.error_for_status()) + .map_err(|e| anyhow::anyhow!("error uploading package to {session_uri}: {e}",))??; - Ok(url) + Ok(session_uri) } /// Read and return a manifest given a path. diff --git a/lib/cli/src/commands/package/push.rs b/lib/cli/src/commands/package/push.rs index 71e19260a06..7fc583a3fb2 100644 --- a/lib/cli/src/commands/package/push.rs +++ b/lib/cli/src/commands/package/push.rs @@ -120,7 +120,7 @@ impl PackagePush { ) -> anyhow::Result<()> { let pb = make_spinner!(self.quiet, "Uploading the package to the registry.."); - let signed_url = upload(client, package_hash, self.timeout, package, &pb).await?; + let signed_url = upload(client, package_hash, self.timeout, package, pb.clone()).await?; let id = match wasmer_api::query::push_package_release( client, From 4f290ab7a0234a626ad5d77c8b7ed4f16764bdcc Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Thu, 16 May 2024 19:04:58 +0200 Subject: [PATCH 07/14] fix(cli): move spinners around --- lib/cli/src/commands/app/create.rs | 5 +++++ lib/cli/src/commands/package/common/wait.rs | 6 ------ lib/cli/src/commands/package/push.rs | 13 +++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/cli/src/commands/app/create.rs b/lib/cli/src/commands/app/create.rs index c941bf1b580..14daaa7b730 100644 --- a/lib/cli/src/commands/app/create.rs +++ b/lib/cli/src/commands/app/create.rs @@ -440,6 +440,11 @@ impl CmdAppCreate { } } } + pb.set_style( + indicatif::ProgressStyle::with_template(&format!("{} {{msg}}", "✔".green().bold())) + .unwrap(), + ); + pb.finish_with_message(format!("{}", "Unpacked template".bold())); pb.finish(); diff --git a/lib/cli/src/commands/package/common/wait.rs b/lib/cli/src/commands/package/common/wait.rs index cf661a30c72..30bb410f310 100644 --- a/lib/cli/src/commands/package/common/wait.rs +++ b/lib/cli/src/commands/package/common/wait.rs @@ -1,7 +1,4 @@ -use super::macros::spinner_ok; -use colored::Colorize; use futures_util::StreamExt; -use indicatif::ProgressBar; use wasmer_api::WasmerClient; /// Different conditions that can be "awaited" when publishing a package. @@ -83,14 +80,12 @@ pub async fn wait_package( client: &WasmerClient, to_wait: PublishWait, package_version_id: wasmer_api::types::Id, - pb: &ProgressBar, timeout: humantime::Duration, ) -> anyhow::Result<()> { if let PublishWait::None = to_wait { return Ok(()); } - pb.set_message("Waiting for package to become available..."); let registry_url = client.graphql_endpoint().to_string(); let login_token = client.auth_token().unwrap_or_default().to_string(); let package_version_id = package_version_id.into_inner(); @@ -146,6 +141,5 @@ pub async fn wait_package( } } - spinner_ok!(pb, "Package is available in the registry"); Ok(()) } diff --git a/lib/cli/src/commands/package/push.rs b/lib/cli/src/commands/package/push.rs index 7fc583a3fb2..c7f2caf7ad3 100644 --- a/lib/cli/src/commands/package/push.rs +++ b/lib/cli/src/commands/package/push.rs @@ -118,10 +118,12 @@ impl PackagePush { package_hash: &PackageHash, private: bool, ) -> anyhow::Result<()> { - let pb = make_spinner!(self.quiet, "Uploading the package to the registry.."); + let pb = make_spinner!(self.quiet, "Uploading the package.."); let signed_url = upload(client, package_hash, self.timeout, package, pb.clone()).await?; + spinner_ok!(pb, "Package correctly uploaded"); + let pb = make_spinner!(self.quiet, "Waiting for package to become available..."); let id = match wasmer_api::query::push_package_release( client, None, @@ -133,10 +135,6 @@ impl PackagePush { { Some(r) => { if r.success { - let msg = format!( - "Succesfully pushed release to namespace {namespace} on the registry" - ); - spinner_ok!(pb, msg); r.package_webc.unwrap().id } else { anyhow::bail!("An unidentified error occurred while publishing the package. (response had success: false)") @@ -145,7 +143,10 @@ impl PackagePush { None => anyhow::bail!("An unidentified error occurred while publishing the package."), // <- This is extremely bad.. }; - wait_package(client, self.wait, id, &pb, self.timeout).await?; + wait_package(client, self.wait, id, self.timeout).await?; + let msg = format!("Succesfully pushed release to namespace {namespace} on the registry"); + spinner_ok!(pb, msg); + Ok(()) } From bfff8fa84e22de1eee8ca7213f8b8da6e1b8c876 Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Fri, 17 May 2024 10:46:40 +0200 Subject: [PATCH 08/14] feat(cli): Spurious UX fixes --- lib/cli/src/commands/app/deploy.rs | 2 +- lib/cli/src/commands/package/common/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cli/src/commands/app/deploy.rs b/lib/cli/src/commands/app/deploy.rs index b54f080a9c4..98b95f485ef 100644 --- a/lib/cli/src/commands/app/deploy.rs +++ b/lib/cli/src/commands/app/deploy.rs @@ -669,7 +669,7 @@ pub async fn wait_app( eprintln!(); } eprintln!( - "{} Deployment complete, new version reachable at {check_url}", + "{} Deployment complete", "𖥔".yellow().bold() ); break; diff --git a/lib/cli/src/commands/package/common/mod.rs b/lib/cli/src/commands/package/common/mod.rs index 612c1fe3b49..7b08047ac67 100644 --- a/lib/cli/src/commands/package/common/mod.rs +++ b/lib/cli/src/commands/package/common/mod.rs @@ -118,7 +118,7 @@ pub(super) async fn upload( let total_bytes = bytes.len(); pb.set_length(total_bytes.try_into().unwrap()); - pb.set_style(ProgressStyle::with_template("{spinner:.yellow} [{elapsed_precise}] [{bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})") + pb.set_style(ProgressStyle::with_template("{spinner:.yellow} [{elapsed_precise}] [{bar:.white}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})") .unwrap() .progress_chars("█▉▊▋▌▍▎▏ ") .tick_strings(&["✶", "✸", "✹", "✺", "✹", "✷"])); From 9068f18c1759a26ce1cf2d14c13afdb665311f52 Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Fri, 17 May 2024 11:30:46 +0200 Subject: [PATCH 09/14] fix(cli): Make linter happy --- lib/cli/src/commands/app/deploy.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/cli/src/commands/app/deploy.rs b/lib/cli/src/commands/app/deploy.rs index 98b95f485ef..7cf55daff71 100644 --- a/lib/cli/src/commands/app/deploy.rs +++ b/lib/cli/src/commands/app/deploy.rs @@ -668,10 +668,7 @@ pub async fn wait_app( if !quiet { eprintln!(); } - eprintln!( - "{} Deployment complete", - "𖥔".yellow().bold() - ); + eprintln!("{} Deployment complete", "𖥔".yellow().bold()); break; } From 459f7d0aa8d1be247e379982bfa954c618017221 Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Fri, 17 May 2024 11:33:02 +0200 Subject: [PATCH 10/14] feat(cli): Don't tag twice If a package with hash `x` was tagged with `namespace/name@version` and the user wants (or ends up trying) to tag the package with the same `namespace/name@version`, just conclude with a nop and don't send the tag to the backend --- lib/backend-api/src/types.rs | 10 ++++++-- lib/cli/src/commands/package/tag.rs | 30 ++++++++++++++++++++---- tests/wasmer-argus/src/argus/mod.rs | 4 ++-- tests/wasmer-argus/src/argus/packages.rs | 2 +- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/lib/backend-api/src/types.rs b/lib/backend-api/src/types.rs index 83af3ae8c1d..1f6156772d6 100644 --- a/lib/backend-api/src/types.rs +++ b/lib/backend-api/src/types.rs @@ -121,9 +121,15 @@ mod queries { pub version: String, pub created_at: DateTime, pub pirita_manifest: Option, - pub distribution: PackageDistribution, - pub package: Package, + + #[arguments(version: "V3")] + #[cynic(rename = "distribution")] + pub distribution_v3: PackageDistribution, + + #[arguments(version: "V2")] + #[cynic(rename = "distribution")] + pub distribution_v2: PackageDistribution, } #[derive(cynic::QueryVariables, Debug)] diff --git a/lib/cli/src/commands/package/tag.rs b/lib/cli/src/commands/package/tag.rs index d49c1c4e0ab..8e56af2644b 100644 --- a/lib/cli/src/commands/package/tag.rs +++ b/lib/cli/src/commands/package/tag.rs @@ -374,8 +374,6 @@ impl PackageTag { .interact()? { user_version = new_version.clone(); self.update_manifest_version(manifest_path, manifest, &user_version).await?; - } else { - eprintln!("{}: if version {user_version} of {full_pkg_name} already exists tagging will fail.", "WARN".bold().yellow()); } } } @@ -465,12 +463,34 @@ impl PackageTag { None => return Ok(PackageIdent::Hash(self.package_hash.clone())), }; - self.do_tag(client, &id, manifest, &package_id) - .await - .map_err(on_error)?; + if self.should_tag(client, &id).await? { + self.do_tag(client, &id, manifest, &package_id) + .await + .map_err(on_error)?; + } Ok(PackageIdent::Named(id.into())) } + + // Check if a package with the same hash, namespace, name and version already exists. In such a + // case, don't tag the package again. + async fn should_tag(&self, client: &WasmerClient, id: &NamedPackageId) -> anyhow::Result { + if let Some(pkg) = wasmer_api::query::get_package_version( + client, + id.full_name.clone(), + id.version.to_string(), + ) + .await? + { + if let Some(hash) = pkg.distribution_v3.pirita_sha256_hash { + let registry_package_hash = PackageHash::from_str(&format!("sha256:{hash}"))?; + if ®istry_package_hash == &self.package_hash { + return Ok(false); + } + } + } + Ok(true) + } } #[async_trait::async_trait] diff --git a/tests/wasmer-argus/src/argus/mod.rs b/tests/wasmer-argus/src/argus/mod.rs index 073dc460e65..648da747108 100644 --- a/tests/wasmer-argus/src/argus/mod.rs +++ b/tests/wasmer-argus/src/argus/mod.rs @@ -95,7 +95,7 @@ impl Argus { p.enable_steady_tick(Duration::from_millis(100)); let package_name = Argus::get_package_id(package); - let webc_url: Url = match &package.distribution.pirita_download_url { + let webc_url: Url = match &package.distribution_v2.pirita_download_url { Some(url) => url.parse().unwrap(), None => { info!("package {} has no download url, skipping", package_name); @@ -172,7 +172,7 @@ impl Argus { return true; } - if pkg.distribution.pirita_sha256_hash.is_none() { + if pkg.distribution_v2.pirita_sha256_hash.is_none() { info!("skipping test for {name} as it has no hash"); return false; } diff --git a/tests/wasmer-argus/src/argus/packages.rs b/tests/wasmer-argus/src/argus/packages.rs index 5768e7ac258..0fd0d48546e 100644 --- a/tests/wasmer-argus/src/argus/packages.rs +++ b/tests/wasmer-argus/src/argus/packages.rs @@ -188,7 +188,7 @@ impl Argus { /// Return the complete path to the folder of the test for the package, from the outdir to the /// hash pub async fn get_path(config: Arc, pkg: &PackageVersionWithPackage) -> PathBuf { - let hash = match &pkg.distribution.pirita_sha256_hash { + let hash = match &pkg.distribution_v2.pirita_sha256_hash { Some(hash) => hash, None => { unreachable!("no package without an hash should reach this function!") From 1362adebf74f40c1bc3c317166808b5150a9aba2 Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Fri, 17 May 2024 12:09:22 +0200 Subject: [PATCH 11/14] fix(cli): Make linter happy --- lib/cli/src/commands/package/tag.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cli/src/commands/package/tag.rs b/lib/cli/src/commands/package/tag.rs index 8e56af2644b..b74eb563aa5 100644 --- a/lib/cli/src/commands/package/tag.rs +++ b/lib/cli/src/commands/package/tag.rs @@ -484,7 +484,7 @@ impl PackageTag { { if let Some(hash) = pkg.distribution_v3.pirita_sha256_hash { let registry_package_hash = PackageHash::from_str(&format!("sha256:{hash}"))?; - if ®istry_package_hash == &self.package_hash { + if registry_package_hash == self.package_hash { return Ok(false); } } From ea7a19a738ee2462788e2fd25e555411f580cd48 Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Fri, 17 May 2024 12:10:41 +0200 Subject: [PATCH 12/14] Bump syn version in displaydoc --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 36fef7bf7da..d8469af6d49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1415,7 +1415,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.64", ] [[package]] From d06983803f55026a1daa0231da2c7319d438b5fc Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Fri, 17 May 2024 13:37:03 +0200 Subject: [PATCH 13/14] feat(CLI): fix `app create` in offline mode --- lib/cli/src/commands/app/create.rs | 100 +++++++++++++++++------------ lib/cli/src/utils/prompts.rs | 2 + 2 files changed, 61 insertions(+), 41 deletions(-) diff --git a/lib/cli/src/commands/app/create.rs b/lib/cli/src/commands/app/create.rs index 14daaa7b730..5a6668a4290 100644 --- a/lib/cli/src/commands/app/create.rs +++ b/lib/cli/src/commands/app/create.rs @@ -164,7 +164,7 @@ impl CmdAppCreate { ) } - async fn get_owner(&self, client: &WasmerClient) -> anyhow::Result { + async fn get_owner(&self, client: Option<&WasmerClient>) -> anyhow::Result { if let Some(owner) = &self.owner { return Ok(owner.clone()); } @@ -174,19 +174,12 @@ impl CmdAppCreate { anyhow::bail!("No owner specified: use --owner "); } - if !self.offline { - let user = wasmer_api::query::current_user_with_namespaces(client, None).await?; - crate::utils::prompts::prompt_for_namespace( - "Who should own this app?", - None, - Some(&user), - ) + let user = if let Some(client) = client { + Some(wasmer_api::query::current_user_with_namespaces(client, None).await?) } else { - anyhow::bail!( - "Please, user `wasmer login` before deploying an app or use the --owner - flag to specify the owner of the app to deploy." - ) - } + None + }; + crate::utils::prompts::prompt_for_namespace("Who should own this app?", None, user.as_ref()) } async fn create_from_local_manifest( @@ -237,7 +230,7 @@ impl CmdAppCreate { async fn create_from_package( &self, - client: &WasmerClient, + client: Option<&WasmerClient>, owner: &str, app_name: &str, ) -> anyhow::Result { @@ -254,8 +247,12 @@ impl CmdAppCreate { let (package_id, _) = crate::utils::prompts::prompt_for_package( "Enter the name of the package", Some("wasmer/hello"), - Some(PackageCheckMode::MustExist), - Some(client), + if client.is_some() { + Some(PackageCheckMode::MustExist) + } else { + None + }, + client, ) .await?; @@ -360,10 +357,15 @@ impl CmdAppCreate { async fn create_from_template( &self, - client: &WasmerClient, + client: Option<&WasmerClient>, owner: &str, app_name: &str, ) -> anyhow::Result { + let client = match client { + Some(client) => client, + None => anyhow::bail!("Cannot"), + }; + let url = self.get_template_url(client).await?; tracing::info!("Downloading template from url {url}"); @@ -531,41 +533,57 @@ impl AsyncCliCommand for CmdAppCreate { type Output = (); async fn run_async(self) -> Result { - let client = login_user( - &self.api, - &self.env, - !self.non_interactive, - "retrieve informations about the owner of the app", - ) - .await?; + let client = if self.offline { + None + } else { + Some( + login_user( + &self.api, + &self.env, + !self.non_interactive, + "retrieve informations about the owner of the app", + ) + .await?, + ) + }; // Get the future owner of the app. - let owner = self.get_owner(&client).await?; + let owner = self.get_owner(client.as_ref()).await?; // Get the name of the app. let app_name = self.get_app_name().await?; if !self.create_from_local_manifest(&owner, &app_name).await? { if self.template.is_some() { - self.create_from_template(&client, &owner, &app_name) + self.create_from_template(client.as_ref(), &owner, &app_name) .await?; } else if self.package.is_some() { - self.create_from_package(&client, &owner, &app_name).await?; + self.create_from_package(client.as_ref(), &owner, &app_name) + .await?; } else if !self.non_interactive { - let theme = ColorfulTheme::default(); - let choice = Select::with_theme(&theme) - .with_prompt("What would you like to deploy?") - .items(&["Start with a template", "Choose an existing package"]) - .default(0) - .interact()?; - match choice { - 0 => { - self.create_from_template(&client, &owner, &app_name) - .await? - } - 1 => self.create_from_package(&client, &owner, &app_name).await?, - x => panic!("unhandled selection {x}"), - }; + if self.offline { + eprintln!("Creating app from a package name running in offline mode"); + self.create_from_package(client.as_ref(), &owner, &app_name) + .await?; + } else { + let theme = ColorfulTheme::default(); + let choice = Select::with_theme(&theme) + .with_prompt("What would you like to deploy?") + .items(&["Start with a template", "Choose an existing package"]) + .default(0) + .interact()?; + match choice { + 0 => { + self.create_from_template(client.as_ref(), &owner, &app_name) + .await? + } + 1 => { + self.create_from_package(client.as_ref(), &owner, &app_name) + .await? + } + x => panic!("unhandled selection {x}"), + }; + } } else { eprintln!("Warning: the creation process did not produce any result."); } diff --git a/lib/cli/src/utils/prompts.rs b/lib/cli/src/utils/prompts.rs index 7c2b9edfe7c..c5ab5da3020 100644 --- a/lib/cli/src/utils/prompts.rs +++ b/lib/cli/src/utils/prompts.rs @@ -129,6 +129,8 @@ pub async fn prompt_for_package( } } } + } else { + break Ok((ident, None)); } } } From aaf33dc656baccbe457bdee7402b6efcf43977f3 Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Fri, 17 May 2024 15:43:32 +0200 Subject: [PATCH 14/14] fix(cli): add a newline after publishing --- lib/cli/src/commands/package/publish.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/cli/src/commands/package/publish.rs b/lib/cli/src/commands/package/publish.rs index 0a2a23867f0..798bd98fcaf 100644 --- a/lib/cli/src/commands/package/publish.rs +++ b/lib/cli/src/commands/package/publish.rs @@ -149,10 +149,13 @@ impl AsyncCliCommand for PackagePublish { match ident { PackageIdent::Named(ref n) => { let url = make_package_url(&client, n); - eprintln!("{} Package URL: {url}", "𖥔".yellow().bold()); + eprintln!("\n{} Package URL: {url}", "𖥔".yellow().bold()); } PackageIdent::Hash(ref h) => { - eprintln!("{} Succesfully published package ({h})", "✔".green().bold()); + eprintln!( + "\n{} Succesfully published package ({h})", + "✔".green().bold() + ); } }