From 2fbe56c83412db1d03136b51e8eec508772abcc9 Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Thu, 29 Aug 2024 17:15:12 +0800 Subject: [PATCH 1/8] Split the extension list function into a separate file. --- src/dfx-core/src/extension/manager/list.rs | 29 ++++++++++++++++++++++ src/dfx-core/src/extension/manager/mod.rs | 29 +++------------------- 2 files changed, 32 insertions(+), 26 deletions(-) create mode 100644 src/dfx-core/src/extension/manager/list.rs diff --git a/src/dfx-core/src/extension/manager/list.rs b/src/dfx-core/src/extension/manager/list.rs new file mode 100644 index 0000000000..d95a23e50f --- /dev/null +++ b/src/dfx-core/src/extension/manager/list.rs @@ -0,0 +1,29 @@ +use super::ExtensionManager; +use crate::error::extension::ListInstalledExtensionsError; +use crate::extension::installed::InstalledExtensionList; + +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) + } +} diff --git a/src/dfx-core/src/extension/manager/mod.rs b/src/dfx-core/src/extension/manager/mod.rs index bf9a3b8ff4..c5565390d3 100644 --- a/src/dfx-core/src/extension/manager/mod.rs +++ b/src/dfx-core/src/extension/manager/mod.rs @@ -1,10 +1,10 @@ use crate::config::cache::get_cache_path_for_version; use crate::error::extension::{ - GetExtensionBinaryError, ListInstalledExtensionsError, LoadExtensionManifestsError, + GetExtensionBinaryError, LoadExtensionManifestsError, NewExtensionManagerError, }; use crate::extension::{ - installed::{InstalledExtensionList, InstalledExtensionManifests}, + installed::InstalledExtensionManifests, manifest::ExtensionManifest, }; pub use install::InstallOutcome; @@ -14,6 +14,7 @@ use std::path::PathBuf; mod execute; mod install; +mod list; mod uninstall; pub struct ExtensionManager { @@ -59,30 +60,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 { From 262c1eb895d45c897020de6adedae9ff82abf48d Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Thu, 29 Aug 2024 20:11:14 +0800 Subject: [PATCH 2/8] Add support for listing remote extensions. --- src/dfx-core/src/extension/manager/list.rs | 19 ++++++++++ src/dfx/src/commands/extension/list.rs | 43 ++++++++++++++++++++-- src/dfx/src/commands/extension/mod.rs | 6 +-- 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/src/dfx-core/src/extension/manager/list.rs b/src/dfx-core/src/extension/manager/list.rs index d95a23e50f..f71e397d1d 100644 --- a/src/dfx-core/src/extension/manager/list.rs +++ b/src/dfx-core/src/extension/manager/list.rs @@ -1,7 +1,15 @@ +use std::vec; + use super::ExtensionManager; use crate::error::extension::ListInstalledExtensionsError; +use crate::extension::catalog::ExtensionCatalog; +use crate::extension::ExtensionName; use crate::extension::installed::InstalledExtensionList; +use url::Url; + +pub type RemoteExtensionList = Vec; + impl ExtensionManager { pub fn list_installed_extensions( &self, @@ -26,4 +34,15 @@ impl ExtensionManager { .collect(); Ok(extensions) } + + pub async fn list_remote_extensions( + &self, + catalog_url: Option<&Url> + ) -> Result + { + let _catalog = ExtensionCatalog::fetch(catalog_url).await.unwrap(); + let extensions : Vec = _catalog.0.into_keys().collect(); + + Ok(extensions) + } } diff --git a/src/dfx/src/commands/extension/list.rs b/src/dfx/src/commands/extension/list.rs index 5ebf5730a9..1c8c1cb713 100644 --- a/src/dfx/src/commands/extension/list.rs +++ b/src/dfx/src/commands/extension/list.rs @@ -1,22 +1,57 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; +use clap::Parser; use std::io::Write; +use tokio::runtime::Runtime; +use url::Url; + +#[derive(Parser)] +pub struct ListOpts { + /// Specifies to list the installed extensions. + #[arg(long, + conflicts_with("catalog_url"), + )] + installed: 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<()> { + // println!("{}", opts.installed); -pub fn exec(env: &dyn Environment) -> DfxResult<()> { let mgr = env.get_extension_manager(); - let extensions = mgr.list_installed_extensions()?; + let extensions; + let extension_msg_1; + let extension_msg_2; + + if opts.installed { + extensions = mgr.list_installed_extensions()?; + extension_msg_1 = "No extensions installed."; + extension_msg_2 = "Installed extensions:"; + } else { + let runtime = Runtime::new().expect("Unable to create a runtime"); + extensions = runtime.block_on(async { + mgr.list_remote_extensions(opts.catalog_url.as_ref()) + .await + })?; + + extension_msg_1 = "No remote extensions available."; + extension_msg_2 = "Remote extensions:"; + }; if extensions.is_empty() { - eprintln!("No extensions installed."); + eprintln!("{}", extension_msg_1); return Ok(()); } - eprintln!("Installed extensions:"); + eprintln!("{}", extension_msg_2); 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..507cdfd452 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 remote 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), } } From 0b5c2d86f1b6fa3c54c2b850c547afdf5497dfc0 Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Thu, 29 Aug 2024 20:25:40 +0800 Subject: [PATCH 3/8] Format files. --- src/dfx-core/src/extension/manager/list.rs | 11 +++++------ src/dfx-core/src/extension/manager/mod.rs | 8 ++------ src/dfx/src/commands/extension/list.rs | 12 +++--------- 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/dfx-core/src/extension/manager/list.rs b/src/dfx-core/src/extension/manager/list.rs index f71e397d1d..6c6c03a6d7 100644 --- a/src/dfx-core/src/extension/manager/list.rs +++ b/src/dfx-core/src/extension/manager/list.rs @@ -3,8 +3,8 @@ use std::vec; use super::ExtensionManager; use crate::error::extension::ListInstalledExtensionsError; use crate::extension::catalog::ExtensionCatalog; -use crate::extension::ExtensionName; use crate::extension::installed::InstalledExtensionList; +use crate::extension::ExtensionName; use url::Url; @@ -37,11 +37,10 @@ impl ExtensionManager { pub async fn list_remote_extensions( &self, - catalog_url: Option<&Url> - ) -> Result - { - let _catalog = ExtensionCatalog::fetch(catalog_url).await.unwrap(); - let extensions : Vec = _catalog.0.into_keys().collect(); + catalog_url: Option<&Url>, + ) -> Result { + let catalog = ExtensionCatalog::fetch(catalog_url).await.unwrap(); + 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 c5565390d3..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, LoadExtensionManifestsError, - NewExtensionManagerError, -}; -use crate::extension::{ - installed::InstalledExtensionManifests, - manifest::ExtensionManifest, + GetExtensionBinaryError, LoadExtensionManifestsError, NewExtensionManagerError, }; +use crate::extension::{installed::InstalledExtensionManifests, manifest::ExtensionManifest}; pub use install::InstallOutcome; use semver::Version; use std::collections::HashMap; diff --git a/src/dfx/src/commands/extension/list.rs b/src/dfx/src/commands/extension/list.rs index 1c8c1cb713..2837db935d 100644 --- a/src/dfx/src/commands/extension/list.rs +++ b/src/dfx/src/commands/extension/list.rs @@ -8,9 +8,7 @@ use url::Url; #[derive(Parser)] pub struct ListOpts { /// Specifies to list the installed extensions. - #[arg(long, - conflicts_with("catalog_url"), - )] + #[arg(long, conflicts_with("catalog_url"))] installed: bool, /// Specifies the URL of the catalog to use to find the extension. #[clap(long)] @@ -18,8 +16,6 @@ pub struct ListOpts { } pub fn exec(env: &dyn Environment, opts: ListOpts) -> DfxResult<()> { - // println!("{}", opts.installed); - let mgr = env.get_extension_manager(); let extensions; let extension_msg_1; @@ -31,10 +27,8 @@ pub fn exec(env: &dyn Environment, opts: ListOpts) -> DfxResult<()> { extension_msg_2 = "Installed extensions:"; } else { let runtime = Runtime::new().expect("Unable to create a runtime"); - extensions = runtime.block_on(async { - mgr.list_remote_extensions(opts.catalog_url.as_ref()) - .await - })?; + extensions = runtime + .block_on(async { mgr.list_remote_extensions(opts.catalog_url.as_ref()).await })?; extension_msg_1 = "No remote extensions available."; extension_msg_2 = "Remote extensions:"; From a3c2e30badba5525aff3b6435e8df24eb28a7c3f Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Thu, 29 Aug 2024 21:40:52 +0800 Subject: [PATCH 4/8] Add 'ListRemoteExtensionsError' error type. --- src/dfx-core/src/error/extension.rs | 6 ++++++ src/dfx-core/src/extension/manager/list.rs | 8 +++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/dfx-core/src/error/extension.rs b/src/dfx-core/src/error/extension.rs index 3e0e51b491..2df9ee0a4c 100644 --- a/src/dfx-core/src/error/extension.rs +++ b/src/dfx-core/src/error/extension.rs @@ -39,6 +39,12 @@ pub enum ListInstalledExtensionsError { ExtensionsDirectoryIsNotReadable(#[from] ReadDirError), } +#[derive(Error, Debug)] +pub enum ListRemoteExtensionsError { + #[error(transparent)] + FetchCatalog(#[from] FetchCatalogError), +} + #[derive(Error, Debug)] pub enum LoadExtensionManifestsError { #[error(transparent)] diff --git a/src/dfx-core/src/extension/manager/list.rs b/src/dfx-core/src/extension/manager/list.rs index 6c6c03a6d7..cd48335696 100644 --- a/src/dfx-core/src/extension/manager/list.rs +++ b/src/dfx-core/src/extension/manager/list.rs @@ -1,7 +1,7 @@ use std::vec; use super::ExtensionManager; -use crate::error::extension::ListInstalledExtensionsError; +use crate::error::extension::{ListInstalledExtensionsError, ListRemoteExtensionsError}; use crate::extension::catalog::ExtensionCatalog; use crate::extension::installed::InstalledExtensionList; use crate::extension::ExtensionName; @@ -38,8 +38,10 @@ impl ExtensionManager { pub async fn list_remote_extensions( &self, catalog_url: Option<&Url>, - ) -> Result { - let catalog = ExtensionCatalog::fetch(catalog_url).await.unwrap(); + ) -> Result { + let catalog = ExtensionCatalog::fetch(catalog_url) + .await + .map_err(ListRemoteExtensionsError::FetchCatalog)?; let extensions: Vec = catalog.0.into_keys().collect(); Ok(extensions) From 3791d56c3329f2fb92e9bb701cf296d0ac2cc9bb Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Fri, 30 Aug 2024 09:21:01 +0800 Subject: [PATCH 5/8] Address the review comments, mainly keep the behavior consistent with other commands. --- src/dfx-core/src/error/extension.rs | 8 ++-- src/dfx-core/src/extension/manager/list.rs | 14 +++---- src/dfx/src/commands/extension/list.rs | 48 ++++++++++++++-------- 3 files changed, 42 insertions(+), 28 deletions(-) diff --git a/src/dfx-core/src/error/extension.rs b/src/dfx-core/src/error/extension.rs index 2df9ee0a4c..c1cab152cc 100644 --- a/src/dfx-core/src/error/extension.rs +++ b/src/dfx-core/src/error/extension.rs @@ -34,15 +34,15 @@ pub enum ConvertExtensionSubcommandIntoClapCommandError { } #[derive(Error, Debug)] -pub enum ListInstalledExtensionsError { +pub enum ListAvailableExtensionsError { #[error(transparent)] - ExtensionsDirectoryIsNotReadable(#[from] ReadDirError), + FetchCatalog(#[from] FetchCatalogError), } #[derive(Error, Debug)] -pub enum ListRemoteExtensionsError { +pub enum ListInstalledExtensionsError { #[error(transparent)] - FetchCatalog(#[from] FetchCatalogError), + ExtensionsDirectoryIsNotReadable(#[from] ReadDirError), } #[derive(Error, Debug)] diff --git a/src/dfx-core/src/extension/manager/list.rs b/src/dfx-core/src/extension/manager/list.rs index cd48335696..0736fbe0d9 100644 --- a/src/dfx-core/src/extension/manager/list.rs +++ b/src/dfx-core/src/extension/manager/list.rs @@ -1,14 +1,12 @@ -use std::vec; - use super::ExtensionManager; -use crate::error::extension::{ListInstalledExtensionsError, ListRemoteExtensionsError}; +use crate::error::extension::{ListAvailableExtensionsError, ListInstalledExtensionsError}; use crate::extension::catalog::ExtensionCatalog; use crate::extension::installed::InstalledExtensionList; use crate::extension::ExtensionName; - +use std::vec; use url::Url; -pub type RemoteExtensionList = Vec; +pub type AvailableExtensionList = Vec; impl ExtensionManager { pub fn list_installed_extensions( @@ -35,13 +33,13 @@ impl ExtensionManager { Ok(extensions) } - pub async fn list_remote_extensions( + pub async fn list_available_extensions( &self, catalog_url: Option<&Url>, - ) -> Result { + ) -> Result { let catalog = ExtensionCatalog::fetch(catalog_url) .await - .map_err(ListRemoteExtensionsError::FetchCatalog)?; + .map_err(ListAvailableExtensionsError::FetchCatalog)?; let extensions: Vec = catalog.0.into_keys().collect(); Ok(extensions) diff --git a/src/dfx/src/commands/extension/list.rs b/src/dfx/src/commands/extension/list.rs index 2837db935d..15e2d658d7 100644 --- a/src/dfx/src/commands/extension/list.rs +++ b/src/dfx/src/commands/extension/list.rs @@ -7,9 +7,9 @@ use url::Url; #[derive(Parser)] pub struct ListOpts { - /// Specifies to list the installed extensions. - #[arg(long, conflicts_with("catalog_url"))] - installed: bool, + /// 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, @@ -18,28 +18,44 @@ pub struct ListOpts { pub fn exec(env: &dyn Environment, opts: ListOpts) -> DfxResult<()> { let mgr = env.get_extension_manager(); let extensions; - let extension_msg_1; - let extension_msg_2; - if opts.installed { - extensions = mgr.list_installed_extensions()?; - extension_msg_1 = "No extensions installed."; - extension_msg_2 = "Installed extensions:"; - } else { + let result; + if opts.available || opts.catalog_url.is_some() { let runtime = Runtime::new().expect("Unable to create a runtime"); - extensions = runtime - .block_on(async { mgr.list_remote_extensions(opts.catalog_url.as_ref()).await })?; + extensions = runtime.block_on(async { + mgr.list_available_extensions(opts.catalog_url.as_ref()) + .await + })?; - extension_msg_1 = "No remote extensions available."; - extension_msg_2 = "Remote extensions:"; + result = display_extension_list( + &extensions, + "No extensions available.", + "Available extensions:", + ); + } else { + extensions = mgr.list_installed_extensions()?; + + result = display_extension_list( + &extensions, + "No extensions installed.", + "Installed extensions:", + ); }; + result +} + +fn display_extension_list( + extensions: &Vec, + empty_msg: &str, + header_msg: &str, +) -> DfxResult<()> { if extensions.is_empty() { - eprintln!("{}", extension_msg_1); + eprintln!("{}", empty_msg); return Ok(()); } - eprintln!("{}", extension_msg_2); + eprintln!("{}", header_msg); for extension in extensions { eprint!(" "); std::io::stderr().flush()?; From b377290cb443b2af416667ba2a03ebe086e1d4d6 Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Fri, 30 Aug 2024 20:52:46 +0800 Subject: [PATCH 6/8] Add e2e tests for listing available extensions. --- e2e/tests-dfx/extension.bash | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/e2e/tests-dfx/extension.bash b/e2e/tests-dfx/extension.bash index 5920644657..bb601e04fc 100644 --- a/e2e/tests-dfx/extension.bash +++ b/e2e/tests-dfx/extension.bash @@ -725,3 +725,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 < Date: Sat, 31 Aug 2024 10:57:56 +0800 Subject: [PATCH 7/8] Address review comments. --- src/dfx/src/commands/extension/list.rs | 18 +++++++----------- src/dfx/src/commands/extension/mod.rs | 2 +- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/dfx/src/commands/extension/list.rs b/src/dfx/src/commands/extension/list.rs index 15e2d658d7..8ffa76127a 100644 --- a/src/dfx/src/commands/extension/list.rs +++ b/src/dfx/src/commands/extension/list.rs @@ -17,32 +17,28 @@ pub struct ListOpts { pub fn exec(env: &dyn Environment, opts: ListOpts) -> DfxResult<()> { let mgr = env.get_extension_manager(); - let extensions; - let result; if opts.available || opts.catalog_url.is_some() { let runtime = Runtime::new().expect("Unable to create a runtime"); - extensions = runtime.block_on(async { + let extensions = runtime.block_on(async { mgr.list_available_extensions(opts.catalog_url.as_ref()) .await })?; - result = display_extension_list( + display_extension_list( &extensions, "No extensions available.", "Available extensions:", - ); + ) } else { - extensions = mgr.list_installed_extensions()?; + let extensions = mgr.list_installed_extensions()?; - result = display_extension_list( + display_extension_list( &extensions, "No extensions installed.", "Installed extensions:", - ); - }; - - result + ) + } } fn display_extension_list( diff --git a/src/dfx/src/commands/extension/mod.rs b/src/dfx/src/commands/extension/mod.rs index 507cdfd452..1ed77b534b 100644 --- a/src/dfx/src/commands/extension/mod.rs +++ b/src/dfx/src/commands/extension/mod.rs @@ -24,7 +24,7 @@ pub enum SubCommand { Uninstall(uninstall::UninstallOpts), /// Execute an extension. Run(run::RunOpts), - /// List installed or remote extensions. + /// List installed or available extensions. List(list::ListOpts), } From 89afa0c6e1a09d6089aae344992ffd4e254ca789 Mon Sep 17 00:00:00 2001 From: Vincent Zhang Date: Tue, 3 Sep 2024 13:08:59 +0800 Subject: [PATCH 8/8] Update changelog. --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6498416b8a..7d244980b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,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. + # 0.23.0 ### feat: Add canister snapshots