From 8072c82911fdaf71397ed4deff715349414e8dae Mon Sep 17 00:00:00 2001 From: Vincent Zhang <118719397+vincent-dfinity@users.noreply.github.com> Date: Wed, 4 Sep 2024 11:22:16 +0800 Subject: [PATCH] feat: Add ability to list remote available extensions (#3896) * Split the extension list function into a separate file. * Add support for listing remote extensions. * Format files. * Add 'ListRemoteExtensionsError' error type. * Address the review comments, mainly keep the behavior consistent with other commands. * Add e2e tests for listing available extensions. * Address review comments. * Update changelog. --- CHANGELOG.md | 6 +++ e2e/tests-dfx/extension.bash | 25 +++++++++++ src/dfx-core/src/error/extension.rs | 6 +++ src/dfx-core/src/extension/manager/list.rs | 47 +++++++++++++++++++++ src/dfx-core/src/extension/manager/mod.rs | 33 ++------------- src/dfx/src/commands/extension/list.rs | 49 ++++++++++++++++++++-- src/dfx/src/commands/extension/mod.rs | 6 +-- 7 files changed, 135 insertions(+), 37 deletions(-) create mode 100644 src/dfx-core/src/extension/manager/list.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index aff917ef01..e34f1e8512 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,12 @@ This applies to the following: - tech stack value computation - packtool (vessel, mops etc) +### feat: `dfx extension list` supports listing available extensions + +`dfx extension list` now support `--available` flag to list available extensions from the +[extension catalog](https://github.com/dfinity/dfx-extensions/blob/main/catalog.json). +The extension catalog can be overridden with the `--catalog-url` parameter. + ## Dependencies ### Frontend canister diff --git a/e2e/tests-dfx/extension.bash b/e2e/tests-dfx/extension.bash index df575759a3..0b56c07240 100644 --- a/e2e/tests-dfx/extension.bash +++ b/e2e/tests-dfx/extension.bash @@ -767,3 +767,28 @@ EOF assert_command dfx extension run test_extension abc --the-another-param 464646 --the-param 123 456 789 assert_eq "abc --the-another-param 464646 --the-param 123 456 789 --dfx-cache-path $CACHE_DIR" } + +@test "list available extensions from official catalog" { + assert_command dfx extension list --available + assert_contains "sns" + assert_contains "nns" +} + +@test "list available extensions from customized catalog" { + start_webserver --directory www + CATALOG_URL_URL="http://localhost:$E2E_WEB_SERVER_PORT/arbitrary/catalog.json" + mkdir -p www/arbitrary + + cat > www/arbitrary/catalog.json <; + +impl ExtensionManager { + pub fn list_installed_extensions( + &self, + ) -> Result { + if !self.dir.exists() { + return Ok(vec![]); + } + let dir_content = crate::fs::read_dir(&self.dir)?; + + let extensions = dir_content + .filter_map(|v| { + let dir_entry = v.ok()?; + if dir_entry.file_type().map_or(false, |e| e.is_dir()) + && !dir_entry.file_name().to_str()?.starts_with(".tmp") + { + let name = dir_entry.file_name().to_string_lossy().to_string(); + Some(name) + } else { + None + } + }) + .collect(); + Ok(extensions) + } + + pub async fn list_available_extensions( + &self, + catalog_url: Option<&Url>, + ) -> Result { + let catalog = ExtensionCatalog::fetch(catalog_url) + .await + .map_err(ListAvailableExtensionsError::FetchCatalog)?; + let extensions: Vec = catalog.0.into_keys().collect(); + + Ok(extensions) + } +} diff --git a/src/dfx-core/src/extension/manager/mod.rs b/src/dfx-core/src/extension/manager/mod.rs index bf9a3b8ff4..ba491ac23f 100644 --- a/src/dfx-core/src/extension/manager/mod.rs +++ b/src/dfx-core/src/extension/manager/mod.rs @@ -1,12 +1,8 @@ use crate::config::cache::get_cache_path_for_version; use crate::error::extension::{ - GetExtensionBinaryError, ListInstalledExtensionsError, LoadExtensionManifestsError, - NewExtensionManagerError, -}; -use crate::extension::{ - installed::{InstalledExtensionList, InstalledExtensionManifests}, - manifest::ExtensionManifest, + GetExtensionBinaryError, LoadExtensionManifestsError, NewExtensionManagerError, }; +use crate::extension::{installed::InstalledExtensionManifests, manifest::ExtensionManifest}; pub use install::InstallOutcome; use semver::Version; use std::collections::HashMap; @@ -14,6 +10,7 @@ use std::path::PathBuf; mod execute; mod install; +mod list; mod uninstall; pub struct ExtensionManager { @@ -59,30 +56,6 @@ impl ExtensionManager { self.get_extension_directory(extension_name).exists() } - pub fn list_installed_extensions( - &self, - ) -> Result { - if !self.dir.exists() { - return Ok(vec![]); - } - let dir_content = crate::fs::read_dir(&self.dir)?; - - let extensions = dir_content - .filter_map(|v| { - let dir_entry = v.ok()?; - if dir_entry.file_type().map_or(false, |e| e.is_dir()) - && !dir_entry.file_name().to_str()?.starts_with(".tmp") - { - let name = dir_entry.file_name().to_string_lossy().to_string(); - Some(name) - } else { - None - } - }) - .collect(); - Ok(extensions) - } - pub fn load_installed_extension_manifests( &self, ) -> Result { diff --git a/src/dfx/src/commands/extension/list.rs b/src/dfx/src/commands/extension/list.rs index 5ebf5730a9..8ffa76127a 100644 --- a/src/dfx/src/commands/extension/list.rs +++ b/src/dfx/src/commands/extension/list.rs @@ -1,22 +1,63 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; +use clap::Parser; use std::io::Write; +use tokio::runtime::Runtime; +use url::Url; -pub fn exec(env: &dyn Environment) -> DfxResult<()> { +#[derive(Parser)] +pub struct ListOpts { + /// Specifies to list the available remote extensions. + #[arg(long)] + available: bool, + /// Specifies the URL of the catalog to use to find the extension. + #[clap(long)] + catalog_url: Option, +} + +pub fn exec(env: &dyn Environment, opts: ListOpts) -> DfxResult<()> { let mgr = env.get_extension_manager(); - let extensions = mgr.list_installed_extensions()?; + if opts.available || opts.catalog_url.is_some() { + let runtime = Runtime::new().expect("Unable to create a runtime"); + let extensions = runtime.block_on(async { + mgr.list_available_extensions(opts.catalog_url.as_ref()) + .await + })?; + + display_extension_list( + &extensions, + "No extensions available.", + "Available extensions:", + ) + } else { + let extensions = mgr.list_installed_extensions()?; + + display_extension_list( + &extensions, + "No extensions installed.", + "Installed extensions:", + ) + } +} + +fn display_extension_list( + extensions: &Vec, + empty_msg: &str, + header_msg: &str, +) -> DfxResult<()> { if extensions.is_empty() { - eprintln!("No extensions installed."); + eprintln!("{}", empty_msg); return Ok(()); } - eprintln!("Installed extensions:"); + eprintln!("{}", header_msg); for extension in extensions { eprint!(" "); std::io::stderr().flush()?; println!("{}", extension); std::io::stdout().flush()?; } + Ok(()) } diff --git a/src/dfx/src/commands/extension/mod.rs b/src/dfx/src/commands/extension/mod.rs index 1d5898217b..1ed77b534b 100644 --- a/src/dfx/src/commands/extension/mod.rs +++ b/src/dfx/src/commands/extension/mod.rs @@ -24,8 +24,8 @@ pub enum SubCommand { Uninstall(uninstall::UninstallOpts), /// Execute an extension. Run(run::RunOpts), - /// List installed extensions. - List, + /// List installed or available extensions. + List(list::ListOpts), } pub fn exec(env: &dyn Environment, opts: ExtensionOpts) -> DfxResult { @@ -33,6 +33,6 @@ pub fn exec(env: &dyn Environment, opts: ExtensionOpts) -> DfxResult { SubCommand::Install(v) => install::exec(env, v), SubCommand::Uninstall(v) => uninstall::exec(env, v), SubCommand::Run(v) => run::exec(env, v), - SubCommand::List => list::exec(env), + SubCommand::List(v) => list::exec(env, v), } }