Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add ability to list remote available extensions #3896

Merged
merged 11 commits into from
Sep 4, 2024
25 changes: 25 additions & 0 deletions e2e/tests-dfx/extension.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<EOF
{
"nns": "https://raw.githubusercontent.com/dfinity/dfx-extensions/main/extensions/nns/extension.json",
"sns": "https://raw.githubusercontent.com/dfinity/dfx-extensions/main/extensions/sns/extension.json",
"test": "https://raw.githubusercontent.com/dfinity/dfx-extensions/main/extensions/sns/extension.json"
}
EOF

assert_command dfx extension list --catalog-url="$CATALOG_URL_URL"
assert_contains "sns"
assert_contains "nns"
assert_contains "test"
}
6 changes: 6 additions & 0 deletions src/dfx-core/src/error/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ pub enum ConvertExtensionSubcommandIntoClapCommandError {
ConvertExtensionSubcommandIntoClapArgError(#[from] ConvertExtensionSubcommandIntoClapArgError),
}

#[derive(Error, Debug)]
pub enum ListAvailableExtensionsError {
#[error(transparent)]
FetchCatalog(#[from] FetchCatalogError),
}

#[derive(Error, Debug)]
pub enum ListInstalledExtensionsError {
#[error(transparent)]
Expand Down
47 changes: 47 additions & 0 deletions src/dfx-core/src/extension/manager/list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use super::ExtensionManager;
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 AvailableExtensionList = Vec<ExtensionName>;

impl ExtensionManager {
pub fn list_installed_extensions(
&self,
) -> Result<InstalledExtensionList, ListInstalledExtensionsError> {
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<AvailableExtensionList, ListAvailableExtensionsError> {
let catalog = ExtensionCatalog::fetch(catalog_url)
.await
.map_err(ListAvailableExtensionsError::FetchCatalog)?;
let extensions: Vec<String> = catalog.0.into_keys().collect();

Ok(extensions)
}
}
33 changes: 3 additions & 30 deletions src/dfx-core/src/extension/manager/mod.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
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;
use std::path::PathBuf;

mod execute;
mod install;
mod list;
mod uninstall;

pub struct ExtensionManager {
Expand Down Expand Up @@ -59,30 +56,6 @@ impl ExtensionManager {
self.get_extension_directory(extension_name).exists()
}

pub fn list_installed_extensions(
&self,
) -> Result<InstalledExtensionList, ListInstalledExtensionsError> {
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<InstalledExtensionManifests, LoadExtensionManifestsError> {
Expand Down
53 changes: 49 additions & 4 deletions src/dfx/src/commands/extension/list.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,67 @@
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<Url>,
}

pub fn exec(env: &dyn Environment, opts: ListOpts) -> DfxResult<()> {
let mgr = env.get_extension_manager();
let extensions = mgr.list_installed_extensions()?;
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 {
mgr.list_available_extensions(opts.catalog_url.as_ref())
.await
})?;

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
vincent-dfinity marked this conversation as resolved.
Show resolved Hide resolved
}

fn display_extension_list(
extensions: &Vec<String>,
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(())
}
6 changes: 3 additions & 3 deletions src/dfx/src/commands/extension/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ 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 {
match opts.subcmd {
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),
}
}
Loading