diff --git a/Cargo.lock b/Cargo.lock index 3a5a532a5a..cc16da607f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3638,6 +3638,7 @@ dependencies = [ name = "mise" version = "2025.5.12" dependencies = [ + "async-trait", "base64 0.22.1", "built", "bzip2", @@ -3653,7 +3654,6 @@ dependencies = [ "confique", "console", "contracts", - "crossbeam-channel", "ctor", "demand", "digest", @@ -3694,7 +3694,6 @@ dependencies = [ "petgraph", "pretty_assertions", "rand 0.9.1", - "rayon", "regex", "reqwest", "rmp-serde", @@ -4583,26 +4582,6 @@ dependencies = [ "getrandom 0.3.3", ] -[[package]] -name = "rayon" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - [[package]] name = "redox_syscall" version = "0.5.12" diff --git a/Cargo.toml b/Cargo.toml index 72e048b63a..0140d96f36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ lto = true opt-level = 3 [dependencies] +async-trait = "0.1" base64 = "0.22" bzip2 = "0.5" calm_io = "0.1" @@ -63,7 +64,6 @@ comfy-table = "7.1.3" confique = { version = "0.3", default-features = false } console = "0.15" contracts = "0.6" -crossbeam-channel = "0.5" demand = "1" digest = "0.10.7" dotenvy = "0.15" @@ -76,6 +76,8 @@ eyre = "0.6" filetime = "0.2" flate2 = "1" fslock = "0.2.1" +fuzzy-matcher = "0.3" +gix = { version = "<1", features = ["worktree-mutation"] } glob = "0.3" globset = "0.4" heck = "0.5" @@ -84,6 +86,7 @@ indexmap = { version = "2", features = ["serde"] } indicatif = { version = "0.17", features = ["default", "improved_unicode"] } indoc = "2" itertools = "0.14" +jiff = "0.2" junction = "1" log = "0.4" minisign-verify = "0.2" @@ -96,7 +99,6 @@ os-release = "0.1" path-absolutize = "3" petgraph = "0.8" rand = "0.9" -rayon = "1" regex = "1" reqwest = { version = "0.12", default-features = false, features = [ "json", @@ -134,11 +136,12 @@ tempfile = "3" tera = "1" terminal_size = "0.4" thiserror = "2" -tokio = { version = "1", features = ["io-std", "rt", "time"] } +tokio = { version = "1", features = ["full"] } toml = { version = "0.8", features = ["parse"] } toml_edit = { version = "0.22", features = ["parse"] } ubi = { version = "0.6.1", default-features = false } url = "2" +urlencoding = "2.1.3" usage-lib = { version = "2", features = ["clap", "docs"] } versions = { version = "6", features = ["serde"] } # vfox = { path = "../vfox.rs" } @@ -149,10 +152,6 @@ xx = { version = "2", features = ["glob"] } xz2 = "0.1" zip = { version = "3", default-features = false, features = ["deflate"] } zstd = "0.13" -gix = { version = "<1", features = ["worktree-mutation"] } -jiff = "0.2" -urlencoding = "2.1.3" -fuzzy-matcher = "0.3.7" [target.'cfg(unix)'.dependencies] exec = "0.3" diff --git a/e2e/cli/test_alias b/e2e/cli/test_alias index 4d05335ac6..dfd85fe994 100644 --- a/e2e/cli/test_alias +++ b/e2e/cli/test_alias @@ -1,8 +1,8 @@ #!/usr/bin/env bash -mise alias set tiny xxx 2 +assert "mise alias set tiny xxx 2" assert_contains "mise alias ls tiny" "tiny xxx 2" -mise alias unset tiny xxx +assert "mise alias unset tiny xxx" assert_not_contains "mise alias ls" "tiny xxx" assert "mise config set alias.nushell aqua:nushell/nushell" diff --git a/e2e/cli/test_local b/e2e/cli/test_local index 408171d9e4..5baa5c6caa 100644 --- a/e2e/cli/test_local +++ b/e2e/cli/test_local @@ -2,9 +2,9 @@ export MISE_USE_TOML=0 -mise i dummy@{1,2} tiny@3 +assert "mise i dummy@{1,2} tiny@3" -mise local dummy@2 +assert "mise local dummy@2 -vv" assert "mise local" "dummy 2" assert "mise local --path" "$PWD/.tool-versions" diff --git a/e2e/run_all_tests b/e2e/run_all_tests index 3212e5c78b..eed63b8058 100755 --- a/e2e/run_all_tests +++ b/e2e/run_all_tests @@ -52,9 +52,6 @@ for index in "${!FILES[@]}"; do test_count=$((test_count + 1)) done -if [[ -v GITHUB_STEP_SUMMARY ]] && [[ -f "$GITHUB_STEP_SUMMARY-extra" ]]; then - cat "$GITHUB_STEP_SUMMARY-extra" >>"$GITHUB_STEP_SUMMARY" -fi echo "E2E: ran $test_count tests, skipped $skipped_count tests" >&2 exit "$status" diff --git a/e2e/run_test b/e2e/run_test index 1e91dff46d..94f5d7ffff 100755 --- a/e2e/run_test +++ b/e2e/run_test @@ -55,7 +55,6 @@ within_isolated_env() { CARGO_LLVM_COV_SHOW_ENV="${CARGO_LLVM_COV_SHOW_ENV:-}" \ CARGO_LLVM_COV_TARGET_DIR="${CARGO_LLVM_COV_TARGET_DIR:-}" \ GITHUB_ACTION="${GITHUB_ACTION:-}" \ - GITHUB_STEP_SUMMARY="${GITHUB_STEP_SUMMARY:-}" \ GITHUB_TOKEN="${GITHUB_TOKEN:-}" \ GOPROXY="${GOPROXY:-}" \ HOME="$TEST_HOME" \ diff --git a/src/aqua/aqua_registry.rs b/src/aqua/aqua_registry.rs index fcc51386f7..85d75766c2 100644 --- a/src/aqua/aqua_registry.rs +++ b/src/aqua/aqua_registry.rs @@ -196,7 +196,7 @@ impl AquaRegistry { Ok(Self { path, repo_exists }) } - pub fn package(&self, id: &str) -> Result { + pub async fn package(&self, id: &str) -> Result { let path_id = id.split('/').join(std::path::MAIN_SEPARATOR_STR); let path = self.path.join("pkgs").join(&path_id).join("registry.yaml"); let registry: RegistryYaml = if !self.repo_exists { @@ -208,7 +208,7 @@ impl AquaRegistry { let url: Url = format!("https://mise-versions.jdx.dev/aqua-registry/{path_id}/registry.yaml") .parse()?; - http::HTTP_FETCH.download_file(url, &path, None)?; + http::HTTP_FETCH.download_file(url, &path, None).await?; serde_yaml::from_reader(file::open(&path)?)? } else { trace!("reading cached aqua-registry for {id} from {path:?}"); @@ -229,8 +229,8 @@ impl AquaRegistry { Ok(pkg) } - pub fn package_with_version(&self, id: &str, v: &str) -> Result { - Ok(self.package(id)?.with_version(v)) + pub async fn package_with_version(&self, id: &str, v: &str) -> Result { + Ok(self.package(id).await?.with_version(v)) } } diff --git a/src/backend/aqua.rs b/src/backend/aqua.rs index 7bd4beb7c8..87cd554b60 100644 --- a/src/backend/aqua.rs +++ b/src/backend/aqua.rs @@ -14,33 +14,36 @@ use crate::plugins::VERSION_REGEX; use crate::registry::REGISTRY; use crate::toolset::ToolVersion; use crate::{file, github, minisign}; +use async_trait::async_trait; use eyre::{ContextCompat, Result, bail}; use indexmap::IndexSet; use itertools::Itertools; use regex::Regex; -use std::collections::HashSet; use std::fmt::Debug; use std::path::{Path, PathBuf}; +use std::{collections::HashSet, sync::Arc}; #[derive(Debug)] pub struct AquaBackend { - ba: BackendArg, + ba: Arc, id: String, } +#[async_trait] impl Backend for AquaBackend { fn get_type(&self) -> BackendType { BackendType::Aqua } - fn description(&self) -> Option { + async fn description(&self) -> Option { AQUA_REGISTRY .package(&self.ba.tool_name) + .await .ok() .and_then(|p| p.description.clone()) } - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } @@ -48,10 +51,10 @@ impl Backend for AquaBackend { Ok(vec!["cosign", "slsa-verifier"]) } - fn _list_remote_versions(&self) -> Result> { - let pkg = AQUA_REGISTRY.package(&self.id)?; + async fn _list_remote_versions(&self) -> Result> { + let pkg = AQUA_REGISTRY.package(&self.id).await?; if !pkg.repo_owner.is_empty() && !pkg.repo_name.is_empty() { - let versions = get_versions(&pkg)?; + let versions = get_versions(&pkg).await?; Ok(versions .into_iter() .filter_map(|v| { @@ -82,14 +85,18 @@ impl Backend for AquaBackend { } } - fn install_version_(&self, ctx: &InstallContext, mut tv: ToolVersion) -> Result { + async fn install_version_( + &self, + ctx: &InstallContext, + mut tv: ToolVersion, + ) -> Result { let mut v = format!("v{}", tv.version); - let pkg = AQUA_REGISTRY.package_with_version(&self.id, &v)?; + let pkg = AQUA_REGISTRY.package_with_version(&self.id, &v).await?; if let Some(prefix) = &pkg.version_prefix { v = format!("{prefix}{v}"); } validate(&pkg)?; - let url = match self.fetch_url(&pkg, &v) { + let url = match self.fetch_url(&pkg, &v).await { Ok(url) => url, Err(err) => { if let Some(prefix) = &pkg.version_prefix { @@ -97,19 +104,23 @@ impl Backend for AquaBackend { } else { v = tv.version.to_string(); } - self.fetch_url(&pkg, &v).map_err(|e| err.wrap_err(e))? + self.fetch_url(&pkg, &v) + .await + .map_err(|e| err.wrap_err(e))? } }; let filename = url.split('/').next_back().unwrap(); - self.download(ctx, &tv, &url, filename)?; - self.verify(ctx, &mut tv, &pkg, &v, filename)?; + self.download(ctx, &tv, &url, filename).await?; + self.verify(ctx, &mut tv, &pkg, &v, filename).await?; self.install(ctx, &tv, &pkg, &v, filename)?; Ok(tv) } - fn list_bin_paths(&self, tv: &ToolVersion) -> Result> { - let pkg = AQUA_REGISTRY.package_with_version(&self.id, &tv.version)?; + async fn list_bin_paths(&self, tv: &ToolVersion) -> Result> { + let pkg = AQUA_REGISTRY + .package_with_version(&self.id, &tv.version) + .await?; let srcs = self.srcs(&pkg, tv)?; if srcs.is_empty() { @@ -163,38 +174,38 @@ impl AquaBackend { } Self { id: id.to_string(), - ba, + ba: Arc::new(ba), } } - fn fetch_url(&self, pkg: &AquaPackage, v: &str) -> Result { + async fn fetch_url(&self, pkg: &AquaPackage, v: &str) -> Result { match pkg.r#type { - AquaPackageType::GithubRelease => self.github_release_url(pkg, v), + AquaPackageType::GithubRelease => self.github_release_url(pkg, v).await, AquaPackageType::GithubArchive | AquaPackageType::GithubContent => { - self.github_archive_url(pkg, v) + self.github_archive_url(pkg, v).await } AquaPackageType::Http => { let url = pkg.url(v)?; - HTTP.head(&url)?; + HTTP.head(&url).await?; Ok(url) } ref t => bail!("unsupported aqua package type: {t}"), } } - fn github_release_url(&self, pkg: &AquaPackage, v: &str) -> Result { + async fn github_release_url(&self, pkg: &AquaPackage, v: &str) -> Result { let asset_strs = pkg.asset_strs(v)?; - self.github_release_asset(pkg, v, asset_strs) + self.github_release_asset(pkg, v, asset_strs).await } - fn github_release_asset( + async fn github_release_asset( &self, pkg: &AquaPackage, v: &str, asset_strs: IndexSet, ) -> Result { let gh_id = format!("{}/{}", pkg.repo_owner, pkg.repo_name); - let gh_release = github::get_release(&gh_id, v)?; + let gh_release = github::get_release(&gh_id, v).await?; let asset = gh_release .assets .iter() @@ -210,14 +221,14 @@ impl AquaBackend { Ok(asset.browser_download_url.to_string()) } - fn github_archive_url(&self, pkg: &AquaPackage, v: &str) -> Result { + async fn github_archive_url(&self, pkg: &AquaPackage, v: &str) -> Result { let gh_id = format!("{}/{}", pkg.repo_owner, pkg.repo_name); let url = format!("https://github.com/{gh_id}/archive/refs/tags/{v}.tar.gz"); - HTTP.head(&url)?; + HTTP.head(&url).await?; Ok(url) } - fn download( + async fn download( &self, ctx: &InstallContext, tv: &ToolVersion, @@ -229,11 +240,12 @@ impl AquaBackend { return Ok(()); } ctx.pr.set_message(format!("download {filename}")); - HTTP.download_file(url, &tarball_path, Some(&ctx.pr))?; + HTTP.download_file(url, &tarball_path, Some(&ctx.pr)) + .await?; Ok(()) } - fn verify( + async fn verify( &self, ctx: &InstallContext, tv: &mut ToolVersion, @@ -241,21 +253,23 @@ impl AquaBackend { v: &str, filename: &str, ) -> Result<()> { - self.verify_slsa(ctx, tv, pkg, v, filename)?; - self.verify_minisign(ctx, tv, pkg, v, filename)?; + self.verify_slsa(ctx, tv, pkg, v, filename).await?; + self.verify_minisign(ctx, tv, pkg, v, filename).await?; if !tv.checksums.contains_key(filename) { if let Some(checksum) = &pkg.checksum { if checksum.enabled() { let url = match checksum._type() { AquaChecksumType::GithubRelease => { let asset_strs = checksum.asset_strs(pkg, v)?; - self.github_release_asset(pkg, v, asset_strs)? + self.github_release_asset(pkg, v, asset_strs).await? } AquaChecksumType::Http => checksum.url(pkg, v)?, }; let checksum_path = tv.download_path().join(format!("{filename}.checksum")); - HTTP.download_file(&url, &checksum_path, Some(&ctx.pr))?; - self.cosign_checksums(ctx, pkg, v, tv, &checksum_path)?; + HTTP.download_file(&url, &checksum_path, Some(&ctx.pr)) + .await?; + self.cosign_checksums(ctx, pkg, v, tv, &checksum_path) + .await?; let mut checksum_file = file::read_to_string(&checksum_path)?; if checksum.file_format() == "regexp" { let pattern = checksum.pattern(); @@ -314,7 +328,7 @@ impl AquaBackend { Ok(()) } - fn verify_minisign( + async fn verify_minisign( &self, ctx: &InstallContext, tv: &mut ToolVersion, @@ -343,14 +357,15 @@ impl AquaBackend { .repo_name .clone() .unwrap_or_else(|| pkg.repo_name.clone()); - let url = github::get_release(&format!("{repo_owner}/{repo_name}"), v)? + let url = github::get_release(&format!("{repo_owner}/{repo_name}"), v) + .await? .assets .into_iter() .find(|a| a.name == asset) .map(|a| a.browser_download_url); if let Some(url) = url { let path = tv.download_path().join(asset); - HTTP.download_file(&url, &path, Some(&ctx.pr))?; + HTTP.download_file(&url, &path, Some(&ctx.pr)).await?; path } else { warn!("no asset found for minisign of {tv}: {asset}"); @@ -360,7 +375,7 @@ impl AquaBackend { AquaMinisignType::Http => { let url = minisign.url(pkg, v)?; let path = tv.download_path().join(filename).with_extension(".minisig"); - HTTP.download_file(&url, &path, Some(&ctx.pr))?; + HTTP.download_file(&url, &path, Some(&ctx.pr)).await?; path } }; @@ -371,7 +386,7 @@ impl AquaBackend { Ok(()) } - fn verify_slsa( + async fn verify_slsa( &self, ctx: &InstallContext, tv: &mut ToolVersion, @@ -387,7 +402,7 @@ impl AquaBackend { debug!("slsa is disabled for {tv}"); return Ok(()); } - if let Some(slsa_bin) = self.dependency_which("slsa-verifier") { + if let Some(slsa_bin) = self.dependency_which("slsa-verifier").await { ctx.pr.set_message("verify slsa".to_string()); let repo_owner = slsa .repo_owner @@ -401,14 +416,15 @@ impl AquaBackend { let provenance_path = match slsa.r#type.as_deref().unwrap_or_default() { "github_release" => { let asset = slsa.asset(pkg, v)?; - let url = github::get_release(&repo, v)? + let url = github::get_release(&repo, v) + .await? .assets .into_iter() .find(|a| a.name == asset) .map(|a| a.browser_download_url); if let Some(url) = url { let path = tv.download_path().join(asset); - HTTP.download_file(&url, &path, Some(&ctx.pr))?; + HTTP.download_file(&url, &path, Some(&ctx.pr)).await?; path.to_string_lossy().to_string() } else { warn!("no asset found for slsa verification of {tv}: {asset}"); @@ -418,7 +434,7 @@ impl AquaBackend { "http" => { let url = slsa.url(pkg, v)?; let path = tv.download_path().join(filename); - HTTP.download_file(&url, &path, Some(&ctx.pr))?; + HTTP.download_file(&url, &path, Some(&ctx.pr)).await?; path.to_string_lossy().to_string() } t => { @@ -452,7 +468,7 @@ impl AquaBackend { Ok(()) } - fn cosign_checksums( + async fn cosign_checksums( &self, ctx: &InstallContext, pkg: &AquaPackage, @@ -468,7 +484,7 @@ impl AquaBackend { debug!("cosign is disabled for {tv}"); return Ok(()); } - if let Some(cosign_bin) = self.dependency_which("cosign") { + if let Some(cosign_bin) = self.dependency_which("cosign").await { ctx.pr .set_message("verify checksums with cosign".to_string()); let mut cmd = CmdLineRunner::new(cosign_bin) @@ -619,17 +635,18 @@ impl AquaBackend { } } -fn get_versions(pkg: &AquaPackage) -> Result> { +async fn get_versions(pkg: &AquaPackage) -> Result> { if let Some("github_tag") = pkg.version_source.as_deref() { - let versions = github::list_tags(&format!("{}/{}", pkg.repo_owner, pkg.repo_name))?; + let versions = github::list_tags(&format!("{}/{}", pkg.repo_owner, pkg.repo_name)).await?; return Ok(versions); } - let mut versions = github::list_releases(&format!("{}/{}", pkg.repo_owner, pkg.repo_name))? + let mut versions = github::list_releases(&format!("{}/{}", pkg.repo_owner, pkg.repo_name)) + .await? .into_iter() .map(|r| r.tag_name) .collect_vec(); if versions.is_empty() { - versions = github::list_tags(&format!("{}/{}", pkg.repo_owner, pkg.repo_name))?; + versions = github::list_tags(&format!("{}/{}", pkg.repo_owner, pkg.repo_name)).await?; } Ok(versions) } diff --git a/src/backend/asdf.rs b/src/backend/asdf.rs index b42e9ecdd5..e7ab6b015d 100644 --- a/src/backend/asdf.rs +++ b/src/backend/asdf.rs @@ -1,10 +1,9 @@ -use std::collections::BTreeMap; use std::fmt::{Debug, Formatter}; use std::fs; use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; +use std::{collections::BTreeMap, sync::Arc}; -use crate::backend::Backend; use crate::backend::backend_type::BackendType; use crate::backend::external_plugin_cache::ExternalPluginCache; use crate::cache::{CacheManager, CacheManagerBuilder}; @@ -16,23 +15,25 @@ use crate::install_context::InstallContext; use crate::plugins::Script::{Download, ExecEnv, Install, ParseIdiomaticFile}; use crate::plugins::asdf_plugin::AsdfPlugin; use crate::plugins::mise_plugin_toml::MisePluginToml; -use crate::plugins::{Plugin, PluginType, Script, ScriptManager}; -use crate::timeout::run_with_timeout; +use crate::plugins::{PluginType, Script, ScriptManager}; use crate::toolset::{ToolRequest, ToolVersion, Toolset}; use crate::ui::progress_report::SingleReport; +use crate::{backend::Backend, plugins::PluginEnum, timeout}; use crate::{dirs, env, file}; +use async_trait::async_trait; use color_eyre::eyre::{Result, WrapErr, eyre}; use console::style; use heck::ToKebabCase; /// This represents a plugin installed to ~/.local/share/mise/plugins pub struct AsdfBackend { - pub ba: BackendArg, + pub ba: Arc, pub name: String, pub plugin_path: PathBuf, pub repo_url: Option, pub toml: MisePluginToml, - plugin: Box, + plugin: Arc, + plugin_enum: PluginEnum, cache: ExternalPluginCache, latest_stable_cache: CacheManager>, alias_cache: CacheManager>, @@ -49,6 +50,8 @@ impl AsdfBackend { toml_path = plugin_path.join("rtx.plugin.toml"); } let toml = MisePluginToml::from_file(&toml_path).unwrap(); + let plugin = Arc::new(plugin); + let plugin_enum = PluginEnum::Asdf(plugin.clone()); Self { cache: ExternalPluginCache::default(), latest_stable_cache: CacheManagerBuilder::new( @@ -69,16 +72,14 @@ impl AsdfBackend { .with_fresh_file(plugin_path.join("bin/list-legacy-filenames")) .build(), plugin_path, - plugin: Box::new(plugin), + plugin, + plugin_enum, repo_url: None, toml, name, - ba, + ba: Arc::new(ba), } } - pub fn plugin(&self) -> &dyn Plugin { - &*self.plugin - } fn fetch_cached_idiomatic_file(&self, idiomatic_file: &Path) -> Result> { let fp = self.idiomatic_cache_file_path(idiomatic_file); @@ -105,12 +106,12 @@ impl AsdfBackend { Ok(()) } - fn fetch_bin_paths(&self, tv: &ToolVersion) -> Result> { + async fn fetch_bin_paths(&self, tv: &ToolVersion) -> Result> { let list_bin_paths = self.plugin_path.join("bin/list-bin-paths"); let bin_paths = if matches!(tv.request, ToolRequest::System { .. }) { Vec::new() } else if list_bin_paths.exists() { - let sm = self.script_man_for_tv(tv)?; + let sm = self.script_man_for_tv(tv).await?; // TODO: find a way to enable this without deadlocking // for (t, tv) in ts.list_current_installed_versions(config) { // if t.name == self.name { @@ -136,9 +137,9 @@ impl AsdfBackend { }; Ok(bin_paths) } - fn fetch_exec_env(&self, ts: &Toolset, tv: &ToolVersion) -> Result { - let mut sm = self.script_man_for_tv(tv)?; - for p in ts.list_paths() { + async fn fetch_exec_env(&self, ts: &Toolset, tv: &ToolVersion) -> Result { + let mut sm = self.script_man_for_tv(tv).await?; + for p in ts.list_paths().await { sm.prepend_path(p); } let script = sm.get_script_path(&ExecEnv); @@ -156,8 +157,8 @@ impl AsdfBackend { Ok(env) } - fn script_man_for_tv(&self, tv: &ToolVersion) -> Result { - let config = Config::get(); + async fn script_man_for_tv(&self, tv: &ToolVersion) -> Result { + let config = Config::get().await; let mut sm = self.plugin.script_man.clone(); for (key, value) in tv.request.options().opts { let k = format!("RTX_TOOL_OPTS__{}", key.to_uppercase()); @@ -187,7 +188,7 @@ impl AsdfBackend { _ => &tv.version, }; // add env vars from mise.toml files - for (key, value) in config.env()? { + for (key, value) in config.env().await? { sm = sm.with_env(key, value.clone()); } let install = tv.install_path().to_string_lossy().to_string(); @@ -223,12 +224,13 @@ impl Hash for AsdfBackend { } } +#[async_trait] impl Backend for AsdfBackend { fn get_type(&self) -> BackendType { BackendType::Asdf } - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } @@ -236,15 +238,15 @@ impl Backend for AsdfBackend { Some(PluginType::Asdf) } - fn _list_remote_versions(&self) -> Result> { + async fn _list_remote_versions(&self) -> Result> { self.plugin.fetch_remote_versions() } - fn latest_stable_version(&self) -> Result> { - run_with_timeout( - || { + async fn latest_stable_version(&self) -> Result> { + timeout::run_with_timeout_async( + || async { if !self.plugin.has_latest_stable_script() { - return self.latest_version(Some("latest".into())); + return self.latest_version(Some("latest".into())).await; } self.latest_stable_cache .get_or_try_init(|| self.plugin.fetch_latest_stable()) @@ -258,6 +260,7 @@ impl Backend for AsdfBackend { }, SETTINGS.fetch_remote_versions_timeout(), ) + .await } fn get_aliases(&self) -> Result> { @@ -320,14 +323,14 @@ impl Backend for AsdfBackend { Ok(idiomatic_version) } - fn plugin(&self) -> Option<&dyn Plugin> { - Some(self.plugin()) + fn plugin(&self) -> Option<&PluginEnum> { + Some(&self.plugin_enum) } - fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { - let mut sm = self.script_man_for_tv(&tv)?; + async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { + let mut sm = self.script_man_for_tv(&tv).await?; - for p in ctx.ts.list_paths() { + for p in ctx.ts.list_paths().await { sm.prepend_path(p); } @@ -344,24 +347,35 @@ impl Backend for AsdfBackend { Ok(tv) } - fn uninstall_version_impl(&self, pr: &Box, tv: &ToolVersion) -> Result<()> { + async fn uninstall_version_impl( + &self, + pr: &Box, + tv: &ToolVersion, + ) -> Result<()> { if self.plugin_path.join("bin/uninstall").exists() { - self.script_man_for_tv(tv)? + self.script_man_for_tv(tv) + .await? .run_by_line(&Script::Uninstall, pr)?; } Ok(()) } - fn list_bin_paths(&self, tv: &ToolVersion) -> Result> { + async fn list_bin_paths(&self, tv: &ToolVersion) -> Result> { Ok(self .cache - .list_bin_paths(self, tv, || self.fetch_bin_paths(tv))? + .list_bin_paths(self, tv, async || self.fetch_bin_paths(tv).await) + .await? .into_iter() .map(|path| tv.install_path().join(path)) .collect()) } - fn exec_env(&self, config: &Config, ts: &Toolset, tv: &ToolVersion) -> eyre::Result { + async fn exec_env( + &self, + config: &Config, + ts: &Toolset, + tv: &ToolVersion, + ) -> eyre::Result { if matches!(tv.request, ToolRequest::System { .. }) { return Ok(BTreeMap::new()); } @@ -371,7 +385,8 @@ impl Backend for AsdfBackend { return Ok(BTreeMap::new()); } self.cache - .exec_env(config, self, tv, || self.fetch_exec_env(ts, tv)) + .exec_env(config, self, tv, async || self.fetch_exec_env(ts, tv).await) + .await } } diff --git a/src/backend/cargo.rs b/src/backend/cargo.rs index dd9cd2162f..9b503b8a82 100644 --- a/src/backend/cargo.rs +++ b/src/backend/cargo.rs @@ -1,10 +1,12 @@ -use std::fmt::Debug; +use std::{fmt::Debug, sync::Arc}; +use async_trait::async_trait; use color_eyre::Section; use eyre::{bail, eyre}; use serde_json::Deserializer; use url::Url; +use crate::Result; use crate::backend::Backend; use crate::backend::backend_type::BackendType; use crate::cli::args::BackendArg; @@ -18,15 +20,16 @@ use crate::{env, file}; #[derive(Debug)] pub struct CargoBackend { - ba: BackendArg, + ba: Arc, } +#[async_trait] impl Backend for CargoBackend { fn get_type(&self) -> BackendType { BackendType::Cargo } - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } @@ -38,12 +41,14 @@ impl Backend for CargoBackend { Ok(vec!["cargo-binstall", "sccache"]) } - fn _list_remote_versions(&self) -> eyre::Result> { + async fn _list_remote_versions(&self) -> eyre::Result> { if self.git_url().is_some() { // TODO: maybe fetch tags/branches from git? return Ok(vec!["HEAD".into()]); } - let raw = HTTP_FETCH.get_text(get_crate_url(&self.tool_name())?)?; + let raw = HTTP_FETCH + .get_text(get_crate_url(&self.tool_name())?) + .await?; let stream = Deserializer::from_str(&raw).into_iter::(); let mut versions = vec![]; for v in stream { @@ -55,8 +60,8 @@ impl Backend for CargoBackend { Ok(versions) } - fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> eyre::Result { - let config = Config::try_get()?; + async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { + let config = Config::get().await; let install_arg = format!("{}@{}", self.tool_name(), tv.version); let registry_name = &SETTINGS.cargo.registry_name; @@ -77,7 +82,7 @@ impl Backend for CargoBackend { ))?; } cmd - } else if self.is_binstall_enabled(&tv) { + } else if self.is_binstall_enabled(&tv).await { let mut cmd = CmdLineRunner::new("cargo-binstall").arg("-y"); if let Some(token) = &*GITHUB_TOKEN { cmd = cmd.env("GITHUB_TOKEN", token) @@ -117,30 +122,35 @@ impl Backend for CargoBackend { cmd.arg("--root") .arg(tv.install_path()) .with_pr(&ctx.pr) - .envs(ctx.ts.env_with_path(&config)?) - .prepend_path(ctx.ts.list_paths())? - .prepend_path(self.dependency_toolset()?.list_paths())? + .envs(ctx.ts.env_with_path(&config).await?) + .prepend_path(ctx.ts.list_paths().await)? + .prepend_path(self.dependency_toolset().await?.list_paths().await)? .execute()?; - Ok(tv) + Ok(tv.clone()) } } impl CargoBackend { pub fn from_arg(ba: BackendArg) -> Self { - Self { ba } + Self { ba: Arc::new(ba) } } - fn is_binstall_enabled(&self, tv: &ToolVersion) -> bool { + async fn is_binstall_enabled(&self, tv: &ToolVersion) -> bool { if !SETTINGS.cargo.binstall { return false; } - if file::which_non_pristine("cargo-binstall").is_none() - && !self - .dependency_toolset() - .is_ok_and(|ts| ts.which("cargo-binstall").is_some()) - { - return false; + if file::which_non_pristine("cargo-binstall").is_none() { + match self.dependency_toolset().await { + Ok(ts) => { + if ts.which("cargo-binstall").await.is_none() { + return false; + } + } + Err(_e) => { + return false; + } + } } let opts = tv.request.options(); if opts.contains_key("features") || opts.contains_key("default-features") { diff --git a/src/backend/dotnet.rs b/src/backend/dotnet.rs index dfd00cf8c6..23660b03eb 100644 --- a/src/backend/dotnet.rs +++ b/src/backend/dotnet.rs @@ -1,22 +1,26 @@ +use std::sync::Arc; + use crate::backend::Backend; use crate::backend::backend_type::BackendType; use crate::cli::args::BackendArg; use crate::cmd::CmdLineRunner; use crate::config::SETTINGS; use crate::http::HTTP_FETCH; +use async_trait::async_trait; use eyre::eyre; #[derive(Debug)] pub struct DotnetBackend { - ba: BackendArg, + ba: Arc, } +#[async_trait] impl Backend for DotnetBackend { fn get_type(&self) -> BackendType { BackendType::Dotnet } - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } @@ -24,18 +28,20 @@ impl Backend for DotnetBackend { Ok(vec!["dotnet"]) } - fn _list_remote_versions(&self) -> eyre::Result> { - let feed_url = self.get_search_url()?; - - let feed: NugetFeedSearch = HTTP_FETCH.json(format!( - "{}?q={}&packageType=dotnettool&take=1&prerelease={}", - feed_url, - &self.tool_name(), - SETTINGS - .dotnet - .package_flags - .contains(&"prerelease".to_string()) - ))?; + async fn _list_remote_versions(&self) -> eyre::Result> { + let feed_url = self.get_search_url().await?; + + let feed: NugetFeedSearch = HTTP_FETCH + .json(format!( + "{}?q={}&packageType=dotnettool&take=1&prerelease={}", + feed_url, + &self.tool_name(), + SETTINGS + .dotnet + .package_flags + .contains(&"prerelease".to_string()) + )) + .await?; if feed.total_hits == 0 { return Err(eyre!("No tool found")); @@ -51,7 +57,7 @@ impl Backend for DotnetBackend { Ok(data.versions.iter().map(|x| x.version.clone()).collect()) } - fn install_version_( + async fn install_version_( &self, ctx: &crate::install_context::InstallContext, tv: crate::toolset::ToolVersion, @@ -70,7 +76,7 @@ impl Backend for DotnetBackend { } cli.with_pr(&ctx.pr) - .envs(self.dependency_env()?) + .envs(self.dependency_env().await?) .execute()?; Ok(tv) @@ -79,13 +85,13 @@ impl Backend for DotnetBackend { impl DotnetBackend { pub fn from_arg(ba: BackendArg) -> Self { - Self { ba } + Self { ba: Arc::new(ba) } } - fn get_search_url(&self) -> eyre::Result { + async fn get_search_url(&self) -> eyre::Result { let nuget_registry = SETTINGS.dotnet.registry_url.as_str(); - let services: NugetFeed = HTTP_FETCH.json(nuget_registry)?; + let services: NugetFeed = HTTP_FETCH.json(nuget_registry).await?; let feed = services .resources diff --git a/src/backend/external_plugin_cache.rs b/src/backend/external_plugin_cache.rs index 2ab29bc85a..72c4e9afd1 100644 --- a/src/backend/external_plugin_cache.rs +++ b/src/backend/external_plugin_cache.rs @@ -9,7 +9,7 @@ use crate::tera::{BASE_CONTEXT, get_tera}; use crate::toolset::{ToolRequest, ToolVersion}; use eyre::{WrapErr, eyre}; use std::collections::HashMap; -use std::sync::RwLock; +use tokio::sync::RwLock; #[derive(Debug, Default)] pub struct ExternalPluginCache { @@ -18,20 +18,21 @@ pub struct ExternalPluginCache { } impl ExternalPluginCache { - pub fn list_bin_paths( + pub async fn list_bin_paths( &self, plugin: &AsdfBackend, tv: &ToolVersion, fetch: F, ) -> eyre::Result> where - F: FnOnce() -> eyre::Result>, + Fut: Future>>, + F: FnOnce() -> Fut, { - let mut w = self.list_bin_paths.write().unwrap(); + let config = Config::get().await; + let mut w = self.list_bin_paths.write().await; let cm = w.entry(tv.request.clone()).or_insert_with(|| { let list_bin_paths_filename = match &plugin.toml.list_bin_paths.cache_key { Some(key) => { - let config = Config::get(); let key = render_cache_key(&config, tv, key); let filename = format!("{key}.msgpack.z"); tv.cache_path().join("list_bin_paths").join(filename) @@ -43,10 +44,10 @@ impl ExternalPluginCache { .with_fresh_file(tv.install_path()) .build() }); - cm.get_or_try_init(fetch).cloned() + cm.get_or_try_init_async(fetch).await.cloned() } - pub fn exec_env( + pub async fn exec_env( &self, config: &Config, plugin: &AsdfBackend, @@ -54,9 +55,10 @@ impl ExternalPluginCache { fetch: F, ) -> eyre::Result where - F: FnOnce() -> eyre::Result, + Fut: Future>, + F: FnOnce() -> Fut, { - let mut w = self.exec_env.write().unwrap(); + let mut w = self.exec_env.write().await; let cm = w.entry(tv.request.clone()).or_insert_with(|| { let exec_env_filename = match &plugin.toml.exec_env.cache_key { Some(key) => { @@ -72,7 +74,7 @@ impl ExternalPluginCache { .with_fresh_file(tv.install_path()) .build() }); - cm.get_or_try_init(fetch).cloned() + cm.get_or_try_init_async(fetch).await.cloned() } } diff --git a/src/backend/gem.rs b/src/backend/gem.rs index 9f89512d56..5f8668f160 100644 --- a/src/backend/gem.rs +++ b/src/backend/gem.rs @@ -1,3 +1,4 @@ +use crate::Result; use crate::backend::Backend; use crate::backend::backend_type::BackendType; use crate::cli::args::BackendArg; @@ -7,21 +8,23 @@ use crate::file; use crate::http::HTTP_FETCH; use crate::install_context::InstallContext; use crate::toolset::ToolVersion; +use async_trait::async_trait; use indoc::formatdoc; -use std::fmt::Debug; +use std::{fmt::Debug, sync::Arc}; use url::Url; #[derive(Debug)] pub struct GemBackend { - ba: BackendArg, + ba: Arc, } +#[async_trait] impl Backend for GemBackend { fn get_type(&self) -> BackendType { BackendType::Gem } - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } @@ -29,10 +32,10 @@ impl Backend for GemBackend { Ok(vec!["ruby"]) } - fn _list_remote_versions(&self) -> eyre::Result> { + async fn _list_remote_versions(&self) -> eyre::Result> { // The `gem list` command does not supporting listing versions as json output // so we use the rubygems.org api to get the list of versions. - let raw = HTTP_FETCH.get_text(get_gem_url(&self.tool_name())?)?; + let raw = HTTP_FETCH.get_text(get_gem_url(&self.tool_name())?).await?; let gem_versions: Vec = serde_json::from_str(&raw)?; let mut versions: Vec = vec![]; for version in gem_versions.iter().rev() { @@ -41,7 +44,7 @@ impl Backend for GemBackend { Ok(versions) } - fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> eyre::Result { + async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { SETTINGS.ensure_experimental("gem backend")?; CmdLineRunner::new("gem") @@ -58,7 +61,7 @@ impl Backend for GemBackend { // gem. We should find a way to fix this. // .arg("--env-shebang") .with_pr(&ctx.pr) - .envs(self.dependency_env()?) + .envs(self.dependency_env().await?) .execute()?; // We install the gem to {install_path}/libexec and create a wrapper script for each executable @@ -71,7 +74,7 @@ impl Backend for GemBackend { impl GemBackend { pub fn from_arg(ba: BackendArg) -> Self { - Self { ba } + Self { ba: Arc::new(ba) } } } diff --git a/src/backend/go.rs b/src/backend/go.rs index 95a91b2d33..0d77cbabea 100644 --- a/src/backend/go.rs +++ b/src/backend/go.rs @@ -6,20 +6,22 @@ use crate::config::SETTINGS; use crate::install_context::InstallContext; use crate::timeout; use crate::toolset::ToolVersion; -use std::fmt::Debug; +use async_trait::async_trait; +use std::{fmt::Debug, sync::Arc}; use xx::regex; #[derive(Debug)] pub struct GoBackend { - ba: BackendArg, + ba: Arc, } +#[async_trait] impl Backend for GoBackend { fn get_type(&self) -> BackendType { BackendType::Go } - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } @@ -27,14 +29,14 @@ impl Backend for GoBackend { Ok(vec!["go"]) } - fn _list_remote_versions(&self) -> eyre::Result> { - timeout::run_with_timeout( - || { + async fn _list_remote_versions(&self) -> eyre::Result> { + timeout::run_with_timeout_async( + async || { let mut mod_path = Some(self.tool_name()); while let Some(cur_mod_path) = mod_path { let res = cmd!("go", "list", "-m", "-versions", "-json", &cur_mod_path) - .full_env(self.dependency_env()?) + .full_env(self.dependency_env().await?) .read(); if let Ok(raw) = res { let res = serde_json::from_str::(&raw); @@ -56,13 +58,18 @@ impl Backend for GoBackend { }, SETTINGS.fetch_remote_versions_timeout(), ) + .await } - fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> eyre::Result { + async fn install_version_( + &self, + ctx: &InstallContext, + tv: ToolVersion, + ) -> eyre::Result { SETTINGS.ensure_experimental("go backend")?; let opts = self.ba.opts(); - let install = |v| { + let install = async |v| { let mut cmd = CmdLineRunner::new("go").arg("install"); if let Some(tags) = opts.get("tags") { @@ -71,7 +78,7 @@ impl Backend for GoBackend { cmd.arg(format!("{}@{v}", self.tool_name())) .with_pr(&ctx.pr) - .envs(self.dependency_env()?) + .envs(self.dependency_env().await?) .env("GOBIN", tv.install_path().join("bin")) .execute() }; @@ -80,14 +87,14 @@ impl Backend for GoBackend { let use_v = regex!(r"^\d+\.\d+\.\d+").is_match(&tv.version); if use_v { - if install(format!("v{}", tv.version)).is_err() { + if install(format!("v{}", tv.version)).await.is_err() { warn!("Failed to install, trying again without added 'v' prefix"); } else { return Ok(tv); } } - install(tv.version.clone())?; + install(tv.version.clone()).await?; Ok(tv) } @@ -95,7 +102,7 @@ impl Backend for GoBackend { impl GoBackend { pub fn from_arg(ba: BackendArg) -> Self { - Self { ba } + Self { ba: Arc::new(ba) } } } diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 491bbc99d6..a38dc37daf 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -6,21 +6,26 @@ use std::hash::Hash; use std::ops::Deref; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; +use tokio::sync::Mutex as TokioMutex; -use crate::cache::{CacheManager, CacheManagerBuilder}; use crate::cli::args::{BackendArg, ToolVersionType}; use crate::cmd::CmdLineRunner; use crate::config::{Config, SETTINGS}; use crate::file::{display_path, remove_all, remove_all_with_warning}; use crate::install_context::InstallContext; use crate::plugins::core::CORE_PLUGINS; -use crate::plugins::{Plugin, PluginType, VERSION_REGEX}; +use crate::plugins::{PluginType, VERSION_REGEX}; use crate::registry::{REGISTRY, tool_enabled}; use crate::runtime_symlinks::is_runtime_symlink; use crate::toolset::outdated_info::OutdatedInfo; use crate::toolset::{ToolRequest, ToolVersion, Toolset, install_state, is_outdated_version}; use crate::ui::progress_report::SingleReport; +use crate::{ + cache::{CacheManager, CacheManagerBuilder}, + plugins::PluginEnum, +}; use crate::{dirs, env, file, hash, lock_file, plugins, versions_host}; +use async_trait::async_trait; use backend_type::BackendType; use console::style; use eyre::{Result, WrapErr, bail, eyre}; @@ -151,6 +156,7 @@ pub fn arg_to_backend(ba: BackendArg) -> Option { } } +#[async_trait] pub trait Backend: Debug + Send + Sync { fn id(&self) -> &str { &self.ba().short @@ -161,8 +167,8 @@ pub trait Backend: Debug + Send + Sync { fn get_type(&self) -> BackendType { BackendType::Core } - fn ba(&self) -> &BackendArg; - fn description(&self) -> Option { + fn ba(&self) -> &Arc; + async fn description(&self) -> Option { None } fn get_plugin_type(&self) -> Option { @@ -193,7 +199,7 @@ pub trait Backend: Debug + Send + Sync { // add dependencies from registry.toml deps.extend(rt.depends.iter().map(BackendArg::from)); } - deps.retain(|ba| self.ba() != ba); + deps.retain(|ba| &**self.ba() != ba); deps.retain(|ba| !all_fulls.contains(&ba.full())); for ba in deps.clone() { if let Ok(backend) = ba.backend() { @@ -203,50 +209,53 @@ pub trait Backend: Debug + Send + Sync { Ok(deps) } - fn list_remote_versions(&self) -> eyre::Result> { - let fetch = || { - trace!("Listing remote versions for {}", self.ba().to_string()); - match versions_host::list_versions(self.ba()) { - Ok(Some(versions)) => return Ok(versions), - Ok(None) => {} - Err(e) => { - debug!("Error getting versions from versions host: {:#}", e); - } - }; - trace!( - "Calling backend to list remote versions for {}", - self.ba().to_string() - ); - let versions = self - ._list_remote_versions()? - .into_iter() - .filter(|v| match v.parse::() { - Ok(ToolVersionType::Version(_)) => true, - _ => { - warn!("Invalid version: {}@{v}", self.id()); - false + async fn list_remote_versions(&self) -> eyre::Result> { + let remote_versions = self.get_remote_version_cache(); + let remote_versions = remote_versions.lock().await; + let ba = self.ba().clone(); + let id = self.id(); + let versions = remote_versions + .get_or_try_init_async(|| async { + trace!("Listing remote versions for {}", ba.to_string()); + match versions_host::list_versions(&ba).await { + Ok(Some(versions)) => return Ok(versions), + Ok(None) => {} + Err(e) => { + debug!("Error getting versions from versions host: {:#}", e); } - }) - .collect_vec(); - if versions.is_empty() { - warn!("No versions found for {}", self.id()); - } - Ok(versions) - }; - if let Ok(cache) = self.get_remote_version_cache().try_lock() { - cache.get_or_try_init(fetch).cloned() - } else { - fetch() - } + }; + trace!( + "Calling backend to list remote versions for {}", + ba.to_string() + ); + let versions = self + ._list_remote_versions() + .await? + .into_iter() + .filter(|v| match v.parse::() { + Ok(ToolVersionType::Version(_)) => true, + _ => { + warn!("Invalid version: {id}@{v}"); + false + } + }) + .collect_vec(); + if versions.is_empty() { + warn!("No versions found for {id}"); + } + Ok(versions) + }) + .await?; + Ok(versions.clone()) } - fn _list_remote_versions(&self) -> eyre::Result>; - fn latest_stable_version(&self) -> eyre::Result> { - self.latest_version(Some("latest".into())) + async fn _list_remote_versions(&self) -> eyre::Result>; + async fn latest_stable_version(&self) -> eyre::Result> { + self.latest_version(Some("latest".into())).await } fn list_installed_versions(&self) -> eyre::Result> { install_state::list_versions(&self.ba().short) } - fn is_version_installed(&self, tv: &ToolVersion, check_symlink: bool) -> bool { + fn is_version_installed(&self, config: &Config, tv: &ToolVersion, check_symlink: bool) -> bool { let check_path = |install_path: &Path, check_symlink: bool| { let is_installed = install_path.exists(); let is_not_incomplete = !self.incomplete_file_path(tv).exists(); @@ -275,7 +284,7 @@ pub trait Backend: Debug + Send + Sync { match tv.request { ToolRequest::System { .. } => true, _ => { - if let Some(install_path) = tv.request.install_path() { + if let Some(install_path) = tv.request.install_path(config) { if check_path(&install_path, true) { return true; } @@ -284,8 +293,9 @@ pub trait Backend: Debug + Send + Sync { } } } - fn is_version_outdated(&self, tv: &ToolVersion) -> bool { - let latest = match tv.latest_version() { + async fn is_version_outdated(&self, tv: &ToolVersion) -> bool { + let config = Config::get().await; + let latest = match tv.latest_version(&config).await { Ok(latest) => latest, Err(e) => { warn!( @@ -296,7 +306,7 @@ pub trait Backend: Debug + Send + Sync { return false; } }; - !self.is_version_installed(tv, true) || is_outdated_version(&tv.version, &latest) + !self.is_version_installed(&config, tv, true) || is_outdated_version(&tv.version, &latest) } fn symlink_path(&self, tv: &ToolVersion) -> Option { match tv.install_path() { @@ -317,20 +327,20 @@ pub trait Backend: Debug + Send + Sync { let versions = self.list_installed_versions()?; self.fuzzy_match_filter(versions, query) } - fn list_versions_matching(&self, query: &str) -> eyre::Result> { - let versions = self.list_remote_versions()?; + async fn list_versions_matching(&self, query: &str) -> eyre::Result> { + let versions = self.list_remote_versions().await?; self.fuzzy_match_filter(versions, query) } - fn latest_version(&self, query: Option) -> eyre::Result> { + async fn latest_version(&self, query: Option) -> eyre::Result> { match query { Some(query) => { - let mut matches = self.list_versions_matching(&query)?; + let mut matches = self.list_versions_matching(&query).await?; if matches.is_empty() && query == "latest" { - matches = self.list_remote_versions()?; + matches = self.list_remote_versions().await?; } Ok(find_match_in_list(&matches, &query)) } - None => self.latest_stable_version(), + None => self.latest_stable_version().await, } } fn latest_installed_version(&self, query: Option) -> eyre::Result> { @@ -359,18 +369,18 @@ pub trait Backend: Debug + Send + Sync { } } - fn warn_if_dependencies_missing(&self) -> eyre::Result<()> { + async fn warn_if_dependencies_missing(&self) -> eyre::Result<()> { let deps = self .get_all_dependencies(false)? .into_iter() - .filter(|ba| self.ba() != ba) + .filter(|ba| &**self.ba() != ba) .map(|ba| ba.short) .collect::>(); if !deps.is_empty() { trace!("Ensuring dependencies installed for {}", self.id()); - let config = Config::get(); - let ts = config.get_tool_request_set()?.filter_by_tool(deps); - let missing = ts.missing_tools(); + let config = Config::get().await; + let ts = config.get_tool_request_set().await?.filter_by_tool(deps); + let missing = ts.missing_tools().await; if !missing.is_empty() { warn_once!( "missing dependency: {}", @@ -399,18 +409,22 @@ pub trait Backend: Debug + Send + Sync { let contents = file::read_to_string(path)?; Ok(contents.trim().to_string()) } - fn plugin(&self) -> Option<&dyn Plugin> { + fn plugin(&self) -> Option<&PluginEnum> { None } - fn install_version(&self, ctx: InstallContext, tv: ToolVersion) -> eyre::Result { + async fn install_version( + &self, + ctx: InstallContext, + tv: ToolVersion, + ) -> eyre::Result { if let Some(plugin) = self.plugin() { plugin.is_installed_err()?; } - let config = Config::get(); - if self.is_version_installed(&tv, true) { + let config = Config::try_get().await?; + if self.is_version_installed(&config, &tv, true) { if ctx.force { - self.uninstall_version(&tv, &ctx.pr, false)?; + self.uninstall_version(&tv, &ctx.pr, false).await?; } else { return Ok(tv); } @@ -420,7 +434,7 @@ pub trait Backend: Debug + Send + Sync { self.create_install_dirs(&tv)?; let old_tv = tv.clone(); - let tv = match self.install_version_(&ctx, tv) { + let tv = match self.install_version_(&ctx, tv).await { Ok(tv) => tv, Err(e) => { self.cleanup_install_dirs_on_error(&old_tv); @@ -449,30 +463,31 @@ pub trait Backend: Debug + Send + Sync { if let Some(script) = tv.request.options().get("postinstall") { ctx.pr .finish_with_message("running custom postinstall hook".to_string()); - self.run_postinstall_hook(&ctx, &tv, script)?; + self.run_postinstall_hook(&ctx, &tv, script).await?; } ctx.pr.finish_with_message("installed".to_string()); Ok(tv) } - fn run_postinstall_hook( + async fn run_postinstall_hook( &self, ctx: &InstallContext, tv: &ToolVersion, script: &str, ) -> eyre::Result<()> { + let config = Config::get().await; CmdLineRunner::new(&*env::SHELL) .env(&*env::PATH_KEY, plugins::core::path_env_with_tv_path(tv)?) .with_pr(&ctx.pr) .arg("-c") .arg(script) - .envs(self.exec_env(&Config::get(), ctx.ts, tv)?) + .envs(self.exec_env(&config, &ctx.ts, tv).await?) .execute()?; Ok(()) } - fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> eyre::Result; - fn uninstall_version( + async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result; + async fn uninstall_version( &self, tv: &ToolVersion, pr: &Box, @@ -481,7 +496,7 @@ pub trait Backend: Debug + Send + Sync { pr.set_message("uninstall".into()); if !dryrun { - self.uninstall_version_impl(pr, tv)?; + self.uninstall_version_impl(pr, tv).await?; } let rmdir = |dir: &Path| { if !dir.exists() { @@ -500,17 +515,21 @@ pub trait Backend: Debug + Send + Sync { rmdir(&tv.cache_path())?; Ok(()) } - fn uninstall_version_impl(&self, _pr: &Box, _tv: &ToolVersion) -> Result<()> { + async fn uninstall_version_impl( + &self, + _pr: &Box, + _tv: &ToolVersion, + ) -> Result<()> { Ok(()) } - fn list_bin_paths(&self, tv: &ToolVersion) -> Result> { + async fn list_bin_paths(&self, tv: &ToolVersion) -> Result> { match tv.request { ToolRequest::System { .. } => Ok(vec![]), _ => Ok(vec![tv.install_path().join("bin")]), } } - fn exec_env( + async fn exec_env( &self, _config: &Config, _ts: &Toolset, @@ -519,9 +538,10 @@ pub trait Backend: Debug + Send + Sync { Ok(BTreeMap::new()) } - fn which(&self, tv: &ToolVersion, bin_name: &str) -> eyre::Result> { + async fn which(&self, tv: &ToolVersion, bin_name: &str) -> eyre::Result> { let bin_paths = self - .list_bin_paths(tv)? + .list_bin_paths(tv) + .await? .into_iter() .filter(|p| p.parent().is_some()); for bin_path in bin_paths { @@ -573,43 +593,46 @@ pub trait Backend: Debug + Send + Sync { install_state::incomplete_file_path(&tv.ba().short, &tv.tv_pathname()) } - fn path_env_for_cmd(&self, tv: &ToolVersion) -> Result { + async fn path_env_for_cmd(&self, tv: &ToolVersion) -> Result { let path = self - .list_bin_paths(tv)? + .list_bin_paths(tv) + .await? .into_iter() - .chain(self.dependency_toolset()?.list_paths()) + .chain(self.dependency_toolset().await?.list_paths().await) .chain(env::PATH.clone()); Ok(env::join_paths(path)?) } - fn dependency_toolset(&self) -> eyre::Result { - let config = Config::get(); + async fn dependency_toolset(&self) -> eyre::Result { + let config = Config::get().await; let dependencies = self .get_all_dependencies(true)? .into_iter() .map(|ba| ba.short) .collect(); let mut ts: Toolset = config - .get_tool_request_set()? + .get_tool_request_set() + .await? .filter_by_tool(dependencies) .into(); - ts.resolve()?; + ts.resolve().await?; Ok(ts) } - fn dependency_which(&self, bin: &str) -> Option { - file::which_non_pristine(bin).or_else(|| { - self.dependency_toolset() - .ok() - .and_then(|ts| ts.which(bin)) - .and_then(|(b, tv)| b.which(&tv, bin).ok()) - .flatten() - }) + async fn dependency_which(&self, bin: &str) -> Option { + if let Some(bin) = file::which_non_pristine(bin) { + return Some(bin); + } + let Ok(ts) = self.dependency_toolset().await else { + return None; + }; + let (b, tv) = ts.which(bin).await?; + b.which(&tv, bin).await.ok().flatten() } - fn dependency_env(&self) -> eyre::Result> { - let config = Config::get(); - self.dependency_toolset()?.full_env(&config) + async fn dependency_env(&self) -> eyre::Result> { + let config = Config::get().await; + self.dependency_toolset().await?.full_env(&config).await } fn fuzzy_match_filter(&self, versions: Vec, query: &str) -> eyre::Result> { @@ -635,10 +658,11 @@ pub trait Backend: Debug + Send + Sync { Ok(versions) } - fn get_remote_version_cache(&self) -> Arc> { + fn get_remote_version_cache(&self) -> Arc> { // use a mutex to prevent deadlocks that occurs due to reentrant cache access - static REMOTE_VERSION_CACHE: Lazy>>>> = - Lazy::new(|| Mutex::new(HashMap::new())); + static REMOTE_VERSION_CACHE: Lazy< + Mutex>>>, + > = Lazy::new(Default::default); REMOTE_VERSION_CACHE .lock() @@ -655,7 +679,7 @@ pub trait Backend: Debug + Send + Sync { .with_fresh_file(plugin_path.join("bin/list-all")) } - Mutex::new(cm.build()).into() + TokioMutex::new(cm.build()).into() }) .clone() } @@ -682,7 +706,7 @@ pub trait Backend: Debug + Send + Sync { Ok(()) } - fn outdated_info(&self, _tv: &ToolVersion, _bump: bool) -> Result> { + async fn outdated_info(&self, _tv: &ToolVersion, _bump: bool) -> Result> { Ok(None) } } diff --git a/src/backend/npm.rs b/src/backend/npm.rs index 87f31b30f5..95e4cca1c6 100644 --- a/src/backend/npm.rs +++ b/src/backend/npm.rs @@ -1,3 +1,4 @@ +use crate::Result; use crate::backend::Backend; use crate::backend::backend_type::BackendType; use crate::cache::{CacheManager, CacheManagerBuilder}; @@ -7,25 +8,27 @@ use crate::config::{Config, SETTINGS}; use crate::install_context::InstallContext; use crate::timeout; use crate::toolset::ToolVersion; +use async_trait::async_trait; use serde_json::Value; -use std::fmt::Debug; -use std::sync::Mutex; +use std::{fmt::Debug, sync::Arc}; +use tokio::sync::Mutex as TokioMutex; #[derive(Debug)] pub struct NPMBackend { - ba: BackendArg, + ba: Arc, // use a mutex to prevent deadlocks that occurs due to reentrant cache access - latest_version_cache: Mutex>>, + latest_version_cache: TokioMutex>>, } const NPM_PROGRAM: &str = if cfg!(windows) { "npm.cmd" } else { "npm" }; +#[async_trait] impl Backend for NPMBackend { fn get_type(&self) -> BackendType { BackendType::Npm } - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } @@ -33,44 +36,47 @@ impl Backend for NPMBackend { Ok(vec!["node", "bun"]) } - fn _list_remote_versions(&self) -> eyre::Result> { - timeout::run_with_timeout( - || { + async fn _list_remote_versions(&self) -> eyre::Result> { + timeout::run_with_timeout_async( + async || { let raw = cmd!(NPM_PROGRAM, "view", self.tool_name(), "versions", "--json") - .full_env(self.dependency_env()?) + .full_env(self.dependency_env().await?) .read()?; let versions: Vec = serde_json::from_str(&raw)?; Ok(versions) }, SETTINGS.fetch_remote_versions_timeout(), ) + .await } - fn latest_stable_version(&self) -> eyre::Result> { - let fetch = || { - let raw = cmd!(NPM_PROGRAM, "view", self.tool_name(), "dist-tags", "--json") - .full_env(self.dependency_env()?) - .read()?; - let dist_tags: Value = serde_json::from_str(&raw)?; - match dist_tags["latest"] { - Value::String(ref s) => Ok(Some(s.clone())), - _ => self.latest_version(Some("latest".into())), - } - }; - timeout::run_with_timeout( - || { - if let Ok(cache) = self.latest_version_cache.try_lock() { - cache.get_or_try_init(fetch).cloned() - } else { - fetch() - } + async fn latest_stable_version(&self) -> eyre::Result> { + let cache = self.latest_version_cache.lock().await; + let this = self; + timeout::run_with_timeout_async( + async || { + cache + .get_or_try_init_async(async || { + let raw = + cmd!(NPM_PROGRAM, "view", this.tool_name(), "dist-tags", "--json") + .full_env(this.dependency_env().await?) + .read()?; + let dist_tags: Value = serde_json::from_str(&raw)?; + match dist_tags["latest"] { + Value::String(ref s) => Ok(Some(s.clone())), + _ => this.latest_version(Some("latest".into())).await, + } + }) + .await }, SETTINGS.fetch_remote_versions_timeout(), ) + .await + .cloned() } - fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> eyre::Result { - let config = Config::try_get()?; + async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { + let config = Config::get().await; if SETTINGS.npm.bun { CmdLineRunner::new("bun") @@ -81,11 +87,11 @@ impl Backend for NPMBackend { .arg("--global") .arg("--trust") .with_pr(&ctx.pr) - .envs(ctx.ts.env_with_path(&config)?) + .envs(ctx.ts.env_with_path(&config).await?) .env("BUN_INSTALL_GLOBAL_DIR", tv.install_path()) .env("BUN_INSTALL_BIN", tv.install_path().join("bin")) - .prepend_path(ctx.ts.list_paths())? - .prepend_path(self.dependency_toolset()?.list_paths())? + .prepend_path(ctx.ts.list_paths().await)? + .prepend_path(self.dependency_toolset().await?.list_paths().await)? .execute()?; } else { CmdLineRunner::new(NPM_PROGRAM) @@ -95,16 +101,16 @@ impl Backend for NPMBackend { .arg("--prefix") .arg(tv.install_path()) .with_pr(&ctx.pr) - .envs(ctx.ts.env_with_path(&config)?) - .prepend_path(ctx.ts.list_paths())? - .prepend_path(self.dependency_toolset()?.list_paths())? + .envs(ctx.ts.env_with_path(&config).await?) + .prepend_path(ctx.ts.list_paths().await)? + .prepend_path(self.dependency_toolset().await?.list_paths().await)? .execute()?; } Ok(tv) } #[cfg(windows)] - fn list_bin_paths( + async fn list_bin_paths( &self, tv: &crate::toolset::ToolVersion, ) -> eyre::Result> { @@ -115,12 +121,12 @@ impl Backend for NPMBackend { impl NPMBackend { pub fn from_arg(ba: BackendArg) -> Self { Self { - latest_version_cache: Mutex::new( + latest_version_cache: TokioMutex::new( CacheManagerBuilder::new(ba.cache_path.join("latest_version.msgpack.z")) .with_fresh_duration(SETTINGS.fetch_remote_versions_cache()) .build(), ), - ba, + ba: Arc::new(ba), } } } diff --git a/src/backend/pipx.rs b/src/backend/pipx.rs index 2433ea400d..17c4bcb2dd 100644 --- a/src/backend/pipx.rs +++ b/src/backend/pipx.rs @@ -1,4 +1,3 @@ -use crate::backend::Backend; use crate::backend::backend_type::BackendType; use crate::cache::{CacheManager, CacheManagerBuilder}; use crate::cli::args::BackendArg; @@ -10,27 +9,30 @@ use crate::install_context::InstallContext; use crate::toolset::{ToolVersion, ToolVersionOptions, Toolset, ToolsetBuilder}; use crate::ui::multi_progress_report::MultiProgressReport; use crate::ui::progress_report::SingleReport; +use crate::{backend::Backend, timeout}; +use async_trait::async_trait; use eyre::{Result, eyre}; use indexmap::IndexMap; use itertools::Itertools; use regex::Regex; -use std::fmt::Debug; use std::str::FromStr; +use std::{fmt::Debug, sync::Arc}; use versions::Versioning; use xx::regex; #[derive(Debug)] pub struct PIPXBackend { - ba: BackendArg, + ba: Arc, latest_version_cache: CacheManager>, } +#[async_trait] impl Backend for PIPXBackend { fn get_type(&self) -> BackendType { BackendType::Pipx } - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } @@ -46,14 +48,14 @@ impl Backend for PIPXBackend { * Pipx doesn't have a remote version concept across its backends, so * we return a single version. */ - fn _list_remote_versions(&self) -> eyre::Result> { + async fn _list_remote_versions(&self) -> eyre::Result> { match self.tool_name().parse()? { PipxRequest::Pypi(package) => { let registry_url = self.get_registry_url()?; if registry_url.contains("/json") { debug!("Fetching JSON for {}", package); let url = format!("https://pypi.org/pypi/{package}/json"); - let data: PypiPackage = HTTP_FETCH.json(url)?; + let data: PypiPackage = HTTP_FETCH.json(url).await?; let versions = data .releases .keys() @@ -65,7 +67,7 @@ impl Backend for PIPXBackend { } else { debug!("Fetching HTML for {}", package); let url = format!("https://pypi.org/simple/{package}/"); - let html = HTTP_FETCH.get_html(url)?; + let html = HTTP_FETCH.get_html(url).await?; let version_re = regex!(r#"href=["'].*?/([^/]+)\.tar\.gz["']"#); let versions: Vec = version_re @@ -86,64 +88,73 @@ impl Backend for PIPXBackend { } PipxRequest::Git(url) if url.starts_with("https://github.com/") => { let repo = url.strip_prefix("https://github.com/").unwrap(); - let data = github::list_releases(repo)?; + let data = github::list_releases(repo).await?; Ok(data.into_iter().rev().map(|r| r.tag_name).collect()) } PipxRequest::Git { .. } => Ok(vec!["latest".to_string()]), } } - fn latest_stable_version(&self) -> eyre::Result> { - self.latest_version_cache - .get_or_try_init(|| match self.tool_name().parse()? { - PipxRequest::Pypi(package) => { - let registry_url = self.get_registry_url()?; - if registry_url.contains("/json") { - debug!("Fetching JSON for {}", package); - let url = format!("https://pypi.org/pypi/{package}/json"); - let pkg: PypiPackage = HTTP_FETCH.json(url)?; - Ok(Some(pkg.info.version)) - } else { - debug!("Fetching HTML for {}", package); - let url = format!("https://pypi.org/simple/{package}/"); - let html = HTTP_FETCH.get_html(url)?; - - let version_re = regex!(r#"href=["'].*?/([^/]+)\.tar\.gz["']"#); - let version = version_re - .captures_iter(&html) - .filter_map(|cap| { - let filename = cap.get(1)?.as_str(); - let escaped_package = regex::escape(&package); - let re_str = format!("^{escaped_package}-(.+)$"); - let pkg_re = regex::Regex::new(&re_str).ok()?; - let pkg_version = pkg_re.captures(filename)?.get(1)?.as_str(); - Some(pkg_version.to_string()) - }) - .filter(|v| { - !v.contains("dev") - && !v.contains("a") - && !v.contains("b") - && !v.contains("rc") - }) - .sorted_by_cached_key(|v| Versioning::new(v)) - .next_back(); - - Ok(version) - } - } - _ => self.latest_version(Some("latest".into())), - }) - .cloned() + async fn latest_stable_version(&self) -> eyre::Result> { + let this = self; + timeout::run_with_timeout_async( + async || { + this.latest_version_cache + .get_or_try_init_async(async || match this.tool_name().parse()? { + PipxRequest::Pypi(package) => { + let registry_url = this.get_registry_url()?; + if registry_url.contains("/json") { + debug!("Fetching JSON for {}", package); + let url = format!("https://pypi.org/pypi/{package}/json"); + let pkg: PypiPackage = HTTP_FETCH.json(url).await?; + Ok(Some(pkg.info.version)) + } else { + debug!("Fetching HTML for {}", package); + let url = format!("https://pypi.org/simple/{package}/"); + let html = HTTP_FETCH.get_html(url).await?; + + let version_re = regex!(r#"href=["'].*?/([^/]+)\.tar\.gz["']"#); + let version = version_re + .captures_iter(&html) + .filter_map(|cap| { + let filename = cap.get(1)?.as_str(); + let escaped_package = regex::escape(&package); + let re_str = format!("^{escaped_package}-(.+)$"); + let pkg_re = regex::Regex::new(&re_str).ok()?; + let pkg_version = + pkg_re.captures(filename)?.get(1)?.as_str(); + Some(pkg_version.to_string()) + }) + .filter(|v| { + !v.contains("dev") + && !v.contains("a") + && !v.contains("b") + && !v.contains("rc") + }) + .sorted_by_cached_key(|v| Versioning::new(v)) + .next_back(); + + Ok(version) + } + } + _ => this.latest_version(Some("latest".into())).await, + }) + .await + }, + SETTINGS.fetch_remote_versions_timeout(), + ) + .await + .cloned() } - fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { - let config = Config::try_get()?; + async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { + let config = Config::get().await; let pipx_request = self .tool_name() .parse::()? .pipx_request(&tv.version, &tv.request.options()); - if self.uv_is_installed() + if self.uv_is_installed().await && SETTINGS.pipx.uvx != Some(false) && tv.request.options().get("uvx") != Some(&"false".to_string()) { @@ -154,9 +165,10 @@ impl Backend for PIPXBackend { &["tool", "install", &pipx_request], self, &tv, - ctx.ts, + &ctx.ts, &ctx.pr, - )?; + ) + .await?; if let Some(args) = tv.request.options().get("uvx_args") { cmd = cmd.args(shell_words::split(args)?); } @@ -168,9 +180,10 @@ impl Backend for PIPXBackend { &["install", &pipx_request], self, &tv, - ctx.ts, + &ctx.ts, &ctx.pr, - )?; + ) + .await?; if let Some(args) = tv.request.options().get("pipx_args") { cmd = cmd.args(shell_words::split(args)?); } @@ -188,7 +201,7 @@ impl PIPXBackend { ) .with_fresh_duration(SETTINGS.fetch_remote_versions_cache()) .build(), - ba, + ba: Arc::new(ba), } } @@ -208,11 +221,12 @@ impl PIPXBackend { Ok(registry_url) } - pub fn reinstall_all() -> Result<()> { - let config = Config::load()?; - let ts = ToolsetBuilder::new().build(&config)?; + pub async fn reinstall_all() -> Result<()> { + let config = Config::load().await?; + let ts = ToolsetBuilder::new().build(&config).await?; let pipx_tools = ts - .list_installed_versions()? + .list_installed_versions() + .await? .into_iter() .filter(|(b, _tv)| b.ba().backend_type() == BackendType::Pipx) .collect_vec(); @@ -224,20 +238,24 @@ impl PIPXBackend { ("install", format!("{}=={}", tv.ba().tool_name, tv.version)), ] { let args = &["tool", cmd, tool]; - Self::uvx_cmd(&config, args, &*b, &tv, &ts, &pr)?.execute()?; + Self::uvx_cmd(&config, args, &*b, &tv, &ts, &pr) + .await? + .execute()?; } } } else { let pr = MultiProgressReport::get().add("reinstalling pipx tools"); for (b, tv) in pipx_tools { let args = &["reinstall", &tv.ba().tool_name]; - Self::pipx_cmd(&config, args, &*b, &tv, &ts, &pr)?.execute()?; + Self::pipx_cmd(&config, args, &*b, &tv, &ts, &pr) + .await? + .execute()?; } } Ok(()) } - fn uvx_cmd<'a>( + async fn uvx_cmd<'a>( config: &Config, args: &[&str], b: &dyn Backend, @@ -252,13 +270,13 @@ impl PIPXBackend { cmd.with_pr(pr) .env("UV_TOOL_DIR", tv.install_path()) .env("UV_TOOL_BIN_DIR", tv.install_path().join("bin")) - .envs(ts.env_with_path(config)?) - .prepend_path(ts.list_paths())? + .envs(ts.env_with_path(config).await?) + .prepend_path(ts.list_paths().await)? .prepend_path(vec![tv.install_path().join("bin")])? - .prepend_path(b.dependency_toolset()?.list_paths()) + .prepend_path(b.dependency_toolset().await?.list_paths().await) } - fn pipx_cmd<'a>( + async fn pipx_cmd<'a>( config: &Config, args: &[&str], b: &dyn Backend, @@ -273,14 +291,14 @@ impl PIPXBackend { cmd.with_pr(pr) .env("PIPX_HOME", tv.install_path()) .env("PIPX_BIN_DIR", tv.install_path().join("bin")) - .envs(ts.env_with_path(config)?) - .prepend_path(ts.list_paths())? + .envs(ts.env_with_path(config).await?) + .prepend_path(ts.list_paths().await)? .prepend_path(vec![tv.install_path().join("bin")])? - .prepend_path(b.dependency_toolset()?.list_paths()) + .prepend_path(b.dependency_toolset().await?.list_paths().await) } - fn uv_is_installed(&self) -> bool { - self.dependency_which("uv").is_some() + async fn uv_is_installed(&self) -> bool { + self.dependency_which("uv").await.is_some() } } diff --git a/src/backend/spm.rs b/src/backend/spm.rs index 74ac1daaba..5a897eb74b 100644 --- a/src/backend/spm.rs +++ b/src/backend/spm.rs @@ -7,27 +7,31 @@ use crate::git::{CloneOptions, Git}; use crate::install_context::InstallContext; use crate::toolset::ToolVersion; use crate::{dirs, file, github}; +use async_trait::async_trait; use eyre::WrapErr; use serde::Deserializer; use serde::de::{MapAccess, Visitor}; use serde_derive::Deserialize; -use std::fmt::{self, Debug}; use std::path::PathBuf; +use std::{ + fmt::{self, Debug}, + sync::Arc, +}; use url::Url; use xx::regex; #[derive(Debug)] pub struct SPMBackend { - ba: BackendArg, + ba: Arc, } -// https://github.com/apple/swift-package-manager +#[async_trait] impl Backend for SPMBackend { fn get_type(&self) -> BackendType { BackendType::Spm } - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } @@ -35,36 +39,44 @@ impl Backend for SPMBackend { Ok(vec!["swift"]) } - fn _list_remote_versions(&self) -> eyre::Result> { + async fn _list_remote_versions(&self) -> eyre::Result> { let repo = SwiftPackageRepo::new(&self.tool_name())?; - Ok(github::list_releases(repo.shorthand.as_str())? + Ok(github::list_releases(repo.shorthand.as_str()) + .await? .into_iter() .map(|r| r.tag_name) .rev() .collect()) } - fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> eyre::Result { + async fn install_version_( + &self, + ctx: &InstallContext, + tv: ToolVersion, + ) -> eyre::Result { let settings = Settings::get(); settings.ensure_experimental("spm backend")?; let repo = SwiftPackageRepo::new(&self.tool_name())?; let revision = if tv.version == "latest" { - self.latest_stable_version()? + self.latest_stable_version() + .await? .ok_or_else(|| eyre::eyre!("No stable versions found"))? } else { tv.version.clone() }; let repo_dir = self.clone_package_repo(ctx, &tv, &repo, &revision)?; - let executables = self.get_executable_names(&repo_dir, &tv)?; + let executables = self.get_executable_names(&repo_dir, &tv).await?; if executables.is_empty() { return Err(eyre::eyre!("No executables found in the package")); } let bin_path = tv.install_path().join("bin"); file::create_dir_all(&bin_path)?; for executable in executables { - let exe_path = self.build_executable(&executable, &repo_dir, ctx, &tv)?; + let exe_path = self + .build_executable(&executable, &repo_dir, ctx, &tv) + .await?; file::make_symlink(&exe_path, &bin_path.join(executable))?; } @@ -78,7 +90,7 @@ impl Backend for SPMBackend { impl SPMBackend { pub fn from_arg(ba: BackendArg) -> Self { - Self { ba } + Self { ba: Arc::new(ba) } } fn clone_package_repo( @@ -106,7 +118,7 @@ impl SPMBackend { Ok(repo.dir) } - fn get_executable_names( + async fn get_executable_names( &self, repo_dir: &PathBuf, tv: &ToolVersion, @@ -122,7 +134,7 @@ impl SPMBackend { "--cache-path", dirs::CACHE.join("spm"), ) - .full_env(self.dependency_env()?) + .full_env(self.dependency_env().await?) .read()?; let executables = serde_json::from_str::(&package_json) .wrap_err("Failed to parse package description")? @@ -135,11 +147,11 @@ impl SPMBackend { Ok(executables) } - fn build_executable( + async fn build_executable( &self, executable: &str, repo_dir: &PathBuf, - ctx: &InstallContext<'_>, + ctx: &InstallContext, tv: &ToolVersion, ) -> Result { debug!("Building swift package"); @@ -156,7 +168,7 @@ impl SPMBackend { .arg("--cache-path") .arg(dirs::CACHE.join("spm")) .with_pr(&ctx.pr) - .prepend_path(self.dependency_toolset()?.list_paths())? + .prepend_path(self.dependency_toolset().await?.list_paths().await)? .execute()?; let bin_path = cmd!( @@ -174,7 +186,7 @@ impl SPMBackend { dirs::CACHE.join("spm"), "--show-bin-path" ) - .full_env(self.dependency_env()?) + .full_env(self.dependency_env().await?) .read()?; Ok(PathBuf::from(bin_path.trim().to_string()).join(executable)) } diff --git a/src/backend/ubi.rs b/src/backend/ubi.rs index 0a1403c27f..d70592d3a9 100644 --- a/src/backend/ubi.rs +++ b/src/backend/ubi.rs @@ -1,4 +1,3 @@ -use crate::backend::Backend; use crate::backend::backend_type::BackendType; use crate::cli::args::BackendArg; use crate::config::SETTINGS; @@ -7,36 +6,37 @@ use crate::env::{ }; use crate::install_context::InstallContext; use crate::plugins::VERSION_REGEX; -use crate::tokio::RUNTIME; use crate::toolset::ToolVersion; +use crate::{backend::Backend, toolset::ToolVersionOptions}; use crate::{file, github, gitlab, hash}; +use async_trait::async_trait; use eyre::bail; use itertools::Itertools; use regex::Regex; -use std::env; -use std::fmt::Debug; use std::path::Path; use std::str::FromStr; use std::sync::OnceLock; +use std::{env, sync::Arc}; +use std::{fmt::Debug, sync::LazyLock}; use ubi::{ForgeType, UbiBuilder}; use xx::regex; #[derive(Debug)] pub struct UbiBackend { - ba: BackendArg, + ba: Arc, } -// Uses ubi for installations https://github.com/houseabsolute/ubi +#[async_trait] impl Backend for UbiBackend { fn get_type(&self) -> BackendType { BackendType::Ubi } - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } - fn _list_remote_versions(&self) -> eyre::Result> { + async fn _list_remote_versions(&self) -> eyre::Result> { if name_is_url(&self.tool_name()) { Ok(vec!["latest".to_string()]) } else { @@ -54,11 +54,13 @@ impl Backend for UbiBackend { }; let tag_regex = OnceLock::new(); let mut versions = match forge { - ForgeType::GitHub => github::list_releases_from_url(api_url, &self.tool_name())? + ForgeType::GitHub => github::list_releases_from_url(api_url, &self.tool_name()) + .await? .into_iter() .map(|r| r.tag_name) .collect::>(), - ForgeType::GitLab => gitlab::list_releases_from_url(api_url, &self.tool_name())? + ForgeType::GitLab => gitlab::list_releases_from_url(api_url, &self.tool_name()) + .await? .into_iter() .map(|r| r.tag_name) .collect::>(), @@ -66,12 +68,14 @@ impl Backend for UbiBackend { if versions.is_empty() { match forge { ForgeType::GitHub => { - versions = github::list_tags_from_url(api_url, &self.tool_name())? + versions = github::list_tags_from_url(api_url, &self.tool_name()) + .await? .into_iter() .collect(); } ForgeType::GitLab => { - versions = gitlab::list_tags_from_url(api_url, &self.tool_name())? + versions = gitlab::list_tags_from_url(api_url, &self.tool_name()) + .await? .into_iter() .collect(); } @@ -99,7 +103,7 @@ impl Backend for UbiBackend { } } - fn install_version_( + async fn install_version_( &self, ctx: &InstallContext, mut tv: ToolVersion, @@ -122,12 +126,12 @@ impl Backend for UbiBackend { if !name_is_url(&self.tool_name()) { let release: Result<_, eyre::Report> = match forge { - ForgeType::GitHub => { - github::get_release_for_url(api_url, &self.tool_name(), &v).map(|_| "github") - } - ForgeType::GitLab => { - gitlab::get_release_for_url(api_url, &self.tool_name(), &v).map(|_| "gitlab") - } + ForgeType::GitHub => github::get_release_for_url(api_url, &self.tool_name(), &v) + .await + .map(|_| "github"), + ForgeType::GitLab => gitlab::get_release_for_url(api_url, &self.tool_name(), &v) + .await + .map(|_| "gitlab"), }; if let Err(err) = release { // this can fail with a rate limit error or 404, either way, try prefixing and if it fails, try without the prefix @@ -141,63 +145,17 @@ impl Backend for UbiBackend { } } - let install = |v: &str| { - // Workaround because of not knowing how to pull out the value correctly without quoting - let name = self.tool_name(); - - let mut builder = UbiBuilder::new().install_dir(&bin_dir); - - if name_is_url(&name) { - builder = builder.url(&name); - } else { - builder = builder.project(&name); - builder = builder.tag(v); - } - - if extract_all { - builder = builder.extract_all(); - } else { - if let Some(exe) = opts.get("exe") { - builder = builder.exe(exe); - } - if let Some(rename_exe) = opts.get("rename_exe") { - builder = builder.rename_exe_to(rename_exe) - } - } - if let Some(matching) = opts.get("matching") { - builder = builder.matching(matching); - } - - let forge = match opts.get("provider") { - Some(forge) => ForgeType::from_str(forge)?, - None => ForgeType::default(), - }; - builder = builder.forge(forge.clone()); - builder = set_token(builder, &forge); - - if let Some(api_url) = opts.get("api_url") { - if !api_url.contains("github.com") && !api_url.contains("gitlab.com") { - builder = builder.api_base_url(api_url.strip_suffix("/").unwrap_or(api_url)); - builder = set_enterprise_token(builder, &forge); - } - } - - let mut ubi = builder.build().map_err(|e| eyre::eyre!(e))?; - - RUNTIME - .block_on(ubi.install_binary()) - .map_err(|e| eyre::eyre!(e)) - }; - - install(&v).or_else(|err: eyre::Error| { + if let Err(err) = install(&self.tool_name(), &v, &bin_dir, extract_all, &opts).await { debug!( "Failed to install with ubi version '{}': {}, trying with '{}'", v, err, tv ); - install(&tv.version).or_else(|_| { + if let Err(err) = + install(&self.tool_name(), &tv.version, &bin_dir, extract_all, &opts).await + { bail!("Failed to install with ubi '{}': {}", tv, err); - }) - })?; + } + } let mut possible_exes = vec![ tv.request @@ -283,7 +241,7 @@ impl Backend for UbiBackend { Ok(()) } - fn list_bin_paths(&self, tv: &ToolVersion) -> eyre::Result> { + async fn list_bin_paths(&self, tv: &ToolVersion) -> eyre::Result> { let opts = tv.request.options(); if let Some(bin_path) = opts.get("bin_path") { Ok(vec![tv.install_path().join(bin_path)]) @@ -302,7 +260,7 @@ impl Backend for UbiBackend { impl UbiBackend { pub fn from_arg(ba: BackendArg) -> Self { - Self { ba } + Self { ba: Arc::new(ba) } } } @@ -343,3 +301,66 @@ fn set_enterprise_token<'a>(mut builder: UbiBuilder<'a>, forge: &ForgeType) -> U } } } + +async fn install( + name: &str, + v: &str, + bin_dir: &Path, + extract_all: bool, + opts: &ToolVersionOptions, +) -> eyre::Result<()> { + let mut builder = UbiBuilder::new().install_dir(bin_dir); + + if name_is_url(name) { + builder = builder.url(name); + } else { + builder = builder.project(name); + builder = builder.tag(v); + } + + if extract_all { + builder = builder.extract_all(); + } else { + if let Some(exe) = opts.get("exe") { + builder = builder.exe(exe); + } + if let Some(rename_exe) = opts.get("rename_exe") { + builder = builder.rename_exe_to(rename_exe) + } + } + if let Some(matching) = opts.get("matching") { + builder = builder.matching(matching); + } + + let forge = match opts.get("provider") { + Some(forge) => ForgeType::from_str(forge)?, + None => ForgeType::default(), + }; + builder = builder.forge(forge.clone()); + builder = set_token(builder, &forge); + + if let Some(api_url) = opts.get("api_url") { + if !api_url.contains("github.com") && !api_url.contains("gitlab.com") { + builder = builder.api_base_url(api_url.strip_suffix("/").unwrap_or(api_url)); + builder = set_enterprise_token(builder, &forge); + } + } + + let mut ubi = builder.build().map_err(|e| eyre::eyre!(e))?; + + // TODO: hacky but does not compile without it + tokio::task::block_in_place(|| { + static RT: LazyLock = LazyLock::new(|| { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + }); + RT.block_on(async { + match ubi.install_binary().await { + Ok(_) => Ok(()), + Err(e) => Err(eyre::eyre!(e)), + } + }) + }) +} diff --git a/src/backend/vfox.rs b/src/backend/vfox.rs index b47a60969f..7994d1b1b8 100644 --- a/src/backend/vfox.rs +++ b/src/backend/vfox.rs @@ -1,10 +1,12 @@ -use crate::{env, timeout}; +use crate::{env, plugins::PluginEnum, timeout}; +use async_trait::async_trait; use heck::ToKebabCase; use std::collections::{BTreeMap, HashMap}; use std::fmt::Debug; use std::path::PathBuf; -use std::sync::RwLock; +use std::sync::Arc; use std::thread; +use tokio::sync::RwLock; use crate::backend::Backend; use crate::backend::backend_type::BackendType; @@ -16,24 +18,25 @@ use crate::env_diff::EnvMap; use crate::install_context::InstallContext; use crate::plugins::vfox_plugin::VfoxPlugin; use crate::plugins::{Plugin, PluginType}; -use crate::tokio::RUNTIME; use crate::toolset::{ToolVersion, Toolset}; use crate::ui::multi_progress_report::MultiProgressReport; #[derive(Debug)] pub struct VfoxBackend { - ba: BackendArg, - plugin: Box, + ba: Arc, + plugin: Arc, + plugin_enum: PluginEnum, exec_env_cache: RwLock>>, pathname: String, } +#[async_trait] impl Backend for VfoxBackend { fn get_type(&self) -> BackendType { BackendType::Vfox } - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } @@ -41,12 +44,13 @@ impl Backend for VfoxBackend { Some(PluginType::Vfox) } - fn _list_remote_versions(&self) -> eyre::Result> { - timeout::run_with_timeout( - || { - let (vfox, _log_rx) = self.plugin.vfox(); - self.ensure_plugin_installed()?; - let versions = RUNTIME.block_on(vfox.list_available_versions(&self.pathname))?; + async fn _list_remote_versions(&self) -> eyre::Result> { + let this = self; + timeout::run_with_timeout_async( + || async { + let (vfox, _log_rx) = this.plugin.vfox(); + this.ensure_plugin_installed().await?; + let versions = vfox.list_available_versions(&this.pathname).await?; Ok(versions .into_iter() .rev() @@ -55,14 +59,15 @@ impl Backend for VfoxBackend { }, SETTINGS.fetch_remote_versions_timeout(), ) + .await } - fn install_version_( + async fn install_version_( &self, _ctx: &InstallContext, tv: ToolVersion, ) -> eyre::Result { - self.ensure_plugin_installed()?; + self.ensure_plugin_installed().await?; let (vfox, log_rx) = self.plugin.vfox(); thread::spawn(|| { for line in log_rx { @@ -70,13 +75,15 @@ impl Backend for VfoxBackend { info!("{}", line); } }); - RUNTIME.block_on(vfox.install(&self.pathname, &tv.version, tv.install_path()))?; + vfox.install(&self.pathname, &tv.version, tv.install_path()) + .await?; Ok(tv) } - fn list_bin_paths(&self, tv: &ToolVersion) -> eyre::Result> { + async fn list_bin_paths(&self, tv: &ToolVersion) -> eyre::Result> { let path = self - ._exec_env(tv)? + ._exec_env(tv) + .await? .iter() .find(|(k, _)| k.to_uppercase() == "PATH") .map(|(_, v)| v.to_string()) @@ -84,16 +91,22 @@ impl Backend for VfoxBackend { Ok(env::split_paths(&path).collect()) } - fn exec_env(&self, _config: &Config, _ts: &Toolset, tv: &ToolVersion) -> eyre::Result { + async fn exec_env( + &self, + _config: &Config, + _ts: &Toolset, + tv: &ToolVersion, + ) -> eyre::Result { Ok(self - ._exec_env(tv)? + ._exec_env(tv) + .await? .into_iter() .filter(|(k, _)| k.to_uppercase() != "PATH") .collect()) } - fn plugin(&self) -> Option<&dyn Plugin> { - Some(&*self.plugin) + fn plugin(&self) -> Option<&PluginEnum> { + Some(&self.plugin_enum) } } @@ -103,18 +116,20 @@ impl VfoxBackend { let plugin_path = dirs::PLUGINS.join(&pathname); let mut plugin = VfoxPlugin::new(pathname.clone(), plugin_path.clone()); plugin.full = Some(ba.full()); + let plugin = Arc::new(plugin); Self { exec_env_cache: Default::default(), - plugin: Box::new(plugin), - ba, + plugin: plugin.clone(), + plugin_enum: PluginEnum::Vfox(plugin), + ba: Arc::new(ba), pathname, } } - fn _exec_env(&self, tv: &ToolVersion) -> eyre::Result> { + async fn _exec_env(&self, tv: &ToolVersion) -> eyre::Result> { let key = tv.to_string(); - if !self.exec_env_cache.read().unwrap().contains_key(&key) { - let mut caches = self.exec_env_cache.write().unwrap(); + if !self.exec_env_cache.read().await.contains_key(&key) { + let mut caches = self.exec_env_cache.write().await; caches.insert( key.clone(), CacheManagerBuilder::new(tv.cache_path().join("exec_env.msgpack.z")) @@ -124,14 +139,15 @@ impl VfoxBackend { .build(), ); } - let exec_env_cache = self.exec_env_cache.read().unwrap(); + let exec_env_cache = self.exec_env_cache.read().await; let cache = exec_env_cache.get(&key).unwrap(); cache - .get_or_try_init(|| { - self.ensure_plugin_installed()?; + .get_or_try_init_async(async || { + self.ensure_plugin_installed().await?; let (vfox, _log_rx) = self.plugin.vfox(); - Ok(RUNTIME - .block_on(vfox.env_keys(&self.pathname, &tv.version))? + Ok(vfox + .env_keys(&self.pathname, &tv.version) + .await? .into_iter() .fold(BTreeMap::new(), |mut acc, env_key| { let key = &env_key.key; @@ -151,11 +167,13 @@ impl VfoxBackend { acc })) }) + .await .cloned() } - fn ensure_plugin_installed(&self) -> eyre::Result<()> { + async fn ensure_plugin_installed(&self) -> eyre::Result<()> { self.plugin .ensure_installed(&MultiProgressReport::get(), false) + .await } } diff --git a/src/cache.rs b/src/cache.rs index ad5f3610a4..d1c121f936 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -81,6 +81,7 @@ impl CacheManagerBuilder { CacheManager { cache_file_path, cache: Box::new(OnceCell::new()), + cache_async: Box::new(tokio::sync::OnceCell::new()), fresh_files: self.fresh_files, fresh_duration: self.fresh_duration, } @@ -96,6 +97,7 @@ where fresh_duration: Option, fresh_files: Vec, cache: Box>, + cache_async: Box>, } impl CacheManager @@ -125,6 +127,33 @@ where Ok(val) } + pub async fn get_or_try_init_async(&self, fetch: F) -> Result<&T> + where + F: FnOnce() -> Fut, + Fut: Future>, + { + let val = self + .cache_async + .get_or_try_init(|| async { + let path = &self.cache_file_path; + if self.is_fresh() { + match self.parse() { + Ok(val) => return Ok::<_, color_eyre::Report>(val), + Err(err) => { + warn!("failed to parse cache file: {} {:#}", path.display(), err); + } + } + } + let val = fetch().await?; + if let Err(err) = self.write(&val) { + warn!("failed to write cache file: {} {:#}", path.display(), err); + } + Ok(val) + }) + .await?; + Ok(val) + } + fn parse(&self) -> Result { let path = &self.cache_file_path; trace!("reading {}", display_path(path)); diff --git a/src/cli/alias/get.rs b/src/cli/alias/get.rs index 66ad9923b2..07c2ae9a86 100644 --- a/src/cli/alias/get.rs +++ b/src/cli/alias/get.rs @@ -17,8 +17,8 @@ pub struct AliasGet { } impl AliasGet { - pub fn run(self) -> Result<()> { - let config = Config::try_get()?; + pub async fn run(self) -> Result<()> { + let config = Config::get().await; match config.all_aliases.get(&self.plugin.short) { Some(alias) => match alias.versions.get(&self.alias) { Some(alias) => { diff --git a/src/cli/alias/ls.rs b/src/cli/alias/ls.rs index e57bd71c20..734bccd96c 100644 --- a/src/cli/alias/ls.rs +++ b/src/cli/alias/ls.rs @@ -27,8 +27,8 @@ pub struct AliasLs { } impl AliasLs { - pub fn run(self) -> Result<()> { - let config = Config::try_get()?; + pub async fn run(self) -> Result<()> { + let config = Config::get().await; let rows = config .all_aliases .iter() diff --git a/src/cli/alias/mod.rs b/src/cli/alias/mod.rs index 9588e0b3e2..50480f26e9 100644 --- a/src/cli/alias/mod.rs +++ b/src/cli/alias/mod.rs @@ -36,23 +36,23 @@ enum Commands { } impl Commands { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { match self { - Self::Get(cmd) => cmd.run(), - Self::Ls(cmd) => cmd.run(), - Self::Set(cmd) => cmd.run(), - Self::Unset(cmd) => cmd.run(), + Self::Get(cmd) => cmd.run().await, + Self::Ls(cmd) => cmd.run().await, + Self::Set(cmd) => cmd.run().await, + Self::Unset(cmd) => cmd.run().await, } } } impl Alias { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { let cmd = self.command.unwrap_or(Commands::Ls(ls::AliasLs { tool: self.plugin, no_header: self.no_header, })); - cmd.run() + cmd.run().await } } diff --git a/src/cli/alias/set.rs b/src/cli/alias/set.rs index 5c91c17215..11a68c8d97 100644 --- a/src/cli/alias/set.rs +++ b/src/cli/alias/set.rs @@ -19,8 +19,8 @@ pub struct AliasSet { } impl AliasSet { - pub fn run(self) -> Result<()> { - let mut global_config = Config::try_get()?.global_config()?; + pub async fn run(self) -> Result<()> { + let mut global_config = Config::get().await.global_config()?; global_config.set_alias(&self.plugin, &self.alias, &self.value)?; global_config.save() } diff --git a/src/cli/alias/unset.rs b/src/cli/alias/unset.rs index c0539421fc..5b578380e2 100644 --- a/src/cli/alias/unset.rs +++ b/src/cli/alias/unset.rs @@ -17,8 +17,8 @@ pub struct AliasUnset { } impl AliasUnset { - pub fn run(self) -> Result<()> { - let mut global_config = Config::try_get()?.global_config()?; + pub async fn run(self) -> Result<()> { + let mut global_config = Config::get().await.global_config()?; global_config.remove_alias(&self.plugin, &self.alias)?; global_config.save() } diff --git a/src/cli/args/backend_arg.rs b/src/cli/args/backend_arg.rs index 0483428635..04fcfa50c7 100644 --- a/src/cli/args/backend_arg.rs +++ b/src/cli/args/backend_arg.rs @@ -113,7 +113,7 @@ impl BackendArg { return backend_type; } if config::is_loaded() { - if let Some(repo_url) = Config::get().get_repo_url(&self.short) { + if let Some(repo_url) = Config::get_().get_repo_url(&self.short) { return if repo_url.contains("vfox-") { BackendType::Vfox } else { @@ -128,21 +128,24 @@ impl BackendArg { pub fn full(&self) -> String { let short = unalias_backend(&self.short); if config::is_loaded() { - if let Some(full) = Config::get() + if let Some(full) = Config::get_() .all_aliases .get(short) .and_then(|a| a.backend.clone()) { return full; } - if let Some(url) = Config::get().repo_urls.get(short) { + if let Some(url) = Config::get_().repo_urls.get(short) { deprecated!( "config_plugins", "[plugins] section of mise.toml is deprecated. Use [alias] instead. https://mise.jdx.dev/dev-tools/aliases.html" ); return format!("asdf:{url}"); } - if let Some(lt) = lockfile::get_locked_version(None, short, "").unwrap_or_default() { + let config = Config::get_(); + if let Some(lt) = + lockfile::get_locked_version(&config, None, short, "").unwrap_or_default() + { if let Some(backend) = lt.backend { return backend; } diff --git a/src/cli/args/tool_arg.rs b/src/cli/args/tool_arg.rs index 219c25e2a4..f37226d41e 100644 --- a/src/cli/args/tool_arg.rs +++ b/src/cli/args/tool_arg.rs @@ -1,6 +1,6 @@ -use std::fmt::Display; use std::path::PathBuf; use std::str::FromStr; +use std::{fmt::Display, sync::Arc}; use crate::cli::args::BackendArg; use crate::toolset::{ToolRequest, ToolSource}; @@ -12,7 +12,7 @@ use xx::regex; #[derive(Debug, Clone, Eq, PartialEq)] pub struct ToolArg { pub short: String, - pub ba: BackendArg, + pub ba: Arc, pub version: Option, pub version_type: ToolVersionType, pub tvr: Option, @@ -34,21 +34,21 @@ impl FromStr for ToolArg { fn from_str(input: &str) -> eyre::Result { let (backend_input, version) = parse_input(input); - let backend: BackendArg = backend_input.into(); + let ba: Arc = Arc::new(backend_input.into()); let version_type = match version.as_ref() { Some(version) => version.parse()?, None => ToolVersionType::Version(String::from("latest")), }; let tvr = version .as_ref() - .map(|v| ToolRequest::new(backend.clone(), v, ToolSource::Argument)) + .map(|v| ToolRequest::new(ba.clone(), v, ToolSource::Argument)) .transpose()?; Ok(Self { - short: backend.short.clone(), + short: ba.short.clone(), tvr, version: version.map(|v| v.to_string()), version_type, - ba: backend, + ba, }) } } @@ -111,7 +111,7 @@ impl ToolArg { )?); tools[1].ba = a.ba; tools[1].version_type = b.ba.tool_name.parse()?; - tools[1].version = Some(b.ba.tool_name); + tools[1].version = Some(b.ba.tool_name.clone()); tools.remove(0); } } @@ -187,7 +187,7 @@ mod tests { tool, ToolArg { short: "node".into(), - ba: "node".into(), + ba: Arc::new("node".into()), version: None, version_type: ToolVersionType::Version("latest".into()), tvr: None, @@ -202,10 +202,12 @@ mod tests { tool, ToolArg { short: "node".into(), - ba: "node".into(), + ba: Arc::new("node".into()), version: Some("20".into()), version_type: ToolVersionType::Version("20".into()), - tvr: Some(ToolRequest::new("node".into(), "20", ToolSource::Argument).unwrap()), + tvr: Some( + ToolRequest::new(Arc::new("node".into()), "20", ToolSource::Argument).unwrap() + ), } ); } @@ -217,10 +219,12 @@ mod tests { tool, ToolArg { short: "node".into(), - ba: "node".into(), + ba: Arc::new("node".into()), version: Some("lts".into()), version_type: ToolVersionType::Version("lts".into()), - tvr: Some(ToolRequest::new("node".into(), "lts", ToolSource::Argument).unwrap()), + tvr: Some( + ToolRequest::new(Arc::new("node".into()), "lts", ToolSource::Argument).unwrap() + ), } ); } diff --git a/src/cli/asdf.rs b/src/cli/asdf.rs index 375b096eb5..8a9cda6f4a 100644 --- a/src/cli/asdf.rs +++ b/src/cli/asdf.rs @@ -17,37 +17,38 @@ pub struct Asdf { } impl Asdf { - pub fn run(mut self) -> Result<()> { - let config = Config::try_get()?; + pub async fn run(mut self) -> Result<()> { + let config = Config::get().await; let mut args = vec![String::from("mise")]; args.append(&mut self.args); match args.get(1).map(|s| s.as_str()) { - Some("reshim") => Cli::run(&args), - Some("list") => list_versions(&config, &args), + Some("reshim") => Box::pin(Cli::run(&args)).await, + Some("list") => list_versions(&config, &args).await, Some("install") => { if args.len() == 4 { let version = args.pop().unwrap(); args[2] = format!("{}@{}", args[2], version); } - Cli::run(&args) + Box::pin(Cli::run(&args)).await } - _ => Cli::run(&args), + _ => Box::pin(Cli::run(&args)).await, } } } -fn list_versions(config: &Config, args: &[String]) -> Result<()> { +async fn list_versions(config: &Config, args: &[String]) -> Result<()> { if args[2] == "all" { return LsRemote { prefix: None, all: false, plugin: args.get(3).map(|s| s.parse()).transpose()?, } - .run(); + .run() + .await; } - let ts = ToolsetBuilder::new().build(config)?; - let mut versions = ts.list_installed_versions()?; + let ts = ToolsetBuilder::new().build(config).await?; + let mut versions = ts.list_installed_versions().await?; let plugin = match args.len() { 3 => Some(&args[2]), _ => None, diff --git a/src/cli/backends/mod.rs b/src/cli/backends/mod.rs index f571ae66d9..566090ffa1 100644 --- a/src/cli/backends/mod.rs +++ b/src/cli/backends/mod.rs @@ -24,7 +24,7 @@ impl Commands { } impl Backends { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { let cmd = self.command.unwrap_or(Commands::Ls(ls::BackendsLs {})); cmd.run() diff --git a/src/cli/bin_paths.rs b/src/cli/bin_paths.rs index 832d8541bc..aaf0b23f24 100644 --- a/src/cli/bin_paths.rs +++ b/src/cli/bin_paths.rs @@ -14,18 +14,18 @@ pub struct BinPaths { } impl BinPaths { - pub fn run(self) -> Result<()> { - let config = Config::try_get()?; + pub async fn run(self) -> Result<()> { + let config = Config::get().await; let mut tsb = ToolsetBuilder::new(); if let Some(tool) = &self.tool { tsb = tsb.with_args(tool); } - let mut ts = tsb.build(&config)?; + let mut ts = tsb.build(&config).await?; if let Some(tool) = &self.tool { - ts.versions.retain(|k, _| tool.iter().any(|t| t.ba == *k)); + ts.versions.retain(|k, _| tool.iter().any(|t| *t.ba == **k)); } - ts.notify_if_versions_missing(); - for p in ts.list_paths() { + ts.notify_if_versions_missing().await; + for p in ts.list_paths().await { miseprintln!("{}", p.display()); } Ok(()) diff --git a/src/cli/completion.rs b/src/cli/completion.rs index ac6760ed9e..6448bbd652 100644 --- a/src/cli/completion.rs +++ b/src/cli/completion.rs @@ -36,10 +36,10 @@ pub struct Completion { } impl Completion { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { let shell = self.shell.or(self.shell_type).unwrap(); - let script = match self.call_usage(shell) { + let script = match self.call_usage(shell).await { Ok(script) => script, Err(e) => { debug!("usage command failed, falling back to prerendered completions"); @@ -52,8 +52,9 @@ impl Completion { Ok(()) } - fn call_usage(&self, shell: Shell) -> Result { - let toolset = ToolsetBuilder::new().build(&Config::get())?; + async fn call_usage(&self, shell: Shell) -> Result { + let config = Config::try_get().await?; + let toolset = ToolsetBuilder::new().build(&config).await?; let mut args = vec![ "generate".into(), "completion".into(), @@ -67,9 +68,9 @@ impl Completion { if self.include_bash_completion_lib { args.push("--include-bash-completion-lib".into()); } - let config = Config::get(); + let config = Config::get().await; let output = cmd("usage", args) - .full_env(toolset.full_env(&config)?) + .full_env(toolset.full_env(&config).await?) .read()?; Ok(output) } diff --git a/src/cli/config/generate.rs b/src/cli/config/generate.rs index d4fa7f0168..b83f27f847 100644 --- a/src/cli/config/generate.rs +++ b/src/cli/config/generate.rs @@ -38,7 +38,7 @@ impl ConfigGenerate { } fn tool_versions(&self, tool_versions: &Path) -> Result { - let mut to = config_file::parse_or_init(&PathBuf::from("mise.toml"))?; + let to = config_file::parse_or_init(&PathBuf::from("mise.toml"))?; let from = config_file::parse(tool_versions)?; let tools = from.to_tool_request_set()?.tools; for (ba, tools) in tools { diff --git a/src/cli/config/ls.rs b/src/cli/config/ls.rs index 07b420a500..9d73ae5161 100644 --- a/src/cli/config/ls.rs +++ b/src/cli/config/ls.rs @@ -25,19 +25,19 @@ pub struct ConfigLs { } impl ConfigLs { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { if self.tracked_configs { - self.display_tracked_configs()?; + self.display_tracked_configs().await?; } else if self.json { - self.display_json()?; + self.display_json().await?; } else { - self.display()?; + self.display().await?; } Ok(()) } - fn display(&self) -> Result<()> { - let config = Config::get(); + async fn display(&self) -> Result<()> { + let config = Config::get().await; let configs = config .config_files .values() @@ -60,8 +60,9 @@ impl ConfigLs { table.truncate(true).print() } - fn display_json(&self) -> Result<()> { + async fn display_json(&self) -> Result<()> { let array_items = Config::get() + .await .config_files .values() .map(|cf| { @@ -87,7 +88,7 @@ impl ConfigLs { Ok(()) } - fn display_tracked_configs(&self) -> Result<()> { + async fn display_tracked_configs(&self) -> Result<()> { let tracked_configs = Tracker::list_all()?.into_iter().unique().sorted(); for path in tracked_configs { println!("{}", path.display()); diff --git a/src/cli/config/mod.rs b/src/cli/config/mod.rs index ee10834c50..cef7f077ad 100644 --- a/src/cli/config/mod.rs +++ b/src/cli/config/mod.rs @@ -27,20 +27,20 @@ enum Commands { } impl Commands { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { match self { Self::Generate(cmd) => cmd.run(), Self::Get(cmd) => cmd.run(), - Self::Ls(cmd) => cmd.run(), + Self::Ls(cmd) => cmd.run().await, Self::Set(cmd) => cmd.run(), } } } impl Config { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { let cmd = self.command.unwrap_or(Commands::Ls(self.ls)); - cmd.run() + cmd.run().await } } diff --git a/src/cli/current.rs b/src/cli/current.rs index 39112429b9..cc46ca2f6b 100644 --- a/src/cli/current.rs +++ b/src/cli/current.rs @@ -20,9 +20,9 @@ pub struct Current { } impl Current { - pub fn run(self) -> Result<()> { - let config = Config::try_get()?; - let ts = ToolsetBuilder::new().build(&config)?; + pub async fn run(self) -> Result<()> { + let config = Config::get().await; + let ts = ToolsetBuilder::new().build(&config).await?; match &self.plugin { Some(ba) => { if let Some(plugin) = ba.backend()?.plugin() { @@ -30,13 +30,13 @@ impl Current { bail!("Plugin {ba} is not installed"); } } - self.one(ts, ba.backend()?.as_ref()) + self.one(ts, ba.backend()?.as_ref()).await } - None => self.all(ts), + None => self.all(ts).await, } } - fn one(&self, ts: Toolset, tool: &dyn Backend) -> Result<()> { + async fn one(&self, ts: Toolset, tool: &dyn Backend) -> Result<()> { if let Some(plugin) = tool.plugin() { if !plugin.is_installed() { warn!("Plugin {} is not installed", tool.id()); @@ -68,13 +68,14 @@ impl Current { Ok(()) } - fn all(&self, ts: Toolset) -> Result<()> { + async fn all(&self, ts: Toolset) -> Result<()> { + let config = Config::try_get().await?; for (plugin, versions) in ts.list_versions_by_plugin() { if versions.is_empty() { continue; } for tv in versions { - if !plugin.is_version_installed(tv, true) { + if !plugin.is_version_installed(&config, tv, true) { let source = ts.versions.get(tv.ba()).unwrap().source.clone(); warn!( "{}@{} is specified in {}, but not installed", diff --git a/src/cli/direnv/activate.rs b/src/cli/direnv/activate.rs index d8c0252f7e..e5a41f56b4 100644 --- a/src/cli/direnv/activate.rs +++ b/src/cli/direnv/activate.rs @@ -13,7 +13,7 @@ use indoc::indoc; pub struct DirenvActivate {} impl DirenvActivate { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { miseprintln!( // source_env "$(mise direnv envrc "$@")" indoc! {r#" diff --git a/src/cli/direnv/envrc.rs b/src/cli/direnv/envrc.rs index 16e2d99f27..b104ac7c7e 100644 --- a/src/cli/direnv/envrc.rs +++ b/src/cli/direnv/envrc.rs @@ -17,8 +17,8 @@ use crate::toolset::ToolsetBuilder; pub struct Envrc {} impl Envrc { - pub fn run(self, config: &Config) -> Result<()> { - let ts = ToolsetBuilder::new().build(config)?; + pub async fn run(self, config: &Config) -> Result<()> { + let ts = ToolsetBuilder::new().build(config).await?; let envrc_path = env::MISE_TMP_DIR .join("direnv") @@ -35,7 +35,7 @@ impl Envrc { for cf in config.config_files.keys() { writeln!(file, "watch_file {}", cf.to_string_lossy())?; } - let (env, env_results) = ts.final_env(config)?; + let (env, env_results) = ts.final_env(config).await?; for (k, v) in env { if k == *PATH_KEY { writeln!(file, "PATH_add {v}")?; @@ -48,7 +48,12 @@ impl Envrc { )?; } } - for path in ts.list_final_paths(config, env_results)?.into_iter().rev() { + for path in ts + .list_final_paths(config, env_results) + .await? + .into_iter() + .rev() + { writeln!(file, "PATH_add {}", path.to_string_lossy())?; } diff --git a/src/cli/direnv/exec.rs b/src/cli/direnv/exec.rs index 9bba196623..3e1c21cc35 100644 --- a/src/cli/direnv/exec.rs +++ b/src/cli/direnv/exec.rs @@ -18,12 +18,12 @@ struct DirenvWatches { } impl DirenvExec { - pub fn run(self, config: &Config) -> Result<()> { - let ts = ToolsetBuilder::new().build(config)?; + pub async fn run(self, config: &Config) -> Result<()> { + let ts = ToolsetBuilder::new().build(config).await?; let mut cmd = env_cmd(); - for (k, v) in ts.env_with_path(config)? { + for (k, v) in ts.env_with_path(config).await? { cmd = cmd.env(k, v); } diff --git a/src/cli/direnv/mod.rs b/src/cli/direnv/mod.rs index 6c279ae053..0b968795c4 100644 --- a/src/cli/direnv/mod.rs +++ b/src/cli/direnv/mod.rs @@ -29,21 +29,21 @@ enum Commands { } impl Commands { - pub fn run(self, config: &Config) -> Result<()> { + pub async fn run(self, config: &Config) -> Result<()> { match self { - Self::Activate(cmd) => cmd.run(), - Self::Envrc(cmd) => cmd.run(config), - Self::Exec(cmd) => cmd.run(config), + Self::Activate(cmd) => cmd.run().await, + Self::Envrc(cmd) => cmd.run(config).await, + Self::Exec(cmd) => cmd.run(config).await, } } } impl Direnv { - pub fn run(self) -> Result<()> { - let config = Config::try_get()?; + pub async fn run(self) -> Result<()> { + let config = Config::get().await; let cmd = self .command .unwrap_or(Commands::Activate(activate::DirenvActivate {})); - cmd.run(&config) + cmd.run(&config).await } } diff --git a/src/cli/doctor/mod.rs b/src/cli/doctor/mod.rs index c70fef8b56..eb6f5104d6 100644 --- a/src/cli/doctor/mod.rs +++ b/src/cli/doctor/mod.rs @@ -1,6 +1,6 @@ mod path; -use crate::exit; +use crate::{exit, plugins::PluginEnum}; use std::collections::BTreeMap; use crate::backend::backend_type::BackendType; @@ -21,7 +21,6 @@ use heck::ToSnakeCase; use indexmap::IndexMap; use indoc::formatdoc; use itertools::Itertools; -use rayon::prelude::*; use std::env::split_paths; use std::path::{Path, PathBuf}; use strum::IntoEnumIterator; @@ -46,19 +45,19 @@ pub enum Commands { } impl Doctor { - pub fn run(self) -> eyre::Result<()> { + pub async fn run(self) -> eyre::Result<()> { if let Some(cmd) = self.subcommand { match cmd { - Commands::Path(cmd) => cmd.run(), + Commands::Path(cmd) => cmd.run().await, } } else if self.json { - self.doctor_json() + self.doctor_json().await } else { - self.doctor() + self.doctor().await } } - fn doctor_json(mut self) -> crate::Result<()> { + async fn doctor_json(mut self) -> crate::Result<()> { let mut data: BTreeMap = BTreeMap::new(); data.insert( "version".into(), @@ -99,13 +98,14 @@ impl Doctor { serde_json::from_str(&cmd!(&*env::MISE_BIN, "settings", "-J").read()?)?, ); - let config = Config::get(); - let ts = config.get_toolset()?; - self.analyze_shims(ts); + let config = Config::get().await; + let ts = config.get_toolset().await?; + self.analyze_shims(ts).await; self.analyze_plugins(); data.insert( "paths".into(), - self.paths(ts)? + self.paths(ts) + .await? .into_iter() .map(|p| p.to_string_lossy().to_string()) .collect(), @@ -131,7 +131,7 @@ impl Doctor { .iter() .map(|tv: &ToolVersion| { let mut tool = serde_json::Map::new(); - match f.is_version_installed(tv, true) { + match f.is_version_installed(&config, tv, true) { true => { tool.insert("version".into(), tv.version.to_string().into()); } @@ -169,7 +169,7 @@ impl Doctor { Ok(()) } - fn doctor(mut self) -> eyre::Result<()> { + async fn doctor(mut self) -> eyre::Result<()> { info::inline_section("version", &*VERSION)?; #[cfg(unix)] info::inline_section("activated", yn(env::is_activated()))?; @@ -190,8 +190,8 @@ impl Doctor { .join("\n"); info::section("dirs", mise_dirs)?; - match Config::try_get() { - Ok(config) => self.analyze_config(config)?, + match Config::try_get().await { + Ok(config) => self.analyze_config(config).await?, Err(err) => self.errors.push(format!("failed to load config: {err}")), } @@ -208,8 +208,8 @@ impl Doctor { } self.analyze_settings()?; - if let Some(latest) = version::check_for_new_version(duration::HOURLY) { - version::show_latest(); + if let Some(latest) = version::check_for_new_version(duration::HOURLY).await { + version::show_latest().await; self.errors.push(format!( "new mise version {latest} available, currently on {}", *version::V @@ -254,7 +254,7 @@ impl Doctor { } Ok(()) } - fn analyze_config(&mut self, config: impl AsRef) -> eyre::Result<()> { + async fn analyze_config(&mut self, config: impl AsRef) -> eyre::Result<()> { let config = config.as_ref(); info::section("config_files", render_config_files(config))?; @@ -299,11 +299,11 @@ impl Doctor { } } - match ToolsetBuilder::new().build(config) { + match ToolsetBuilder::new().build(config).await { Ok(ts) => { - self.analyze_shims(&ts); - self.analyze_toolset(&ts)?; - self.analyze_paths(&ts)?; + self.analyze_shims(&ts).await; + self.analyze_toolset(&ts).await?; + self.analyze_paths(&ts).await?; } Err(err) => self.errors.push(format!("failed to load toolset: {err}")), } @@ -311,11 +311,12 @@ impl Doctor { Ok(()) } - fn analyze_toolset(&mut self, ts: &Toolset) -> eyre::Result<()> { + async fn analyze_toolset(&mut self, ts: &Toolset) -> eyre::Result<()> { + let config = Config::try_get().await?; let tools = ts .list_current_versions() .into_iter() - .map(|(f, tv)| match f.is_version_installed(&tv, true) { + .map(|(f, tv)| match f.is_version_installed(&config, &tv, true) { true => (tv.to_string(), style::nstyle("")), false => { self.errors.push(format!( @@ -342,10 +343,10 @@ impl Doctor { Ok(()) } - fn analyze_shims(&mut self, toolset: &Toolset) { + async fn analyze_shims(&mut self, toolset: &Toolset) { let mise_bin = file::which("mise").unwrap_or(env::MISE_BIN.clone()); - if let Ok((missing, extra)) = shims::get_shim_diffs(mise_bin, toolset) { + if let Ok((missing, extra)) = shims::get_shim_diffs(mise_bin, toolset).await { let cmd = style::nyellow("mise reshim"); if !missing.is_empty() { @@ -379,17 +380,22 @@ impl Doctor { } } - fn paths(&mut self, ts: &Toolset) -> eyre::Result> { - let config = Config::get(); - let env = ts.full_env(&config)?; + async fn paths(&mut self, ts: &Toolset) -> eyre::Result> { + let config = Config::get().await; + let env = ts.full_env(&config).await?; let path = env .get(&*PATH_KEY) .ok_or_else(|| eyre::eyre!("Path not found"))?; Ok(split_paths(path).collect()) } - fn analyze_paths(&mut self, ts: &Toolset) -> eyre::Result<()> { - let paths = self.paths(ts)?.into_iter().map(display_path).join("\n"); + async fn analyze_paths(&mut self, ts: &Toolset) -> eyre::Result<()> { + let paths = self + .paths(ts) + .await? + .into_iter() + .map(display_path) + .join("\n"); info::section("path", paths)?; Ok(()) @@ -473,13 +479,13 @@ fn render_plugins() -> String { .unwrap_or(0) .min(40); plugins - .into_par_iter() + .into_iter() .filter(|b| b.plugin().is_some()) .map(|p| { let p = p.plugin().unwrap(); let padded_name = pad_str(p.name(), max_plugin_name_len, Alignment::Left, None); - let extra = match p.get_plugin_type() { - PluginType::Asdf | PluginType::Vfox => { + let extra = match p { + PluginEnum::Asdf(_) | PluginEnum::Vfox(_) => { let git = Git::new(dirs::PLUGINS.join(p.name())); match git.get_remote_url() { Some(url) => { diff --git a/src/cli/doctor/path.rs b/src/cli/doctor/path.rs index bc2294fa55..844ca2ed01 100644 --- a/src/cli/doctor/path.rs +++ b/src/cli/doctor/path.rs @@ -12,16 +12,16 @@ pub struct Path { } impl Path { - pub fn run(self) -> Result<()> { - let config = Config::get(); - let ts = config.get_toolset()?; + pub async fn run(self) -> Result<()> { + let config = Config::get().await; + let ts = config.get_toolset().await?; let paths = if self.full { - let env = ts.env_with_path(&config)?; + let env = ts.env_with_path(&config).await?; let path = env.get("PATH").cloned().unwrap_or_default(); env::split_paths(&path).collect() } else { - let (_env, env_results) = ts.final_env(&config)?; - ts.list_final_paths(&config, env_results)? + let (_env, env_results) = ts.final_env(&config).await?; + ts.list_final_paths(&config, env_results).await? }; for path in paths { println!("{}", path.display()); diff --git a/src/cli/en.rs b/src/cli/en.rs index 7282ec0508..2119bc0790 100644 --- a/src/cli/en.rs +++ b/src/cli/en.rs @@ -24,7 +24,7 @@ pub struct En { } impl En { - pub fn run(self) -> eyre::Result<()> { + pub async fn run(self) -> eyre::Result<()> { let settings = Settings::get(); settings.ensure_experimental("en")?; @@ -40,6 +40,7 @@ impl En { command: Some(command), } .run() + .await } } diff --git a/src/cli/env.rs b/src/cli/env.rs index 1fa29d1149..c794cbb674 100644 --- a/src/cli/env.rs +++ b/src/cli/env.rs @@ -35,37 +35,41 @@ pub struct Env { } impl Env { - pub fn run(self) -> Result<()> { - let config = Config::try_get()?; - let mut ts = ToolsetBuilder::new().with_args(&self.tool).build(&config)?; - ts.install_missing_versions(&InstallOptions::default())?; - ts.notify_if_versions_missing(); + pub async fn run(self) -> Result<()> { + let config = Config::get().await; + let mut ts = ToolsetBuilder::new() + .with_args(&self.tool) + .build(&config) + .await?; + ts.install_missing_versions(&config, &InstallOptions::default()) + .await?; + ts.notify_if_versions_missing().await; if self.json { - self.output_json(&config, ts) + self.output_json(&config, ts).await } else if self.json_extended { - self.output_extended_json(&config, ts) + self.output_extended_json(&config, ts).await } else if self.dotenv { - self.output_dotenv(&config, ts) + self.output_dotenv(&config, ts).await } else { - self.output_shell(&config, ts) + self.output_shell(&config, ts).await } } - fn output_json(&self, config: &Config, ts: Toolset) -> Result<()> { - let env = ts.env_with_path(config)?; + async fn output_json(&self, config: &Config, ts: Toolset) -> Result<()> { + let env = ts.env_with_path(config).await?; miseprintln!("{}", serde_json::to_string_pretty(&env)?); Ok(()) } - fn output_extended_json(&self, config: &Config, ts: Toolset) -> Result<()> { + async fn output_extended_json(&self, config: &Config, ts: Toolset) -> Result<()> { let mut res = BTreeMap::new(); - ts.env_with_path(config)?.iter().for_each(|(k, v)| { + ts.env_with_path(config).await?.iter().for_each(|(k, v)| { res.insert(k.to_string(), BTreeMap::from([("value", v.to_string())])); }); - config.env_with_sources()?.iter().for_each(|(k, v)| { + config.env_with_sources().await?.iter().for_each(|(k, v)| { res.insert( k.to_string(), BTreeMap::from([ @@ -76,7 +80,8 @@ impl Env { }); let tool_map: BTreeMap = ts - .list_all_versions()? + .list_all_versions() + .await? .into_iter() .map(|(b, tv)| { ( @@ -90,7 +95,8 @@ impl Env { }) .collect(); - ts.env_from_tools(config) + ts.env_from_tools() + .await .iter() .for_each(|(name, value, tool_id)| { res.insert( @@ -113,10 +119,10 @@ impl Env { Ok(()) } - fn output_shell(&self, config: &Config, ts: Toolset) -> Result<()> { + async fn output_shell(&self, config: &Config, ts: Toolset) -> Result<()> { let default_shell = get_shell(Some(ShellType::Bash)).unwrap(); let shell = get_shell(self.shell).unwrap_or(default_shell); - for (k, v) in ts.env_with_path(config)? { + for (k, v) in ts.env_with_path(config).await? { let k = k.to_string(); let v = v.to_string(); miseprint!("{}", shell.set_env(&k, &v))?; @@ -124,8 +130,8 @@ impl Env { Ok(()) } - fn output_dotenv(&self, config: &Config, ts: Toolset) -> Result<()> { - let (env, _) = ts.final_env(config)?; + async fn output_dotenv(&self, config: &Config, ts: Toolset) -> Result<()> { + let (env, _) = ts.final_env(config).await?; for (k, v) in env { let k = k.to_string(); let v = v.to_string(); diff --git a/src/cli/exec.rs b/src/cli/exec.rs index 809cf6af81..a6f335c980 100644 --- a/src/cli/exec.rs +++ b/src/cli/exec.rs @@ -52,13 +52,14 @@ pub struct Exec { } impl Exec { - pub fn run(self) -> Result<()> { - let config = Config::get(); + pub async fn run(self) -> Result<()> { + let config = Config::get().await; let mut ts = measure!("toolset", { ToolsetBuilder::new() .with_args(&self.tool) .with_default_to_latest(true) - .build(&config)? + .build(&config) + .await? }); let opts = InstallOptions { force: false, @@ -75,14 +76,14 @@ impl Exec { ..Default::default() }; measure!("install_arg_versions", { - ts.install_missing_versions(&opts)? + ts.install_missing_versions(&config, &opts).await? }); measure!("notify_if_versions_missing", { - ts.notify_if_versions_missing() + ts.notify_if_versions_missing().await; }); let (program, mut args) = parse_command(&env::SHELL, &self.command, &self.c); - let env = measure!("env_with_path", { ts.env_with_path(&config)? }); + let env = measure!("env_with_path", { ts.env_with_path(&config).await? }); if program.rsplit('/').next() == Some("fish") { let mut cmd = vec![]; @@ -94,8 +95,8 @@ impl Exec { )); } // TODO: env is being calculated twice with final_env and env_with_path - let (_, env_results) = ts.final_env(&config)?; - for p in ts.list_final_paths(&config, env_results)? { + let (_, env_results) = ts.final_env(&config).await?; + for p in ts.list_final_paths(&config, env_results).await? { cmd.push(format!( "fish_add_path -gm {}", shell_escape::escape(p.to_string_lossy()) diff --git a/src/cli/external.rs b/src/cli/external.rs index ca5d5f35a3..acf16123cb 100644 --- a/src/cli/external.rs +++ b/src/cli/external.rs @@ -1,6 +1,5 @@ use clap::Command; use eyre::Result; -use rayon::prelude::*; use std::collections::HashMap; use std::sync::LazyLock as Lazy; @@ -9,7 +8,7 @@ use crate::cli::args::BackendArg; pub static COMMANDS: Lazy> = Lazy::new(|| { backend::list() - .into_par_iter() + .into_iter() .flat_map(|b| { if let Some(p) = b.plugin() { return p.external_commands().unwrap_or_else(|e| { diff --git a/src/cli/generate/bootstrap.rs b/src/cli/generate/bootstrap.rs index 4e0e1523a3..12329dacd2 100644 --- a/src/cli/generate/bootstrap.rs +++ b/src/cli/generate/bootstrap.rs @@ -30,9 +30,9 @@ pub struct Bootstrap { } impl Bootstrap { - pub fn run(self) -> eyre::Result<()> { + pub async fn run(self) -> eyre::Result<()> { SETTINGS.ensure_experimental("generate bootstrap")?; - let output = self.generate()?; + let output = self.generate().await?; if let Some(bin) = &self.write { if let Some(parent) = bin.parent() { file::create_dir_all(parent)?; @@ -46,14 +46,14 @@ impl Bootstrap { Ok(()) } - fn generate(&self) -> Result { + async fn generate(&self) -> Result { let url = if let Some(v) = &self.version { format!("https://mise.jdx.dev/v{v}/install.sh") } else { "https://mise.jdx.dev/install.sh".into() }; - let install = HTTP.get_text(&url)?; - let install_sig = HTTP.get_text(format!("{url}.minisig"))?; + let install = HTTP.get_text(&url).await?; + let install_sig = HTTP.get_text(format!("{url}.minisig")).await?; minisign::verify(&minisign::MISE_PUB_KEY, install.as_bytes(), &install_sig)?; let install = info::indent_by(install, " "); let version = regex!(r#"version="\$\{MISE_VERSION:-v([0-9.]+)\}""#) diff --git a/src/cli/generate/config.rs b/src/cli/generate/config.rs index 53aad83d5c..16b161323b 100644 --- a/src/cli/generate/config.rs +++ b/src/cli/generate/config.rs @@ -10,7 +10,7 @@ pub struct Config { } impl Config { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { self.generate.run() } } diff --git a/src/cli/generate/devcontainer.rs b/src/cli/generate/devcontainer.rs index 4360b47102..2ad84df284 100644 --- a/src/cli/generate/devcontainer.rs +++ b/src/cli/generate/devcontainer.rs @@ -53,7 +53,7 @@ struct DevcontainerMount { } impl Devcontainer { - pub fn run(self) -> eyre::Result<()> { + pub async fn run(self) -> eyre::Result<()> { SETTINGS.ensure_experimental("generate devcontainer")?; let output = self.generate()?; diff --git a/src/cli/generate/git_pre_commit.rs b/src/cli/generate/git_pre_commit.rs index 7369cea178..4265583766 100644 --- a/src/cli/generate/git_pre_commit.rs +++ b/src/cli/generate/git_pre_commit.rs @@ -27,7 +27,7 @@ pub struct GitPreCommit { } impl GitPreCommit { - pub fn run(self) -> eyre::Result<()> { + pub async fn run(self) -> eyre::Result<()> { let settings = Settings::get(); settings.ensure_experimental("generate git-pre-commit")?; let output = self.generate(); diff --git a/src/cli/generate/github_action.rs b/src/cli/generate/github_action.rs index ae17cd4f7c..aee4f07fc0 100644 --- a/src/cli/generate/github_action.rs +++ b/src/cli/generate/github_action.rs @@ -23,7 +23,7 @@ pub struct GithubAction { } impl GithubAction { - pub fn run(self) -> eyre::Result<()> { + pub async fn run(self) -> eyre::Result<()> { let settings = Settings::get(); settings.ensure_experimental("generate github-action")?; let output = self.generate()?; diff --git a/src/cli/generate/mod.rs b/src/cli/generate/mod.rs index 7e06bad059..07a9af600f 100644 --- a/src/cli/generate/mod.rs +++ b/src/cli/generate/mod.rs @@ -28,21 +28,21 @@ enum Commands { } impl Commands { - pub fn run(self) -> eyre::Result<()> { + pub async fn run(self) -> eyre::Result<()> { match self { - Self::Bootstrap(cmd) => cmd.run(), - Self::Config(cmd) => cmd.run(), - Self::Devcontainer(cmd) => cmd.run(), - Self::GitPreCommit(cmd) => cmd.run(), - Self::GithubAction(cmd) => cmd.run(), - Self::TaskDocs(cmd) => cmd.run(), - Self::TaskStubs(cmd) => cmd.run(), + Self::Bootstrap(cmd) => cmd.run().await, + Self::Config(cmd) => cmd.run().await, + Self::Devcontainer(cmd) => cmd.run().await, + Self::GitPreCommit(cmd) => cmd.run().await, + Self::GithubAction(cmd) => cmd.run().await, + Self::TaskDocs(cmd) => cmd.run().await, + Self::TaskStubs(cmd) => cmd.run().await, } } } impl Generate { - pub fn run(self) -> eyre::Result<()> { - self.command.run() + pub async fn run(self) -> eyre::Result<()> { + self.command.run().await } } diff --git a/src/cli/generate/task_docs.rs b/src/cli/generate/task_docs.rs index f253deb1b8..cf3fa9dfe1 100644 --- a/src/cli/generate/task_docs.rs +++ b/src/cli/generate/task_docs.rs @@ -2,6 +2,8 @@ use crate::config::Config; use crate::{dirs, file}; use std::path::PathBuf; +use crate::config; + /// Generate documentation for tasks in a project #[derive(Debug, clap::Args)] #[clap(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)] @@ -39,21 +41,21 @@ enum TaskDocsStyle { } impl TaskDocs { - pub fn run(self) -> eyre::Result<()> { - let config = Config::get(); - let ts = config.get_toolset()?; + pub async fn run(self) -> eyre::Result<()> { + let config = Config::get().await; + let ts = config.get_toolset().await?; let dir = dirs::CWD.as_ref().unwrap(); - let tasks = config.load_tasks_in_dir(dir)?; + let tasks = config::load_tasks_in_dir(dir, &config.config_files).await?; let mut out = vec![]; for task in tasks.iter().filter(|t| !t.hide) { - out.push(task.render_markdown(ts, dir)?); + out.push(task.render_markdown(ts, dir).await?); } if let Some(output) = &self.output { if self.multi { if output.is_dir() { for (i, task) in tasks.iter().filter(|t| !t.hide).enumerate() { let path = output.join(format!("{i}.md")); - file::write(&path, &task.render_markdown(ts, dir)?)?; + file::write(&path, &task.render_markdown(ts, dir).await?)?; } } else { return Err(eyre::eyre!( diff --git a/src/cli/generate/task_stubs.rs b/src/cli/generate/task_stubs.rs index eb12b8e483..5dee98a4a0 100644 --- a/src/cli/generate/task_stubs.rs +++ b/src/cli/generate/task_stubs.rs @@ -25,10 +25,10 @@ pub struct TaskStubs { } impl TaskStubs { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { SETTINGS.ensure_experimental("generate task-stubs")?; - let config = Config::get(); - for task in config.tasks()?.values() { + let config = Config::get().await; + for task in config.tasks().await?.values() { let bin = self.dir.join(task.name_to_path()); let output = self.generate(task)?; if let Some(parent) = bin.parent() { @@ -44,7 +44,7 @@ impl TaskStubs { fn generate(&self, task: &Task) -> Result { let mise_bin = self.mise_bin.to_string_lossy(); let mise_bin = shell_words::quote(&mise_bin); - let display_name = task.display_name(); + let display_name = &task.display_name; let script = format!( r#" #!/bin/sh diff --git a/src/cli/global.rs b/src/cli/global.rs index 86e78a644e..003c76f638 100644 --- a/src/cli/global.rs +++ b/src/cli/global.rs @@ -45,7 +45,7 @@ pub struct Global { } impl Global { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { let settings = Settings::try_get()?; local( &settings.global_tools_file(), @@ -55,6 +55,7 @@ impl Global { self.fuzzy, self.path, ) + .await } } diff --git a/src/cli/hook_env.rs b/src/cli/hook_env.rs index 28855441c8..fb790e3ef1 100644 --- a/src/cli/hook_env.rs +++ b/src/cli/hook_env.rs @@ -39,45 +39,48 @@ pub struct HookEnv { } impl HookEnv { - pub fn run(self) -> Result<()> { - let config = Config::try_get()?; - let watch_files = config.watch_files()?; + pub async fn run(self) -> Result<()> { + let config = Config::get().await; + let watch_files = config.watch_files().await?; time!("hook-env"); if !self.force && hook_env::should_exit_early(watch_files.clone()) { trace!("should_exit_early true"); return Ok(()); } time!("should_exit_early false"); - let ts = config.get_toolset()?; + let ts = config.get_toolset().await?; let shell = get_shell(self.shell).expect("no shell provided, use `--shell=zsh`"); miseprint!("{}", hook_env::clear_old_env(&*shell))?; - let (mut mise_env, env_results) = ts.final_env(&config)?; + let (mut mise_env, env_results) = ts.final_env(&config).await?; mise_env.remove(&*PATH_KEY); - self.display_status(&config, ts, &mise_env)?; + self.display_status(&config, ts, &mise_env).await?; let mut diff = EnvDiff::new(&env::PRISTINE_ENV, mise_env.clone()); let mut patches = diff.to_patches(); - let paths = ts.list_final_paths(&config, env_results)?; + let paths = ts.list_final_paths(&config, env_results).await?; diff.path.clone_from(&paths); // update __MISE_DIFF with the new paths for the next run patches.extend(self.build_path_operations(&paths, &__MISE_DIFF.path)?); patches.push(self.build_diff_operation(&diff)?); - patches.push(self.build_session_operation(ts, mise_env, watch_files)?); + patches.push( + self.build_session_operation(ts, mise_env, watch_files) + .await?, + ); let output = hook_env::build_env_commands(&*shell, &patches); miseprint!("{output}")?; - hooks::run_all_hooks(ts, &*shell); - watch_files::execute_runs(ts); + hooks::run_all_hooks(ts, &*shell).await; + watch_files::execute_runs(ts).await; Ok(()) } - fn display_status(&self, config: &Config, ts: &Toolset, cur_env: &EnvMap) -> Result<()> { + async fn display_status(&self, config: &Config, ts: &Toolset, cur_env: &EnvMap) -> Result<()> { if self.status || SETTINGS.status.show_tools { let prev = &PREV_SESSION.loaded_tools; let cur = ts - .list_current_installed_versions() + .list_current_installed_versions(config) .into_iter() .rev() .map(|(_, tv)| format!("{}@{}", tv.short(), tv.version)) @@ -110,6 +113,7 @@ impl HookEnv { } let new_paths: IndexSet = config .path_dirs() + .await .map(|p| p.iter().cloned().collect()) .unwrap_or_default(); let old_paths = &PREV_SESSION.config_paths; @@ -130,7 +134,7 @@ impl HookEnv { info!("{}", format_status(&status)); } } - ts.notify_if_versions_missing(); + ts.notify_if_versions_missing().await; Ok(()) } @@ -203,7 +207,7 @@ impl HookEnv { )) } - fn build_session_operation( + async fn build_session_operation( &self, ts: &Toolset, env: EnvMap, @@ -217,7 +221,7 @@ impl HookEnv { } else { Default::default() }; - let session = hook_env::build_session(env, loaded_tools, watch_files)?; + let session = hook_env::build_session(env, loaded_tools, watch_files).await?; Ok(EnvDiffOperation::Add( "__MISE_SESSION".into(), hook_env::serialize(&session)?, diff --git a/src/cli/hook_not_found.rs b/src/cli/hook_not_found.rs index d881ae2e48..8af20eb409 100644 --- a/src/cli/hook_not_found.rs +++ b/src/cli/hook_not_found.rs @@ -20,12 +20,12 @@ pub struct HookNotFound { } impl HookNotFound { - pub fn run(self) -> Result<()> { - let config = Config::try_get()?; + pub async fn run(self) -> Result<()> { + let config = Config::get().await; let settings = Settings::try_get()?; if settings.not_found_auto_install { - let mut ts = ToolsetBuilder::new().build(&config)?; - if ts.install_missing_bin(&self.bin)?.is_some() { + let mut ts = ToolsetBuilder::new().build(&config).await?; + if ts.install_missing_bin(&config, &self.bin).await?.is_some() { return Ok(()); } } diff --git a/src/cli/install.rs b/src/cli/install.rs index 4dea5260eb..e515abcea4 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -1,8 +1,9 @@ +use std::sync::Arc; + use crate::cli::args::ToolArg; use crate::config::Config; use crate::hooks::Hooks; use crate::toolset::{InstallOptions, ResolveOptions, ToolRequest, ToolSource, Toolset}; -use crate::ui::multi_progress_report::MultiProgressReport; use crate::{config, env, hooks}; use eyre::Result; use itertools::Itertools; @@ -46,31 +47,35 @@ pub struct Install { } impl Install { - pub fn run(self) -> Result<()> { - let config = Config::try_get()?; + pub async fn run(self) -> Result<()> { + let config = Config::get().await; match &self.tool { Some(runtime) => { env::TOOL_ARGS.write().unwrap().clone_from(runtime); - self.install_runtimes(&config, runtime)? + self.install_runtimes(&config, runtime).await? } - None => self.install_missing_runtimes(&config)?, + None => self.install_missing_runtimes(&config).await?, }; Ok(()) } - fn install_runtimes(&self, config: &Config, runtimes: &[ToolArg]) -> Result<()> { - let mpr = MultiProgressReport::get(); + async fn install_runtimes(&self, config: &Arc, runtimes: &[ToolArg]) -> Result<()> { let tools = runtimes.iter().map(|ta| ta.ba.short.clone()).collect(); - let mut ts = config.get_tool_request_set()?.filter_by_tool(tools).into(); + let mut ts = config + .get_tool_request_set() + .await? + .filter_by_tool(tools) + .into(); let tool_versions = self.get_requested_tool_versions(&ts, runtimes)?; let versions = if tool_versions.is_empty() { warn!("no runtimes to install"); warn!("specify a version with `mise install @`"); vec![] } else { - ts.install_all_versions(tool_versions, &mpr, &self.install_opts())? + ts.install_all_versions(config, tool_versions, &self.install_opts()) + .await? }; - config::rebuild_shims_and_runtime_symlinks(&versions)?; + config::rebuild_shims_and_runtime_symlinks(&versions).await?; Ok(()) } @@ -100,7 +105,7 @@ impl Install { Some(tv) => requests.push(tv), None => { if ta.tvr.is_none() { - match ts.versions.get(&ta.ba) { + match ts.versions.get(ta.ba.as_ref()) { // the tool is in config so fetch the params from config // this may match multiple versions of one tool (e.g.: python) Some(tvl) => { @@ -127,19 +132,29 @@ impl Install { Ok(requests) } - fn install_missing_runtimes(&self, config: &Config) -> eyre::Result<()> { - let trs = config.get_tool_request_set()?; - let versions = trs.missing_tools().into_iter().cloned().collect_vec(); + async fn install_missing_runtimes(&self, config: &Arc) -> eyre::Result<()> { + let trs = measure!("get_tool_request_set", { + config.get_tool_request_set().await? + }); + let versions = measure!("fetching missing runtims", { + trs.missing_tools().await.into_iter().cloned().collect_vec() + }); let versions = if versions.is_empty() { - info!("all tools are installed"); - hooks::run_one_hook(config.get_toolset()?, Hooks::Postinstall, None); - vec![] + measure!("run_postinstall_hook", { + info!("all tools are installed"); + hooks::run_one_hook(config.get_toolset().await?, Hooks::Postinstall, None).await; + vec![] + }) } else { - let mpr = MultiProgressReport::get(); let mut ts = Toolset::from(trs.clone()); - ts.install_all_versions(versions, &mpr, &self.install_opts())? + measure!("install_all_versions", { + ts.install_all_versions(config, versions, &self.install_opts()) + .await? + }) }; - config::rebuild_shims_and_runtime_symlinks(&versions)?; + measure!("rebuild_shims_and_runtime_symlinks", { + config::rebuild_shims_and_runtime_symlinks(&versions).await?; + }); Ok(()) } } diff --git a/src/cli/install_into.rs b/src/cli/install_into.rs index 605eec41cb..247fddf9d0 100644 --- a/src/cli/install_into.rs +++ b/src/cli/install_into.rs @@ -5,7 +5,7 @@ use crate::toolset::ToolsetBuilder; use crate::ui::multi_progress_report::MultiProgressReport; use clap::ValueHint; use eyre::{Result, eyre}; -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; /// Install a tool version to a specific path /// @@ -24,14 +24,17 @@ pub struct InstallInto { } impl InstallInto { - pub fn run(self) -> Result<()> { - let config = Config::get(); - let ts = ToolsetBuilder::new() - .with_args(&[self.tool.clone()]) - .build(&config)?; + pub async fn run(self) -> Result<()> { + let config = Config::get().await; + let ts = Arc::new( + ToolsetBuilder::new() + .with_args(&[self.tool.clone()]) + .build(&config) + .await?, + ); let mut tv = ts .versions - .get(&self.tool.ba) + .get(self.tool.ba.as_ref()) .ok_or_else(|| eyre!("Tool not found"))? .versions .first() @@ -40,12 +43,12 @@ impl InstallInto { let backend = tv.backend()?; let mpr = MultiProgressReport::get(); let install_ctx = InstallContext { - ts: &ts, + ts: ts.clone(), pr: mpr.add(&tv.style()), force: true, }; tv.install_path = Some(self.path.clone()); - backend.install_version(install_ctx, tv)?; + backend.install_version(install_ctx, tv).await?; Ok(()) } } diff --git a/src/cli/latest.rs b/src/cli/latest.rs index 583bf69718..48bbe43cf9 100644 --- a/src/cli/latest.rs +++ b/src/cli/latest.rs @@ -27,8 +27,8 @@ pub struct Latest { } impl Latest { - pub fn run(self) -> Result<()> { - let config = Config::try_get()?; + pub async fn run(self) -> Result<()> { + let config = Config::get().await; let mut prefix = match self.tool.tvr { None => self.asdf_version, Some(ToolRequest::Version { version, .. }) => Some(version), @@ -38,16 +38,16 @@ impl Latest { let backend = self.tool.ba.backend()?; let mpr = MultiProgressReport::get(); if let Some(plugin) = backend.plugin() { - plugin.ensure_installed(&mpr, false)?; + plugin.ensure_installed(&mpr, false).await?; } if let Some(v) = prefix { - prefix = Some(config.resolve_alias(&backend, &v)?); + prefix = Some(config.resolve_alias(&backend, &v).await?); } let latest_version = if self.installed { backend.latest_installed_version(prefix)? } else { - backend.latest_version(prefix)? + backend.latest_version(prefix).await? }; if let Some(version) = latest_version { miseprintln!("{}", version); diff --git a/src/cli/link.rs b/src/cli/link.rs index 84358f545a..35da9f306d 100644 --- a/src/cli/link.rs +++ b/src/cli/link.rs @@ -31,7 +31,7 @@ pub struct Link { } impl Link { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { let version = match self.tool.tvr { Some(ref tvr) => tvr.version(), None => bail!("must provide a version for {}", self.tool.style()), @@ -58,7 +58,7 @@ impl Link { file::create_dir_all(target.parent().unwrap())?; make_symlink(&path, &target)?; - config::rebuild_shims_and_runtime_symlinks(&[]) + config::rebuild_shims_and_runtime_symlinks(&[]).await } } diff --git a/src/cli/local.rs b/src/cli/local.rs index 7efd89e973..c23fd7cc3e 100644 --- a/src/cli/local.rs +++ b/src/cli/local.rs @@ -52,7 +52,7 @@ pub struct Local { } impl Local { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { let path = if self.parent { get_parent_path()? } else { @@ -66,6 +66,7 @@ impl Local { self.fuzzy, self.path, ) + .await } } @@ -94,7 +95,7 @@ pub fn get_parent_path() -> Result { } #[allow(clippy::too_many_arguments)] -pub fn local( +pub async fn local( path: &Path, runtime: Vec, remove: Option>, @@ -107,7 +108,7 @@ pub fn local( "mise local/global are deprecated. Use `mise use` instead." ); let settings = Settings::try_get()?; - let mut cf = config_file::parse_or_init(path)?; + let cf = config_file::parse_or_init(path)?; if show_path { miseprintln!("{}", path.display()); return Ok(()); @@ -130,12 +131,13 @@ pub fn local( return Ok(()); } let pin = pin || (settings.asdf_compat && !fuzzy); - cf.add_runtimes(&runtimes, pin)?; + cf.add_runtimes(&runtimes, pin).await?; let tools = runtimes.iter().map(|t| t.style()).join(" "); miseprintln!("{} {} {tools}", style("mise").dim(), display_path(path)); } if !runtime.is_empty() || remove.is_some() { + trace!("saving config file {}", display_path(path)); cf.save()?; } else { miseprint!("{}", cf.dump()?)?; diff --git a/src/cli/ls.rs b/src/cli/ls.rs index 296ea00b21..8d833a541f 100644 --- a/src/cli/ls.rs +++ b/src/cli/ls.rs @@ -2,7 +2,6 @@ use comfy_table::{Attribute, Cell, Color}; use eyre::{Result, ensure}; use indexmap::IndexMap; use itertools::Itertools; -use rayon::prelude::*; use serde_derive::Serialize; use std::path::PathBuf; use std::sync::Arc; @@ -80,17 +79,17 @@ pub struct Ls { } impl Ls { - pub fn run(mut self) -> Result<()> { - let config = Config::try_get()?; + pub async fn run(mut self) -> Result<()> { + let config = Config::get().await; self.installed_tool = self .installed_tool .or_else(|| self.tool_flag.clone().map(|p| vec![p])); self.verify_plugin()?; let mut runtimes = if self.prunable { - self.get_prunable_runtime_list()? + self.get_prunable_runtime_list().await? } else { - self.get_runtime_list(&config)? + self.get_runtime_list(&config).await? }; if self.current || self.global || self.local { // TODO: global is a little weird: it will show global versions as the active ones even if @@ -98,18 +97,30 @@ impl Ls { runtimes.retain(|(_, _, _, source)| !source.is_unknown()); } if self.installed { - runtimes.retain(|(_, p, tv, _)| p.is_version_installed(tv, true)); + let mut installed_runtimes = vec![]; + for (ls, p, tv, source) in runtimes { + if p.is_version_installed(&config, &tv, true) { + installed_runtimes.push((ls, p, tv, source)); + } + } + runtimes = installed_runtimes; } if self.missing { - runtimes.retain(|(_, p, tv, _)| !p.is_version_installed(tv, true)); + let mut missing_runtimes = vec![]; + for (ls, p, tv, source) in runtimes { + if !p.is_version_installed(&config, &tv, true) { + missing_runtimes.push((ls, p, tv, source)); + } + } + runtimes = missing_runtimes; } if let Some(prefix) = &self.prefix { runtimes.retain(|(_, _, tv, _)| tv.version.starts_with(prefix)); } if self.json { - self.display_json(runtimes) + self.display_json(&config, runtimes).await } else { - self.display_user(runtimes) + self.display_user(&config, runtimes).await } } @@ -124,15 +135,18 @@ impl Ls { Ok(()) } - fn display_json(&self, runtimes: Vec) -> Result<()> { + async fn display_json(&self, config: &Config, runtimes: Vec>) -> Result<()> { if let Some(plugins) = &self.installed_tool { // only runtimes for 1 plugin - let runtimes: Vec = runtimes + let runtimes: Vec> = runtimes .into_iter() .filter(|(_, p, _, _)| plugins.contains(p.ba())) - .map(|row| row.into()) .collect(); - miseprintln!("{}", serde_json::to_string_pretty(&runtimes)?); + let mut r = vec![]; + for row in runtimes { + r.push(json_tool_version_from(config, row).await); + } + miseprintln!("{}", serde_json::to_string_pretty(&r)?); return Ok(()); } @@ -141,19 +155,26 @@ impl Ls { .into_iter() .chunk_by(|(_, p, _, _)| p.id().to_string()) { - let runtimes = runtimes.map(|row| row.into()).collect(); - plugins.insert(plugin_name.clone(), runtimes); + let mut r = vec![]; + for (ls, p, tv, source) in runtimes { + r.push(json_tool_version_from(config, (ls, p, tv, source)).await); + } + plugins.insert(plugin_name.clone(), r); } miseprintln!("{}", serde_json::to_string_pretty(&plugins)?); Ok(()) } - fn display_user(&self, runtimes: Vec) -> Result<()> { - let rows = runtimes - .into_par_iter() - .map(|(ls, p, tv, source)| Row { + async fn display_user<'a>( + &'a self, + config: &Config, + runtimes: Vec>, + ) -> Result<()> { + let mut rows = vec![]; + for (ls, p, tv, source) in runtimes { + rows.push(Row { tool: p.clone(), - version: (ls, p.as_ref(), &tv, &source).into(), + version: version_status_from(config, (ls, p.as_ref(), &tv, &source)).await, requested: match source.is_unknown() { true => None, false => Some(tv.request.version()), @@ -163,8 +184,8 @@ impl Ls { } else { Some(source) }, - }) - .collect::>(); + }); + } let mut table = MiseTable::new(self.no_header, &["Tool", "Version", "Source", "Requested"]); for r in rows { let row = vec![ @@ -178,15 +199,16 @@ impl Ls { table.truncate(true).print() } - fn get_prunable_runtime_list(&self) -> Result> { + async fn get_prunable_runtime_list(&self) -> Result> { let installed_tool = self.installed_tool.clone().unwrap_or_default(); - Ok(prune::prunable_tools(installed_tool.iter().collect())? + Ok(prune::prunable_tools(installed_tool.iter().collect()) + .await? .into_iter() .map(|(p, tv)| (self, p, tv, ToolSource::Unknown)) .collect()) } - fn get_runtime_list(&self, config: &Config) -> Result> { - let mut trs = config.get_tool_request_set()?.clone(); + async fn get_runtime_list(&self, config: &Arc) -> Result> { + let mut trs = config.get_tool_request_set().await?.clone(); if self.global { trs = trs .iter() @@ -208,10 +230,11 @@ impl Ls { } let mut ts = Toolset::from(trs); - ts.resolve()?; + ts.resolve().await?; let rvs: Vec = ts - .list_all_versions()? + .list_all_versions() + .await? .into_iter() .map(|(b, tv)| ((b, tv.version.clone()), tv)) .filter(|((b, _), _)| match &self.installed_tool { @@ -227,7 +250,9 @@ impl Ls { }) .map(|(k, tv)| (self, k.0, tv.clone(), tv.request.source().clone())) // if it isn't installed and it's not specified, don't show it - .filter(|(_ls, p, tv, source)| !source.is_unknown() || p.is_version_installed(tv, true)) + .filter(|(_ls, p, tv, source)| { + !source.is_unknown() || p.is_version_installed(config, tv, true) + }) .filter(|(_ls, p, _, _)| match &self.installed_tool { Some(backend) => backend.contains(p.ba()), None => true, @@ -305,31 +330,29 @@ impl Row { } } -impl From> for JSONToolVersion { - fn from(row: RuntimeRow) -> Self { - let (ls, p, tv, source) = row; - let vs: VersionStatus = (ls, p.as_ref(), &tv, &source).into(); - JSONToolVersion { - symlinked_to: p.symlink_path(&tv), - install_path: tv.install_path(), - version: tv.version.clone(), - requested_version: if source.is_unknown() { - None - } else { - Some(tv.request.version()) - }, - source: if source.is_unknown() { - None - } else { - Some(source.as_json()) - }, - installed: !matches!(vs, VersionStatus::Missing(_)), - active: match vs { - VersionStatus::Active(_, _) => true, - VersionStatus::Symlink(_, active) => active, - _ => false, - }, - } +async fn json_tool_version_from(config: &Config, row: RuntimeRow<'_>) -> JSONToolVersion { + let (ls, p, tv, source) = row; + let vs: VersionStatus = version_status_from(config, (ls, p.as_ref(), &tv, &source)).await; + JSONToolVersion { + symlinked_to: p.symlink_path(&tv), + install_path: tv.install_path(), + version: tv.version.clone(), + requested_version: if source.is_unknown() { + None + } else { + Some(tv.request.version()) + }, + source: if source.is_unknown() { + None + } else { + Some(source.as_json()) + }, + installed: !matches!(vs, VersionStatus::Missing(_)), + active: match vs { + VersionStatus::Active(_, _) => true, + VersionStatus::Symlink(_, active) => active, + _ => false, + }, } } @@ -341,22 +364,23 @@ enum VersionStatus { Symlink(String, bool), } -impl From<(&Ls, &dyn Backend, &ToolVersion, &ToolSource)> for VersionStatus { - fn from((ls, p, tv, source): (&Ls, &dyn Backend, &ToolVersion, &ToolSource)) -> Self { - if p.symlink_path(tv).is_some() { - VersionStatus::Symlink(tv.version.clone(), !source.is_unknown()) - } else if !p.is_version_installed(tv, true) { - VersionStatus::Missing(tv.version.clone()) - } else if !source.is_unknown() { - let outdated = if ls.outdated { - p.is_version_outdated(tv) - } else { - false - }; - VersionStatus::Active(tv.version.clone(), outdated) +async fn version_status_from( + config: &Config, + (ls, p, tv, source): (&Ls, &dyn Backend, &ToolVersion, &ToolSource), +) -> VersionStatus { + if p.symlink_path(tv).is_some() { + VersionStatus::Symlink(tv.version.clone(), !source.is_unknown()) + } else if !p.is_version_installed(config, tv, true) { + VersionStatus::Missing(tv.version.clone()) + } else if !source.is_unknown() { + let outdated = if ls.outdated { + p.is_version_outdated(tv).await } else { - VersionStatus::Inactive(tv.version.clone()) - } + false + }; + VersionStatus::Active(tv.version.clone(), outdated) + } else { + VersionStatus::Inactive(tv.version.clone()) } } diff --git a/src/cli/ls_remote.rs b/src/cli/ls_remote.rs index 49ee7202c3..45914cc233 100644 --- a/src/cli/ls_remote.rs +++ b/src/cli/ls_remote.rs @@ -1,8 +1,6 @@ use std::sync::Arc; use eyre::Result; -use itertools::Itertools; -use rayon::prelude::*; use crate::backend; use crate::backend::Backend; @@ -32,15 +30,15 @@ pub struct LsRemote { } impl LsRemote { - pub fn run(self) -> Result<()> { - if let Some(plugin) = self.get_plugin()? { - self.run_single(plugin) + pub async fn run(self) -> Result<()> { + if let Some(plugin) = self.get_plugin().await? { + self.run_single(plugin).await } else { - self.run_all() + self.run_all().await } } - fn run_single(self, plugin: Arc) -> Result<()> { + async fn run_single(self, plugin: Arc) -> Result<()> { let prefix = match &self.plugin { Some(tool_arg) => match &tool_arg.tvr { Some(ToolRequest::Version { version: v, .. }) => Some(v.clone()), @@ -52,7 +50,7 @@ impl LsRemote { _ => self.prefix.clone(), }; - let versions = plugin.list_remote_versions()?; + let versions = plugin.list_remote_versions().await?; let versions = match prefix { Some(prefix) => versions .into_iter() @@ -68,32 +66,27 @@ impl LsRemote { Ok(()) } - fn run_all(self) -> Result<()> { - let versions = backend::list() - .into_par_iter() - .map(|p| { - let versions = p.list_remote_versions()?; - Ok((p, versions)) - }) - .collect::>>()? - .into_iter() - .sorted_by_cached_key(|(p, _)| p.id().to_string()) - .collect::>(); - for (plugin, versions) in versions { - for v in versions { - miseprintln!("{}@{v}", plugin); - } + async fn run_all(self) -> Result<()> { + let mut versions = vec![]; + for b in backend::list() { + let v = b.list_remote_versions().await?; + versions.extend(v.into_iter().map(|v| (b.id().to_string(), v))); + } + versions.sort_by_cached_key(|(id, _)| id.to_string()); + + for (tool, v) in versions { + miseprintln!("{tool}@{v}"); } Ok(()) } - fn get_plugin(&self) -> Result>> { + async fn get_plugin(&self) -> Result>> { match &self.plugin { Some(tool_arg) => { let backend = tool_arg.ba.backend()?; let mpr = MultiProgressReport::get(); if let Some(plugin) = backend.plugin() { - plugin.ensure_installed(&mpr, false)?; + plugin.ensure_installed(&mpr, false).await?; } Ok(Some(backend)) } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index a8a4cee5ac..8286e91451 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -252,63 +252,63 @@ pub enum Commands { } impl Commands { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { match self { Self::Activate(cmd) => cmd.run(), - Self::Alias(cmd) => cmd.run(), - Self::Asdf(cmd) => cmd.run(), - Self::Backends(cmd) => cmd.run(), - Self::BinPaths(cmd) => cmd.run(), + Self::Alias(cmd) => cmd.run().await, + Self::Asdf(cmd) => cmd.run().await, + Self::Backends(cmd) => cmd.run().await, + Self::BinPaths(cmd) => cmd.run().await, Self::Cache(cmd) => cmd.run(), - Self::Completion(cmd) => cmd.run(), - Self::Config(cmd) => cmd.run(), - Self::Current(cmd) => cmd.run(), + Self::Completion(cmd) => cmd.run().await, + Self::Config(cmd) => cmd.run().await, + Self::Current(cmd) => cmd.run().await, Self::Deactivate(cmd) => cmd.run(), - Self::Direnv(cmd) => cmd.run(), - Self::Doctor(cmd) => cmd.run(), - Self::En(cmd) => cmd.run(), - Self::Env(cmd) => cmd.run(), - Self::Exec(cmd) => cmd.run(), + Self::Direnv(cmd) => cmd.run().await, + Self::Doctor(cmd) => cmd.run().await, + Self::En(cmd) => cmd.run().await, + Self::Env(cmd) => cmd.run().await, + Self::Exec(cmd) => cmd.run().await, Self::Fmt(cmd) => cmd.run(), - Self::Generate(cmd) => cmd.run(), - Self::Global(cmd) => cmd.run(), - Self::HookEnv(cmd) => cmd.run(), - Self::HookNotFound(cmd) => cmd.run(), + Self::Generate(cmd) => cmd.run().await, + Self::Global(cmd) => cmd.run().await, + Self::HookEnv(cmd) => cmd.run().await, + Self::HookNotFound(cmd) => cmd.run().await, Self::Implode(cmd) => cmd.run(), - Self::Install(cmd) => cmd.run(), - Self::InstallInto(cmd) => cmd.run(), - Self::Latest(cmd) => cmd.run(), - Self::Link(cmd) => cmd.run(), - Self::Local(cmd) => cmd.run(), - Self::Ls(cmd) => cmd.run(), - Self::LsRemote(cmd) => cmd.run(), - Self::Outdated(cmd) => cmd.run(), - Self::Plugins(cmd) => cmd.run(), - Self::Prune(cmd) => cmd.run(), - Self::Registry(cmd) => cmd.run(), - Self::Reshim(cmd) => cmd.run(), - Self::Run(cmd) => cmd.run(), - Self::Search(cmd) => cmd.run(), + Self::Install(cmd) => cmd.run().await, + Self::InstallInto(cmd) => cmd.run().await, + Self::Latest(cmd) => cmd.run().await, + Self::Link(cmd) => cmd.run().await, + Self::Local(cmd) => cmd.run().await, + Self::Ls(cmd) => cmd.run().await, + Self::LsRemote(cmd) => cmd.run().await, + Self::Outdated(cmd) => cmd.run().await, + Self::Plugins(cmd) => cmd.run().await, + Self::Prune(cmd) => cmd.run().await, + Self::Registry(cmd) => cmd.run().await, + Self::Reshim(cmd) => cmd.run().await, + Self::Run(cmd) => cmd.run().await, + Self::Search(cmd) => cmd.run().await, #[cfg(feature = "self_update")] - Self::SelfUpdate(cmd) => cmd.run(), - Self::Set(cmd) => cmd.run(), - Self::Settings(cmd) => cmd.run(), - Self::Shell(cmd) => cmd.run(), - Self::Sync(cmd) => cmd.run(), - Self::Tasks(cmd) => cmd.run(), - Self::TestTool(cmd) => cmd.run(), - Self::Tool(cmd) => cmd.run(), - Self::Trust(cmd) => cmd.run(), - Self::Uninstall(cmd) => cmd.run(), - Self::Unset(cmd) => cmd.run(), - Self::Unuse(cmd) => cmd.run(), - Self::Upgrade(cmd) => cmd.run(), + Self::SelfUpdate(cmd) => cmd.run().await, + Self::Set(cmd) => cmd.run().await, + Self::Settings(cmd) => cmd.run().await, + Self::Shell(cmd) => cmd.run().await, + Self::Sync(cmd) => cmd.run().await, + Self::Tasks(cmd) => cmd.run().await, + Self::TestTool(cmd) => cmd.run().await, + Self::Tool(cmd) => cmd.run().await, + Self::Trust(cmd) => cmd.run().await, + Self::Uninstall(cmd) => cmd.run().await, + Self::Unset(cmd) => cmd.run().await, + Self::Unuse(cmd) => cmd.run().await, + Self::Upgrade(cmd) => cmd.run().await, Self::Usage(cmd) => cmd.run(), - Self::Use(cmd) => cmd.run(), - Self::Version(cmd) => cmd.run(), - Self::Watch(cmd) => cmd.run(), - Self::Where(cmd) => cmd.run(), - Self::Which(cmd) => cmd.run(), + Self::Use(cmd) => cmd.run().await, + Self::Version(cmd) => cmd.run().await, + Self::Watch(cmd) => cmd.run().await, + Self::Where(cmd) => cmd.run().await, + Self::Which(cmd) => cmd.run().await, #[cfg(debug_assertions)] Self::RenderHelp(cmd) => cmd.run(), @@ -320,19 +320,19 @@ impl Commands { } impl Cli { - pub fn run(args: &Vec) -> Result<()> { + pub async fn run(args: &Vec) -> Result<()> { crate::env::ARGS.write().unwrap().clone_from(args); - measure!("hande_shim", { shims::handle_shim() })?; + measure!("hande_shim", { shims::handle_shim().await })?; ctrlc::init(); let print_version = version::print_version_if_requested(args)?; - let cli = measure!("pre_settings", { Self::pre_settings(args) })?; + let cli = measure!("pre_settings", { Self::pre_settings(args).await })?; measure!("add_cli_matches", { Settings::add_cli_matches(&cli) }); measure!("settings", { let _ = Settings::try_get(); }); measure!("logger", { logger::init() }); - measure!("migrate", { migrate::run() }); + measure!("migrate", { migrate::run().await }); if let Err(err) = crate::cache::auto_prune() { warn!("auto_prune failed: {err:?}"); } @@ -340,40 +340,28 @@ impl Cli { debug!("ARGS: {}", &args.join(" ")); trace!("MISE_BIN: {}", MISE_BIN.to_string_lossy().to_string()); if print_version { - version::show_latest(); + version::show_latest().await; exit(0); } - let cmd = cli.get_command()?; - measure!("run {cmd}", { cmd.run() }) + let cmd = cli.get_command().await?; + measure!("run {cmd}", { cmd.run().await }) } - fn pre_settings(args: &Vec) -> Result { - let mut results = vec![]; - let mut cli = None; - rayon::scope(|r| { - r.spawn(|_| { - measure!("install_state", { - results.push(crate::install_state::init()) - }); - }); - measure!("get_matches_from", { - cli = Some(Cli::parse_from(args)); - }); - }); - results.into_iter().try_for_each(|r| r)?; - Ok(cli.unwrap()) + async fn pre_settings(args: &Vec) -> Result { + let (_, cli) = tokio::try_join!( + measure!("install_state", { crate::install_state::init() }), + async { measure!("get_matches_from", { Ok(Cli::parse_from(args)) }) }, + )?; + Ok(cli) } - fn get_command(self) -> Result { + async fn get_command(self) -> Result { if let Some(cmd) = self.command { Ok(cmd) } else { if let Some(task) = self.task { - if Config::get() - .tasks()? - .iter() - .any(|(_, t)| t.is_match(&task)) - { + let config = Config::get().await; + if config.tasks().await?.iter().any(|(_, t)| t.is_match(&task)) { return Ok(Commands::Run(run::Run { task, args: self.task_args.unwrap_or_default(), diff --git a/src/cli/outdated.rs b/src/cli/outdated.rs index 7b54d6aa92..b93303a5fd 100644 --- a/src/cli/outdated.rs +++ b/src/cli/outdated.rs @@ -41,10 +41,12 @@ pub struct Outdated { } impl Outdated { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { + let config = Config::try_get().await?; let mut ts = ToolsetBuilder::new() .with_args(&self.tool) - .build(&Config::get())?; + .build(&config) + .await?; let tool_set = self .tool .iter() @@ -52,12 +54,12 @@ impl Outdated { .collect::>(); ts.versions .retain(|_, tvl| tool_set.is_empty() || tool_set.contains(&tvl.backend)); - let outdated = ts.list_outdated_versions(self.bump); - self.display(outdated)?; + let outdated = ts.list_outdated_versions(self.bump).await; + self.display(outdated).await?; Ok(()) } - fn display(&self, outdated: Vec) -> Result<()> { + async fn display(&self, outdated: Vec) -> Result<()> { match self.json { true => self.display_json(outdated)?, false => self.display_table(outdated)?, diff --git a/src/cli/plugins/install.rs b/src/cli/plugins/install.rs index fc13762483..2c28e1b3ad 100644 --- a/src/cli/plugins/install.rs +++ b/src/cli/plugins/install.rs @@ -1,12 +1,13 @@ +use std::sync::Arc; + use color_eyre::eyre::{Result, bail, eyre}; use contracts::ensures; use heck::ToKebabCase; -use rayon::ThreadPoolBuilder; -use rayon::prelude::*; +use tokio::task::JoinSet; use url::Url; use crate::backend::unalias_backend; -use crate::config::{Config, Settings}; +use crate::config::Config; use crate::dirs; use crate::plugins::Plugin; use crate::plugins::asdf_plugin::AsdfPlugin; @@ -56,14 +57,14 @@ pub struct PluginsInstall { } impl PluginsInstall { - pub fn run(self, config: &Config) -> Result<()> { - let mpr = MultiProgressReport::get(); - if self.all { - return self.install_all_missing_plugins(config, &mpr); + pub async fn run(self, config: &Config) -> Result<()> { + let this = Arc::new(self); + if this.all { + return this.install_all_missing_plugins(config).await; } - let (name, git_url) = get_name_and_url(&self.new_plugin.clone().unwrap(), &self.git_url)?; + let (name, git_url) = get_name_and_url(&this.new_plugin.clone().unwrap(), &this.git_url)?; if git_url.is_some() { - self.install_one(name, git_url, &mpr)?; + this.install_one(name, git_url).await?; } else { let is_core = CORE_PLUGINS.contains_key(&name); if is_core { @@ -71,57 +72,58 @@ impl PluginsInstall { bail!("{name} is a core plugin and does not need to be installed"); } let mut plugins: Vec = vec![name]; - if let Some(second) = self.git_url.clone() { + if let Some(second) = this.git_url.clone() { plugins.push(second); }; - plugins.extend(self.rest.clone()); - self.install_many(plugins, &mpr)?; + plugins.extend(this.rest.clone()); + this.install_many(plugins).await?; } Ok(()) } - fn install_all_missing_plugins( - &self, - config: &Config, - mpr: &MultiProgressReport, - ) -> Result<()> { - let ts = ToolsetBuilder::new().build(config)?; + async fn install_all_missing_plugins(self: Arc, config: &Config) -> Result<()> { + let ts = ToolsetBuilder::new().build(config).await?; let missing_plugins = ts.list_missing_plugins(); if missing_plugins.is_empty() { warn!("all plugins already installed"); } - self.install_many(missing_plugins, mpr)?; + self.install_many(missing_plugins).await?; Ok(()) } - fn install_many(&self, plugins: Vec, mpr: &MultiProgressReport) -> Result<()> { - ThreadPoolBuilder::new() - .num_threads(Settings::get().jobs) - .build()? - .install(|| -> Result<()> { - plugins - .into_par_iter() - .map(|plugin| self.install_one(plugin, None, mpr)) - .collect::>>()?; - Ok(()) - }) + async fn install_many(self: Arc, plugins: Vec) -> Result<()> { + let mut jset: JoinSet> = JoinSet::new(); + for plugin in plugins { + let this = self.clone(); + jset.spawn(async move { this.install_one(plugin, None).await }); + } + while let Some(result) = jset.join_next().await { + match result { + Ok(Ok(())) => {} + Ok(Err(e)) => { + return Err(e); + } + Err(e) => { + return Err(eyre!(e)); + } + } + } + Ok(()) } - fn install_one( - &self, - name: String, - git_url: Option, - mpr: &MultiProgressReport, - ) -> Result<()> { + async fn install_one(self: Arc, name: String, git_url: Option) -> Result<()> { let path = dirs::PLUGINS.join(name.to_kebab_case()); - let mut plugin = AsdfPlugin::new(name.clone(), path); - plugin.repo_url = git_url; + let plugin = AsdfPlugin::new(name.clone(), path); + if let Some(url) = git_url { + plugin.set_remote_url(url); + } if !self.force && plugin.is_installed() { warn!("Plugin {name} already installed"); warn!("Use --force to install anyway"); } else { - plugin.ensure_installed(mpr, self.force)?; + let mpr = MultiProgressReport::get(); + plugin.ensure_installed(&mpr, self.force).await?; } Ok(()) } diff --git a/src/cli/plugins/link.rs b/src/cli/plugins/link.rs index 5ef401cbb0..752195af13 100644 --- a/src/cli/plugins/link.rs +++ b/src/cli/plugins/link.rs @@ -31,7 +31,7 @@ pub struct PluginsLink { } impl PluginsLink { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { let (name, path) = match self.dir { Some(path) => (self.name, path), None => { diff --git a/src/cli/plugins/ls.rs b/src/cli/plugins/ls.rs index d78c8d0d43..70b60174e0 100644 --- a/src/cli/plugins/ls.rs +++ b/src/cli/plugins/ls.rs @@ -1,5 +1,4 @@ use eyre::Result; -use rayon::prelude::*; use std::collections::BTreeMap; use tabled::{Table, Tabled}; @@ -42,7 +41,7 @@ pub struct PluginsLs { } impl PluginsLs { - pub fn run(self, config: &Config) -> Result<()> { + pub async fn run(self, config: &Config) -> Result<()> { let mut plugins: BTreeMap<_, _> = install_state::list_plugins()? .iter() .map(|(k, p)| (k.clone(), (*p, None))) @@ -56,7 +55,7 @@ impl PluginsLs { } if self.all { - for (name, backends) in config.get_shorthands() { + for (name, backends) in &config.shorthands { for full in backends { let plugin_type = PluginType::from_full(full)?; plugins.insert(name.clone(), (plugin_type, Some(full_to_url(full)))); @@ -67,7 +66,7 @@ impl PluginsLs { let plugins = plugins .into_iter() .map(|(short, (pt, url))| { - let mut plugin = pt.plugin(short.clone()); + let plugin = pt.plugin(short.clone()); if let Some(url) = url { plugin.set_remote_url(url); } @@ -77,7 +76,7 @@ impl PluginsLs { if self.urls || self.refs { let data = plugins - .into_par_iter() + .into_iter() .map(|(name, p)| { let remote_url = p.get_remote_url().unwrap_or_else(|e| { warn!("{name}: {e:?}"); diff --git a/src/cli/plugins/ls_remote.rs b/src/cli/plugins/ls_remote.rs index 3d73c2b2a9..c7f683cf6f 100644 --- a/src/cli/plugins/ls_remote.rs +++ b/src/cli/plugins/ls_remote.rs @@ -21,10 +21,10 @@ pub struct PluginsLsRemote { } impl PluginsLsRemote { - pub fn run(self, config: &Config) -> Result<()> { + pub async fn run(self, config: &Config) -> Result<()> { let installed_plugins = install_state::list_plugins()?; - let shorthands = config.get_shorthands().iter().sorted().collect_vec(); + let shorthands = config.shorthands.iter().sorted().collect_vec(); let max_plugin_len = shorthands .iter() .map(|(plugin, _)| measure_text_width(plugin)) diff --git a/src/cli/plugins/mod.rs b/src/cli/plugins/mod.rs index dd602dd298..74d6d26805 100644 --- a/src/cli/plugins/mod.rs +++ b/src/cli/plugins/mod.rs @@ -56,21 +56,21 @@ enum Commands { } impl Commands { - pub fn run(self, config: &Config) -> Result<()> { + pub async fn run(self, config: &Config) -> Result<()> { match self { - Self::Install(cmd) => cmd.run(config), - Self::Link(cmd) => cmd.run(), - Self::Ls(cmd) => cmd.run(config), - Self::LsRemote(cmd) => cmd.run(config), - Self::Uninstall(cmd) => cmd.run(), - Self::Update(cmd) => cmd.run(), + Self::Install(cmd) => cmd.run(config).await, + Self::Link(cmd) => cmd.run().await, + Self::Ls(cmd) => cmd.run(config).await, + Self::LsRemote(cmd) => cmd.run(config).await, + Self::Uninstall(cmd) => cmd.run().await, + Self::Update(cmd) => cmd.run().await, } } } impl Plugins { - pub fn run(self) -> Result<()> { - let config = Config::try_get()?; + pub async fn run(self) -> Result<()> { + let config = Config::get().await; let cmd = self.command.unwrap_or(Commands::Ls(ls::PluginsLs { all: self.all, core: self.core, @@ -79,6 +79,6 @@ impl Plugins { user: self.user, })); - cmd.run(&config) + cmd.run(&config).await } } diff --git a/src/cli/plugins/uninstall.rs b/src/cli/plugins/uninstall.rs index a0d04a3683..a3f2685240 100644 --- a/src/cli/plugins/uninstall.rs +++ b/src/cli/plugins/uninstall.rs @@ -24,7 +24,7 @@ pub struct PluginsUninstall { } impl PluginsUninstall { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { let mpr = MultiProgressReport::get(); let plugins = match self.all { @@ -34,17 +34,17 @@ impl PluginsUninstall { for plugin_name in plugins { let plugin_name = unalias_backend(&plugin_name); - self.uninstall_one(plugin_name, &mpr)?; + self.uninstall_one(plugin_name, &mpr).await?; } Ok(()) } - fn uninstall_one(&self, plugin_name: &str, mpr: &MultiProgressReport) -> Result<()> { + async fn uninstall_one(&self, plugin_name: &str, mpr: &MultiProgressReport) -> Result<()> { if let Ok(plugin) = plugins::get(plugin_name) { if plugin.is_installed() { let prefix = format!("plugin:{}", style::eblue(&plugin.name())); let pr = mpr.add(&prefix); - plugin.uninstall(&pr)?; + plugin.uninstall(&pr).await?; if self.purge { let backend = backend::get(&plugin_name.into()).unwrap(); backend.purge(&pr)?; diff --git a/src/cli/plugins/update.rs b/src/cli/plugins/update.rs index 53134d299b..b9daffb9b8 100644 --- a/src/cli/plugins/update.rs +++ b/src/cli/plugins/update.rs @@ -1,6 +1,8 @@ +use std::sync::Arc; + use console::style; -use eyre::{Report, Result, WrapErr, eyre}; -use rayon::prelude::*; +use eyre::{Result, WrapErr, eyre}; +use tokio::{sync::Semaphore, task::JoinSet}; use crate::config::Settings; use crate::plugins; @@ -24,7 +26,7 @@ pub struct Update { } impl Update { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { let plugins: Vec<_> = match self.plugin { Some(plugins) => plugins .into_iter() @@ -40,35 +42,37 @@ impl Update { }; let settings = Settings::try_get()?; - let mpr = MultiProgressReport::get(); - let mut errors = rayon::ThreadPoolBuilder::new() - .num_threads(self.jobs.unwrap_or(settings.jobs)) - .build()? - .install(|| { - plugins - .into_par_iter() - .map(|(short, ref_)| { - let plugin = plugins::get(&short)?; - let prefix = format!("plugin:{}", style(plugin.name()).blue().for_stderr()); - let pr = mpr.add(&prefix); - plugin - .update(&pr, ref_) - .wrap_err_with(|| format!("[{plugin}] plugin update"))?; - Ok(()) - }) - .filter_map(|r| r.err()) - .collect::>() + let mut jset: JoinSet> = JoinSet::new(); + let semaphore = Arc::new(Semaphore::new(self.jobs.unwrap_or(settings.jobs))); + for (short, ref_) in plugins { + let semaphore = semaphore.clone(); + jset.spawn(async move { + let _permit = semaphore.acquire_owned().await; + let plugin = plugins::get(&short)?; + let prefix = format!("plugin:{}", style(plugin.name()).blue().for_stderr()); + let mpr = MultiProgressReport::get(); + let pr = mpr.add(&prefix); + plugin + .update(&pr, ref_) + .await + .wrap_err_with(|| format!("[{plugin}] plugin update"))?; + Ok(()) }); - if errors.is_empty() { - Ok(()) - } else if errors.len() == 1 { - Err(errors.pop().unwrap()) - } else { - let err = eyre!("{} plugins failed to update", errors.len()); - Err(errors - .into_iter() - .fold(err, |report: Report, e| report.wrap_err(e))) } + + while let Some(result) = jset.join_next().await { + match result { + Ok(Ok(())) => {} + Ok(Err(e)) => { + return Err(e); + } + Err(e) => { + return Err(eyre!(e)); + } + } + } + + Ok(()) } } diff --git a/src/cli/prune.rs b/src/cli/prune.rs index 2032f98507..be60538375 100644 --- a/src/cli/prune.rs +++ b/src/cli/prune.rs @@ -43,7 +43,7 @@ pub struct Prune { } impl Prune { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { if self.configs || !self.tools { self.prune_configs()?; } @@ -51,8 +51,8 @@ impl Prune { let backends = self .installed_tool .as_ref() - .map(|it| it.iter().map(|ta| &ta.ba).collect()); - prune(backends.unwrap_or_default(), self.dry_run)?; + .map(|it| it.iter().map(|ta| ta.ba.as_ref()).collect()); + prune(backends.unwrap_or_default(), self.dry_run).await?; } Ok(()) } @@ -69,11 +69,14 @@ impl Prune { } } -pub fn prunable_tools(tools: Vec<&BackendArg>) -> Result, ToolVersion)>> { - let config = Config::try_get()?; - let ts = ToolsetBuilder::new().build(&config)?; +pub async fn prunable_tools( + tools: Vec<&BackendArg>, +) -> Result, ToolVersion)>> { + let config = Config::try_get().await?; + let ts = ToolsetBuilder::new().build(&config).await?; let mut to_delete = ts - .list_installed_versions()? + .list_installed_versions() + .await? .into_iter() .map(|(p, tv)| ((tv.ba().short.to_string(), tv.tv_pathname()), (p, tv))) .collect::, ToolVersion)>>(); @@ -84,7 +87,7 @@ pub fn prunable_tools(tools: Vec<&BackendArg>) -> Result, for cf in config.get_tracked_config_files()?.values() { let mut ts = Toolset::from(cf.to_tool_request_set()?); - ts.resolve()?; + ts.resolve().await?; for (_, tv) in ts.list_current_versions() { to_delete.remove(&(tv.ba().short.to_string(), tv.tv_pathname())); } @@ -93,12 +96,12 @@ pub fn prunable_tools(tools: Vec<&BackendArg>) -> Result, Ok(to_delete.into_values().collect()) } -pub fn prune(tools: Vec<&BackendArg>, dry_run: bool) -> Result<()> { - let to_delete = prunable_tools(tools)?; - delete(dry_run, to_delete) +pub async fn prune(tools: Vec<&BackendArg>, dry_run: bool) -> Result<()> { + let to_delete = prunable_tools(tools).await?; + delete(dry_run, to_delete).await } -fn delete(dry_run: bool, to_delete: Vec<(Arc, ToolVersion)>) -> Result<()> { +async fn delete(dry_run: bool, to_delete: Vec<(Arc, ToolVersion)>) -> Result<()> { let mpr = MultiProgressReport::get(); for (p, tv) in to_delete { let mut prefix = tv.style(); @@ -107,7 +110,7 @@ fn delete(dry_run: bool, to_delete: Vec<(Arc, ToolVersion)>) -> Res } let pr = mpr.add(&prefix); if dry_run || SETTINGS.yes || prompt::confirm_with_all(format!("remove {} ?", &tv))? { - p.uninstall_version(&tv, &pr, dry_run)?; + p.uninstall_version(&tv, &pr, dry_run).await?; runtime_symlinks::remove_missing_symlinks(p)?; pr.finish(); } diff --git a/src/cli/registry.rs b/src/cli/registry.rs index 1fe9b76459..eefece34bc 100644 --- a/src/cli/registry.rs +++ b/src/cli/registry.rs @@ -30,7 +30,7 @@ pub struct Registry { } impl Registry { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { if let Some(name) = &self.name { if let Some(rt) = REGISTRY.get(name.as_str()) { miseprintln!("{}", rt.backends().join(" ")); diff --git a/src/cli/reshim.rs b/src/cli/reshim.rs index ea6b4b30dc..e1418b8348 100644 --- a/src/cli/reshim.rs +++ b/src/cli/reshim.rs @@ -36,11 +36,11 @@ pub struct Reshim { } impl Reshim { - pub fn run(self) -> Result<()> { - let config = Config::try_get()?; - let ts = ToolsetBuilder::new().build(&config)?; + pub async fn run(self) -> Result<()> { + let config = Config::get().await; + let ts = ToolsetBuilder::new().build(&config).await?; - shims::reshim(&ts, self.force) + shims::reshim(&ts, self.force).await } } diff --git a/src/cli/run.rs b/src/cli/run.rs index 2e1e191238..567909606c 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -1,4 +1,4 @@ -use crate::hash; +use crate::{config, errors::Error, hash}; use std::collections::BTreeMap; use std::fs; use std::hash::{DefaultHasher, Hash, Hasher}; @@ -7,16 +7,14 @@ use std::iter::once; use std::ops::Deref; use std::path::{Path, PathBuf}; use std::process::Stdio; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::time::{Duration, SystemTime}; -use std::{panic, thread}; use super::args::ToolArg; use crate::cli::Cli; use crate::cmd::CmdLineRunner; use crate::config::{Config, SETTINGS}; use crate::env_diff::EnvMap; -use crate::errors::Error; use crate::file::display_path; use crate::task::task_file_providers::TaskFileProvidersBuilder; use crate::task::{Deps, GetMatchingExt, Task}; @@ -27,7 +25,6 @@ use crate::ui::{ctrlc, prompt, style, time}; use crate::{dirs, env, exit, file, ui}; use clap::{CommandFactory, ValueHint}; use console::Term; -use crossbeam_channel::{select, unbounded}; use demand::{DemandOption, Select}; use duct::IntoExecutablePath; use eyre::{Result, bail, ensure, eyre}; @@ -36,6 +33,10 @@ use indexmap::IndexMap; use itertools::Itertools; #[cfg(unix)] use nix::sys::signal::SIGTERM; +use tokio::{ + sync::{Mutex, Semaphore}, + task::JoinSet, +}; use xx::regex; /// Run task(s) @@ -173,7 +174,7 @@ pub struct Run { pub is_linear: bool, #[clap(skip)] - pub failed_tasks: Mutex>, + pub failed_tasks: std::sync::Mutex>, /// Change how tasks information is output when running tasks /// @@ -191,13 +192,13 @@ pub struct Run { pub tmpdir: PathBuf, #[clap(skip)] - pub keep_order_output: Mutex>, + pub keep_order_output: std::sync::Mutex>, #[clap(skip)] pub task_prs: IndexMap>>, #[clap(skip)] - pub timed_outputs: Arc>>, + pub timed_outputs: Arc>>, // Do not use cache on remote tasks #[clap(long, verbatim_doc_comment, env = "MISE_TASK_REMOTE_NO_CACHE")] @@ -207,7 +208,7 @@ pub struct Run { type KeepOrderOutputs = (Vec<(String, String)>, Vec<(String, String)>); impl Run { - pub fn run(mut self) -> Result<()> { + pub async fn run(mut self) -> Result<()> { if self.task == "-h" { self.get_clap_command().print_help()?; return Ok(()); @@ -223,9 +224,9 @@ impl Run { .chain(self.args.clone()) .chain(self.args_last.clone()) .collect_vec(); - let task_list = get_task_lists(&args, true)?; + let task_list = get_task_lists(&args, true).await?; time!("run get_task_lists"); - self.parallelize_tasks(task_list)?; + self.parallelize_tasks(task_list).await?; time!("run done"); Ok(()) } @@ -238,14 +239,15 @@ impl Run { .clone() } - fn parallelize_tasks(mut self, tasks: Vec) -> Result<()> { + async fn parallelize_tasks(mut self, tasks: Vec) -> Result<()> { time!("parallelize_tasks start"); ctrlc::exit_on_ctrl_c(false); if self.output(None) == TaskOutput::Timed { let timed_outputs = self.timed_outputs.clone(); - thread::spawn(move || { + tokio::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_millis(100)); loop { { let mut outputs = timed_outputs.lock().unwrap(); @@ -261,15 +263,15 @@ impl Run { } } } - thread::sleep(Duration::from_millis(100)); + interval.tick().await; } }); } - let mut tasks = resolve_depends(tasks)?; - self.fetch_tasks(&mut tasks)?; + let mut tasks = resolve_depends(tasks).await?; + self.fetch_tasks(&mut tasks).await?; - let tasks = Deps::new(tasks)?; + let tasks = Deps::new(tasks).await?; for task in tasks.all() { self.validate_task(task)?; match self.output(Some(task)) { @@ -290,90 +292,86 @@ impl Run { let num_tasks = tasks.all().count(); self.is_linear = tasks.is_linear(); self.output = Some(self.output(None)); + let this = Arc::new(self); - let mut all_tools = self.tool.clone(); + let mut all_tools = this.tool.clone(); for t in tasks.all() { for (k, v) in &t.tools { all_tools.push(format!("{k}@{v}").parse()?); } } + let config = Config::get().await; let mut ts = ToolsetBuilder::new() .with_args(&all_tools) - .build(&Config::get())?; + .build(&config) + .await?; + + ts.install_missing_versions( + &config, + &InstallOptions { + missing_args_only: !SETTINGS.task_run_auto_install, + ..Default::default() + }, + ) + .await?; - ts.install_missing_versions(&InstallOptions { - missing_args_only: !SETTINGS.task_run_auto_install, - ..Default::default() - })?; - - let tasks = Mutex::new(tasks); let timer = std::time::Instant::now(); - - let pool = rayon::ThreadPoolBuilder::new() - .num_threads(self.jobs() + 1) - .build()?; - pool.scope(|s| { - let (tx_err, rx_err) = unbounded(); - let run = |task: &Task| { - let t = task.clone(); - let tx_err = tx_err.clone(); - s.spawn(|_| { - let task = t; - let tx_err = tx_err; - let prefix = task.estyled_prefix(); - panic::set_hook(Box::new(move |info| { - prefix_eprintln!(prefix, "panic in task: {info}"); - exit(1); - })); - if !self.is_stopping() { - trace!("running task: {task}"); - if let Err(err) = self.run_task(&task) { - let status = Error::get_exit_status(&err); - if !self.is_stopping() && status.is_none() { - // only show this if it's the first failure, or we haven't killed all the remaining tasks - // otherwise we'll get unhelpful error messages about being killed by mise which we expect - let prefix = task.estyled_prefix(); - let err_body = if SETTINGS.verbose { - format!("{} {err:?}", style::ered("ERROR")) - } else { - format!("{} {err}", style::ered("ERROR")) - }; - self.eprint(&task, &prefix, &err_body); - } - let _ = tx_err.send((task.clone(), status)); + let this_ = this.clone(); + let jset = Arc::new(Mutex::new(JoinSet::new())); + let jset_ = jset.clone(); + + let handle = tokio::task::spawn(async move { + let tasks = Arc::new(Mutex::new(tasks)); + let semaphore = Arc::new(Semaphore::new(this_.jobs())); + let mut rx = tasks.lock().await.subscribe(); + while let Some(Some(task)) = rx.recv().await { + if this_.is_stopping() { + break; + } + trace!("running task: {task}"); + let jset = jset_.clone(); + let this_ = this_.clone(); + let semaphore = semaphore.clone(); + let tasks = tasks.clone(); + jset.lock().await.spawn(async move { + let _permit = semaphore.acquire().await?; + let result = this_.run_task(&task).await; + if let Err(err) = &result { + let status = Error::get_exit_status(err); + if !this_.is_stopping() && status.is_none() { + // only show this if it's the first failure, or we haven't killed all the remaining tasks + // otherwise we'll get unhelpful error messages about being killed by mise which we expect + let prefix = task.estyled_prefix(); + let err_body = if SETTINGS.verbose { + format!("{} {err:?}", style::ered("ERROR")) + } else { + format!("{} {err}", style::ered("ERROR")) + }; + this_.eprint(&task, &prefix, &err_body); } + this_.add_failed_task(task.clone(), status); } - tasks.lock().unwrap().remove(&task); + tasks.lock().await.remove(&task); + result }); - }; - let rx = tasks.lock().unwrap().subscribe(); - while !self.is_stopping() { - select! { - recv(rx) -> task => { // receive a task from Deps - if let Some(task) = task.unwrap() { - run(&task); - }else { - // we get the None value which means the channel closes - break; - } - } - recv(rx_err) -> task => { // a task errored - let (task, status) = task.unwrap(); - self.add_failed_task(task, status); - if !self.continue_on_error { - #[cfg(unix)] - CmdLineRunner::kill_all(SIGTERM); // start killing other running tasks - #[cfg(windows)] - CmdLineRunner::kill_all(); - } - } - } } }); - if self.output(None) == TaskOutput::KeepOrder { + while let Some(result) = jset.lock().await.join_next().await { + if result.is_ok() || this.continue_on_error { + continue; + } + #[cfg(unix)] + CmdLineRunner::kill_all(SIGTERM); + #[cfg(windows)] + CmdLineRunner::kill_all(); + break; + } + handle.await?; + + if this.output(None) == TaskOutput::KeepOrder { // TODO: display these as tasks complete in order somehow rather than waiting until everything is done - let output = self.keep_order_output.lock().unwrap(); + let output = this.keep_order_output.lock().unwrap(); for (out, err) in output.values() { for (prefix, line) in out { if console::colors_enabled() { @@ -391,13 +389,13 @@ impl Run { } } } - if self.timings() && num_tasks > 1 && *env::MISE_TASK_LEVEL == 0 { + if this.timings() && num_tasks > 1 && *env::MISE_TASK_LEVEL == 0 { let msg = format!("Finished in {}", time::format_duration(timer.elapsed())); eprintln!("{}", style::edim(msg)); }; - if let Some((task, status)) = self.failed_tasks.lock().unwrap().first() { + if let Some((task, status)) = this.failed_tasks.lock().unwrap().first() { let prefix = task.estyled_prefix(); - self.eprint( + this.eprint( task, &prefix, &format!("{} task failed", style::ered("ERROR")), @@ -421,7 +419,7 @@ impl Run { } } - fn run_task(&self, task: &Task) -> Result<()> { + async fn run_task(&self, task: &Task) -> Result<()> { let prefix = task.estyled_prefix(); if SETTINGS.task_skip.contains(&task.name) { if !self.quiet(Some(task)) { @@ -429,7 +427,7 @@ impl Run { } return Ok(()); } - if !self.force && self.sources_are_fresh(task)? { + if !self.force && self.sources_are_fresh(task).await? { if !self.quiet(Some(task)) { self.eprint(task, &prefix, "sources up-to-date, skipping"); } @@ -441,13 +439,16 @@ impl Run { } } - let config = Config::get(); + let config = Config::get().await; let mut tools = self.tool.clone(); for (k, v) in &task.tools { tools.push(format!("{k}@{v}").parse()?); } - let ts = ToolsetBuilder::new().with_args(&tools).build(&config)?; - let mut env = task.render_env(&ts)?; + let ts = ToolsetBuilder::new() + .with_args(&tools) + .build(&config) + .await?; + let mut env = task.render_env(&ts).await?; let output = self.output(Some(task)); env.insert("MISE_TASK_OUTPUT".into(), output.to_string()); if output == TaskOutput::Prefix { @@ -477,10 +478,11 @@ impl Run { let timer = std::time::Instant::now(); if let Some(file) = &task.file { - self.exec_file(file, task, &env, &prefix)?; + self.exec_file(file, task, &env, &prefix).await?; } else { - let rendered_run_scripts = - task.render_run_scripts_with_args(self.cd.clone(), &task.args, &env)?; + let rendered_run_scripts = task + .render_run_scripts_with_args(self.cd.clone(), &task.args, &env) + .await?; let get_args = || { [String::new()] @@ -489,10 +491,12 @@ impl Run { .cloned() .collect() }; - self.parse_usage_spec_and_init_env(task, &mut env, get_args)?; + self.parse_usage_spec_and_init_env(task, &mut env, get_args) + .await?; for (script, args) in rendered_run_scripts { - self.exec_script(&script, &args, task, &env, &prefix)?; + self.exec_script(&script, &args, task, &env, &prefix) + .await?; } } @@ -509,7 +513,7 @@ impl Run { Ok(()) } - fn exec_script( + async fn exec_script( &self, script: &str, args: &[String], @@ -517,7 +521,7 @@ impl Run { env: &BTreeMap, prefix: &str, ) -> Result<()> { - let config = Config::get(); + let config = Config::get().await; let script = script.trim_start(); let cmd = style::ebold(format!("$ {script} {args}", args = args.join(" "))) .bright() @@ -535,10 +539,10 @@ impl Run { tmp.flush()?; drop(tmp); file::make_executable(&file)?; - self.exec(&file, args, task, env, prefix) + self.exec(&file, args, task, env, prefix).await } else { let (program, args) = self.get_cmd_program_and_args(script, task, args)?; - self.exec_program(&program, &args, task, env, prefix) + self.exec_program(&program, &args, task, env, prefix).await } } @@ -601,13 +605,14 @@ impl Run { } } - fn exec_file(&self, file: &Path, task: &Task, env: &EnvMap, prefix: &str) -> Result<()> { - let config = Config::get(); + async fn exec_file(&self, file: &Path, task: &Task, env: &EnvMap, prefix: &str) -> Result<()> { + let config = Config::get().await; let mut env = env.clone(); let command = file.to_string_lossy().to_string(); let args = task.args.iter().cloned().collect_vec(); let get_args = || once(command.clone()).chain(args.clone()).collect_vec(); - self.parse_usage_spec_and_init_env(task, &mut env, get_args)?; + self.parse_usage_spec_and_init_env(task, &mut env, get_args) + .await?; if !self.quiet(Some(task)) { let cmd = format!("{} {}", display_path(file), args.join(" ")) @@ -618,10 +623,10 @@ impl Run { self.eprint(task, prefix, &cmd); } - self.exec(file, &args, task, &env, prefix) + self.exec(file, &args, task, &env, prefix).await } - fn exec( + async fn exec( &self, file: &Path, args: &[String], @@ -630,10 +635,10 @@ impl Run { prefix: &str, ) -> Result<()> { let (program, args) = self.get_file_program_and_args(file, task, args)?; - self.exec_program(&program, &args, task, env, prefix) + self.exec_program(&program, &args, task, env, prefix).await } - fn exec_program( + async fn exec_program( &self, program: &str, args: &[String], @@ -641,7 +646,7 @@ impl Run { env: &BTreeMap, prefix: &str, ) -> Result<()> { - let config = Config::get(); + let config = Config::get().await; let program = program.to_executable(); let redactions = config.redactions(); let raw = self.raw(Some(task)); @@ -728,7 +733,7 @@ impl Run { } } } - let dir = self.cwd(task)?; + let dir = self.cwd(task).await?; if !dir.exists() { self.eprint( task, @@ -811,13 +816,13 @@ impl Run { Ok(()) } - fn parse_usage_spec_and_init_env( + async fn parse_usage_spec_and_init_env( &self, task: &Task, env: &mut EnvMap, get_args: impl Fn() -> Vec, ) -> Result<()> { - let (spec, _) = task.parse_usage_spec(self.cd.clone(), env)?; + let (spec, _) = task.parse_usage_spec(self.cd.clone(), env).await?; if !spec.cmd.args.is_empty() || !spec.cmd.flags.is_empty() { let args: Vec = get_args(); debug!("Parsing usage spec for {:?}", args); @@ -833,15 +838,15 @@ impl Run { Ok(()) } - fn sources_are_fresh(&self, task: &Task) -> Result { + async fn sources_are_fresh(&self, task: &Task) -> Result { let outputs = task.outputs.paths(task); if task.sources.is_empty() && outputs.is_empty() { return Ok(false); } // TODO: We should benchmark this and find out if it might be possible to do some caching around this or something // perhaps using some manifest in a state directory or something, maybe leveraging atime? - let run = || -> Result { - let root = self.cwd(task)?; + let run = async || -> Result { + let root = self.cwd(task).await?; let mut sources = task.sources.clone(); sources.push(task.config_source.to_string_lossy().to_string()); let source_metadatas = self.get_file_metadatas(&root, &sources)?; @@ -870,7 +875,7 @@ impl Run { _ => Ok(false), } }; - Ok(run().unwrap_or_else(|err| { + Ok(run().await.unwrap_or_else(|err| { warn!("sources_are_fresh: {err:?}"); false })) @@ -975,13 +980,14 @@ impl Run { Ok(last_mod) } - fn cwd(&self, task: &Task) -> Result { + async fn cwd(&self, task: &Task) -> Result { if let Some(d) = &self.cd { Ok(d.clone()) - } else if let Some(d) = task.dir()? { + } else if let Some(d) = task.dir().await? { Ok(d) } else { Ok(Config::get() + .await .project_root .clone() .or_else(|| dirs::CWD.clone()) @@ -1015,7 +1021,7 @@ impl Run { ) } - fn fetch_tasks(&self, tasks: &mut Vec) -> Result<()> { + async fn fetch_tasks(&self, tasks: &mut Vec) -> Result<()> { let no_cache = self.no_cache || SETTINGS.task_remote_no_cache.unwrap_or(false); let task_file_providers = TaskFileProvidersBuilder::new() .with_cache(!no_cache) @@ -1031,7 +1037,7 @@ impl Run { bail!("No provider found for file: {}", source); } - let local_path = provider.unwrap().get_local_path(&source).unwrap(); + let local_path = provider.unwrap().get_local_path(&source).await?; t.file = Some(local_path); } @@ -1161,15 +1167,15 @@ fn trunc(prefix: &str, msg: &str) -> String { console::truncate_str(msg, *env::TERM_WIDTH - prefix_len - 1, "…").to_string() } -fn err_no_task(name: &str) -> Result<()> { - if Config::get().tasks().is_ok_and(|t| t.is_empty()) { +async fn err_no_task(config: &Config, name: &str) -> Result<()> { + if config.tasks().await.is_ok_and(|t| t.is_empty()) { bail!( "no tasks defined in {}. Are you in a project directory?", display_path(dirs::CWD.clone().unwrap_or_default()) ); } if let Some(cwd) = &*dirs::CWD { - let includes = Config::get().task_includes_for_dir(cwd); + let includes = config::task_includes_for_dir(cwd, &config.config_files); let path = includes .iter() .map(|d| d.join(name)) @@ -1194,9 +1200,9 @@ fn err_no_task(name: &str) -> Result<()> { bail!("no task {} found", style::ered(name)); } -fn prompt_for_task() -> Result { - let config = Config::get(); - let tasks = config.tasks()?; +async fn prompt_for_task() -> Result { + let config = Config::get().await; + let tasks = config.tasks().await?; ensure!( !tasks.is_empty(), "no tasks defined. see {url}", @@ -1209,7 +1215,7 @@ fn prompt_for_task() -> Result { for t in tasks.values().filter(|t| !t.hide) { s = s.option( DemandOption::new(&t.name) - .label(&t.display_name()) + .label(&t.display_name) .description(&t.description), ); } @@ -1226,8 +1232,10 @@ fn prompt_for_task() -> Result { } } -pub fn get_task_lists(args: &[String], prompt: bool) -> Result> { - args.iter() +pub async fn get_task_lists(args: &[String], prompt: bool) -> Result> { + let config = Config::get().await; + let args = args + .iter() .map(|s| vec![s.to_string()]) .coalesce(|a, b| { if b == vec![":::".to_string()] { @@ -1239,58 +1247,60 @@ pub fn get_task_lists(args: &[String], prompt: bool) -> Result> { } }) .flat_map(|args| args.split_first().map(|(t, a)| (t.clone(), a.to_vec()))) - .map(|(t, args)| { - // can be any of the following: - // - ./path/to/script - // - ~/path/to/script - // - /path/to/script - // - ../path/to/script - // - C:\path\to\script - // - .\path\to\script - if regex!(r#"^((\.*|~)(/|\\)|\w:\\)"#).is_match(&t) { - let path = PathBuf::from(&t); - if path.exists() { - let config_root = Config::get() - .project_root - .clone() - .or_else(|| dirs::CWD.clone()) - .unwrap_or_default(); - let task = Task::from_path(&path, &PathBuf::new(), &config_root)?; - return Ok(vec![task.with_args(args)]); - } + .collect::>(); + let mut tasks = vec![]; + let arg_re = regex!(r#"^((\.*|~)(/|\\)|\w:\\)"#); + for (t, args) in args { + // can be any of the following: + // - ./path/to/script + // - ~/path/to/script + // - /path/to/script + // - ../path/to/script + // - C:\path\to\script + // - .\path\to\script + if arg_re.is_match(&t) { + let path = PathBuf::from(&t); + if path.exists() { + let config_root = config + .project_root + .clone() + .or_else(|| dirs::CWD.clone()) + .unwrap_or_default(); + let task = Task::from_path(&path, &PathBuf::new(), &config_root).await?; + return Ok(vec![task.with_args(args)]); } - let config = Config::get(); - let tasks = config - .tasks_with_aliases()? - .get_matching(&t)? - .into_iter() - .cloned() - .collect_vec(); - if tasks.is_empty() { - if t != "default" || !prompt || !console::user_attended_stderr() { - err_no_task(&t)?; - } - - Ok(vec![prompt_for_task()?]) - } else { - Ok(tasks - .into_iter() - .map(|t| t.clone().with_args(args.to_vec())) - .collect()) + } + let cur_tasks = config + .tasks_with_aliases() + .await? + .get_matching(&t)? + .into_iter() + .cloned() + .collect_vec(); + if cur_tasks.is_empty() { + if t != "default" || !prompt || !console::user_attended_stderr() { + err_no_task(&config, &t).await?; } - }) - .flatten_ok() - .collect() + tasks.push(prompt_for_task().await?); + } else { + cur_tasks + .into_iter() + .map(|t| t.clone().with_args(args.to_vec())) + .for_each(|t| tasks.push(t)); + } + } + Ok(tasks) } -pub fn resolve_depends(tasks: Vec) -> Result> { - let tasks = tasks +pub async fn resolve_depends(tasks: Vec) -> Result> { + let config = Config::get().await; + let all_tasks = config.tasks_with_aliases().await?; + tasks .into_iter() .map(|t| { - let depends = t.all_depends()?; - eyre::Ok(once(t).chain(depends).collect::>()) + let depends = t.all_depends(&all_tasks)?; + Ok(once(t).chain(depends).collect::>()) }) .flatten_ok() - .collect::>>()?; - Ok(tasks) + .collect() } diff --git a/src/cli/search.rs b/src/cli/search.rs index 0f6713f28e..04ec05a7be 100644 --- a/src/cli/search.rs +++ b/src/cli/search.rs @@ -54,7 +54,7 @@ pub struct Search { } impl Search { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { if self.interactive { self.interactive()?; } else { diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 65d8b2c502..49daa30588 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -35,7 +35,7 @@ pub struct SelfUpdate { } impl SelfUpdate { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { if !Self::is_available() && !self.force { bail!("mise is installed via a package manager, cannot update"); } diff --git a/src/cli/set.rs b/src/cli/set.rs index d21c1d7fbf..0c81234307 100644 --- a/src/cli/set.rs +++ b/src/cli/set.rs @@ -46,16 +46,16 @@ pub struct Set { } impl Set { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { if self.complete { - return self.complete(); + return self.complete().await; } match (&self.remove, &self.env_vars) { (None, None) => { - return self.list_all(); + return self.list_all().await; } (None, Some(env_vars)) if env_vars.iter().all(|ev| ev.value.is_none()) => { - return self.get(); + return self.get().await; } _ => {} } @@ -93,23 +93,23 @@ impl Set { mise_toml.save() } - fn complete(&self) -> Result<()> { - for ev in self.cur_env()? { + async fn complete(&self) -> Result<()> { + for ev in self.cur_env().await? { println!("{}", ev.key); } Ok(()) } - fn list_all(self) -> Result<()> { - let env = self.cur_env()?; + async fn list_all(self) -> Result<()> { + let env = self.cur_env().await?; let mut table = tabled::Table::new(env); table::default_style(&mut table, false); miseprintln!("{table}"); Ok(()) } - fn get(self) -> Result<()> { - let env = self.cur_env()?; + async fn get(self) -> Result<()> { + let env = self.cur_env().await?; let filter = self.env_vars.unwrap(); let vars = filter .iter() @@ -129,7 +129,7 @@ impl Set { Ok(()) } - fn cur_env(&self) -> Result> { + async fn cur_env(&self) -> Result> { let rows = if let Some(file) = &self.file { let config = MiseToml::from_file(file).unwrap_or_default(); config @@ -146,7 +146,9 @@ impl Set { .collect() } else { Config::get() - .env_with_sources()? + .await + .env_with_sources() + .await? .iter() .map(|(key, (value, source))| Row { key: key.clone(), diff --git a/src/cli/settings/mod.rs b/src/cli/settings/mod.rs index ee331a9310..040fbc8a5e 100644 --- a/src/cli/settings/mod.rs +++ b/src/cli/settings/mod.rs @@ -44,7 +44,7 @@ impl Commands { } impl Settings { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { let cmd = self.command.unwrap_or_else(|| { if let Some(value) = self.value { Commands::Set(set::SettingsSet { diff --git a/src/cli/shell.rs b/src/cli/shell.rs index 77d4b61fb5..6012d07b79 100644 --- a/src/cli/shell.rs +++ b/src/cli/shell.rs @@ -38,8 +38,8 @@ pub struct Shell { } impl Shell { - pub fn run(self) -> Result<()> { - let config = Config::try_get()?; + pub async fn run(self) -> Result<()> { + let config = Config::get().await; if !env::is_activated() { err_inactive()?; } @@ -57,18 +57,21 @@ impl Shell { return Ok(()); } - let mut ts = ToolsetBuilder::new().with_args(&self.tool).build(&config)?; + let mut ts = ToolsetBuilder::new() + .with_args(&self.tool) + .build(&config) + .await?; let opts = InstallOptions { force: false, jobs: self.jobs, raw: self.raw, ..Default::default() }; - ts.install_missing_versions(&opts)?; - ts.notify_if_versions_missing(); + ts.install_missing_versions(&config, &opts).await?; + ts.notify_if_versions_missing().await; - for (p, tv) in ts.list_current_installed_versions() { - let source = &ts.versions.get(p.ba()).unwrap().source; + for (p, tv) in ts.list_current_installed_versions(&config) { + let source = &ts.versions.get(p.ba().as_ref()).unwrap().source; if matches!(source, ToolSource::Argument) { let k = format!("MISE_{}_VERSION", p.id().to_shouty_snake_case()); let op = shell.set_env(&k, &tv.version); diff --git a/src/cli/sync/mod.rs b/src/cli/sync/mod.rs index ddfd908bd3..6e7664a5aa 100644 --- a/src/cli/sync/mod.rs +++ b/src/cli/sync/mod.rs @@ -20,17 +20,17 @@ enum Commands { } impl Commands { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { match self { - Self::Node(cmd) => cmd.run(), - Self::Python(cmd) => cmd.run(), - Self::Ruby(cmd) => cmd.run(), + Self::Node(cmd) => cmd.run().await, + Self::Python(cmd) => cmd.run().await, + Self::Ruby(cmd) => cmd.run().await, } } } impl Sync { - pub fn run(self) -> Result<()> { - self.command.run() + pub async fn run(self) -> Result<()> { + self.command.run().await } } diff --git a/src/cli/sync/node.rs b/src/cli/sync/node.rs index bf69fc96e6..190c1b3992 100644 --- a/src/cli/sync/node.rs +++ b/src/cli/sync/node.rs @@ -35,20 +35,20 @@ pub struct SyncNodeType { } impl SyncNode { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { if self._type.brew { - self.run_brew()?; + self.run_brew().await?; } if self._type.nvm { - self.run_nvm()?; + self.run_nvm().await?; } if self._type.nodenv { - self.run_nodenv()?; + self.run_nodenv().await?; } Ok(()) } - fn run_brew(&self) -> Result<()> { + async fn run_brew(&self) -> Result<()> { let node = backend::get(&"node".into()).unwrap(); let brew_prefix = PathBuf::from(cmd!("brew", "--prefix").read()?).join("opt"); @@ -70,10 +70,10 @@ impl SyncNode { } } - config::rebuild_shims_and_runtime_symlinks(&[]) + config::rebuild_shims_and_runtime_symlinks(&[]).await } - fn run_nvm(&self) -> Result<()> { + async fn run_nvm(&self) -> Result<()> { let node = backend::get(&"node".into()).unwrap(); let nvm_versions_path = NVM_DIR.join("versions").join("node"); @@ -104,10 +104,10 @@ impl SyncNode { debug!("Created symlinks: {created:?}"); } - config::rebuild_shims_and_runtime_symlinks(&[]) + config::rebuild_shims_and_runtime_symlinks(&[]).await } - fn run_nodenv(&self) -> Result<()> { + async fn run_nodenv(&self) -> Result<()> { let node = backend::get(&"node".into()).unwrap(); let nodenv_versions_path = NODENV_ROOT.join("versions"); @@ -128,7 +128,7 @@ impl SyncNode { } } - config::rebuild_shims_and_runtime_symlinks(&[]) + config::rebuild_shims_and_runtime_symlinks(&[]).await } } diff --git a/src/cli/sync/python.rs b/src/cli/sync/python.rs index d44d63bcb9..79062f5b23 100644 --- a/src/cli/sync/python.rs +++ b/src/cli/sync/python.rs @@ -23,17 +23,17 @@ pub struct SyncPython { } impl SyncPython { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { if self.pyenv { - self.pyenv()?; + self.pyenv().await?; } if self.uv { - self.uv()?; + self.uv().await?; } - config::rebuild_shims_and_runtime_symlinks(&[]) + config::rebuild_shims_and_runtime_symlinks(&[]).await } - fn pyenv(&self) -> Result<()> { + async fn pyenv(&self) -> Result<()> { let python = backend::get(&"python".into()).unwrap(); let pyenv_versions_path = PYENV_ROOT.join("versions"); @@ -55,7 +55,7 @@ impl SyncPython { Ok(()) } - fn uv(&self) -> Result<()> { + async fn uv(&self) -> Result<()> { let python = backend::get(&"python".into()).unwrap(); let uv_versions_path = &*env::UV_PYTHON_INSTALL_DIR; let installed_python_versions_path = dirs::INSTALLS.join("python"); diff --git a/src/cli/sync/ruby.rs b/src/cli/sync/ruby.rs index 533d56ff0d..14f80c2d57 100644 --- a/src/cli/sync/ruby.rs +++ b/src/cli/sync/ruby.rs @@ -22,14 +22,14 @@ pub struct SyncRubyType { } impl SyncRuby { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { if self._type.brew { - self.run_brew()?; + self.run_brew().await?; } Ok(()) } - fn run_brew(&self) -> Result<()> { + async fn run_brew(&self) -> Result<()> { let ruby = backend::get(&"ruby".into()).unwrap(); let brew_prefix = PathBuf::from(cmd!("brew", "--prefix").read()?).join("opt"); @@ -51,7 +51,7 @@ impl SyncRuby { } } - config::rebuild_shims_and_runtime_symlinks(&[]) + config::rebuild_shims_and_runtime_symlinks(&[]).await } } diff --git a/src/cli/tasks/add.rs b/src/cli/tasks/add.rs index 252e4ecb13..a163fb3c70 100644 --- a/src/cli/tasks/add.rs +++ b/src/cli/tasks/add.rs @@ -68,9 +68,11 @@ pub struct TasksAdd { } impl TasksAdd { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { if self.file { - let mut path = Task::task_dir().join(self.task.replace(':', MAIN_SEPARATOR_STR)); + let mut path = Task::task_dir() + .await + .join(self.task.replace(':', MAIN_SEPARATOR_STR)); if path.is_dir() { path = path.join("_default"); } diff --git a/src/cli/tasks/deps.rs b/src/cli/tasks/deps.rs index 6533b2b5c4..830c1cce6d 100644 --- a/src/cli/tasks/deps.rs +++ b/src/cli/tasks/deps.rs @@ -27,55 +27,51 @@ pub struct TasksDeps { } impl TasksDeps { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { + let config = Config::try_get().await?; let tasks = if self.tasks.is_none() { - self.get_all_tasks()? + self.get_all_tasks(&config).await? } else { - self.get_task_lists()? + self.get_task_lists(&config).await? }; if self.dot { - self.print_deps_dot(tasks)?; + self.print_deps_dot(tasks).await?; } else { - self.print_deps_tree(tasks)?; + self.print_deps_tree(tasks).await?; } Ok(()) } - fn get_all_tasks(&self) -> Result> { - Ok(Config::get() - .tasks()? + async fn get_all_tasks(&self, config: &Config) -> Result> { + Ok(config + .tasks() + .await? .values() .filter(|t| self.hidden || !t.hide) .cloned() .collect()) } - fn get_task_lists(&self) -> Result> { - let config = Config::get(); - let tasks = config.tasks()?; - let tasks = self.tasks.as_ref().map(|t| { - t.iter() - .map(|tn| { - tasks - .get(tn) - .cloned() - .or_else(|| { - tasks - .values() - .find(|task| task.display_name().as_str() == tn.as_str()) - .cloned() - }) - .ok_or_else(|| self.err_no_task(tn.as_str())) - }) - .collect::>>() - }); - match tasks { - Some(Ok(tasks)) => Ok(tasks), - Some(Err(e)) => Err(e), - None => Ok(vec![]), + async fn get_task_lists(&self, config: &Config) -> Result> { + let all_tasks = config.tasks().await?; + let mut tasks = vec![]; + for task in self.tasks.as_ref().unwrap_or(&vec![]) { + match all_tasks + .get(task) + .or_else(|| all_tasks.values().find(|t| &t.display_name == task)) + .cloned() + { + Some(task) => { + tasks.push(task); + } + None => { + return Err(self.err_no_task(task).await); + } + } } + Ok(tasks) } /// @@ -90,8 +86,8 @@ impl TasksDeps { /// task5 /// ``` /// - fn print_deps_tree(&self, tasks: Vec) -> Result<()> { - let deps = Deps::new(tasks.clone())?; + async fn print_deps_tree(&self, tasks: Vec) -> Result<()> { + let deps = Deps::new(tasks.clone()).await?; // filter out nodes that are not selected let start_indexes = deps.graph.node_indices().filter(|&idx| { let task = &deps.graph[idx]; @@ -121,8 +117,8 @@ impl TasksDeps { /// } /// ``` // - fn print_deps_dot(&self, tasks: Vec) -> Result<()> { - let deps = Deps::new(tasks)?; + async fn print_deps_dot(&self, tasks: Vec) -> Result<()> { + let deps = Deps::new(tasks).await?; miseprintln!( "{:?}", Dot::with_attr_getters( @@ -138,11 +134,12 @@ impl TasksDeps { Ok(()) } - fn err_no_task(&self, t: &str) -> eyre::Report { - let config = Config::get(); + async fn err_no_task(&self, t: &str) -> eyre::Report { + let config = Config::get().await; let tasks = config .tasks() - .map(|t| t.values().map(|v| v.display_name()).collect::>()) + .await + .map(|t| t.values().map(|v| &v.display_name).collect::>()) .unwrap_or_default(); let task_names = tasks.into_iter().map(style::ecyan).join(", "); let t = style(&t).yellow().for_stderr(); diff --git a/src/cli/tasks/edit.rs b/src/cli/tasks/edit.rs index b4e0e0812b..64119a62d2 100644 --- a/src/cli/tasks/edit.rs +++ b/src/cli/tasks/edit.rs @@ -20,21 +20,24 @@ pub struct TasksEdit { } impl TasksEdit { - pub fn run(self) -> Result<()> { - let config = Config::try_get()?; + pub async fn run(self) -> Result<()> { + let config = Config::try_get().await?; let cwd = dirs::CWD.clone().unwrap_or_default(); let project_root = config.project_root.clone().unwrap_or(cwd); - let path = Task::task_dir().join(&self.task); + let path = Task::task_dir().await.join(&self.task); - let task = config - .tasks_with_aliases()? + let task = if let Some(task) = config + .tasks_with_aliases() + .await? .remove(&self.task) .cloned() - .or_else(|| Task::from_path(&path, path.parent().unwrap(), &project_root).ok()) - .map_or_else( - || Task::new(&path, path.parent().unwrap(), &project_root), - Ok, - )?; + { + task + } else { + Task::from_path(&path, path.parent().unwrap(), &project_root) + .await + .or_else(|_| Task::new(&path, path.parent().unwrap(), &project_root))? + }; let file = &task.config_source; if !file.exists() { file::write(file, default_task())?; diff --git a/src/cli/tasks/info.rs b/src/cli/tasks/info.rs index b3b2ce2a09..70d6c28756 100644 --- a/src/cli/tasks/info.rs +++ b/src/cli/tasks/info.rs @@ -21,23 +21,21 @@ pub struct TasksInfo { } impl TasksInfo { - pub fn run(self) -> Result<()> { - let config = Config::get(); - let tasks = config.tasks()?; + pub async fn run(self) -> Result<()> { + let config = Config::get().await; + let tasks = config.tasks().await?; - let task = tasks.get(&self.task).or_else(|| { - tasks - .values() - .find(|task| task.display_name().as_str() == self.task.as_str()) - }); + let task = tasks + .get(&self.task) + .or_else(|| tasks.values().find(|task| task.display_name == self.task)); if let Some(task) = task { - let ts = config.get_toolset()?; - let env = task.render_env(ts)?; + let ts = config.get_toolset().await?; + let env = task.render_env(ts).await?; if self.json { - self.display_json(task, &env)?; + self.display_json(task, &env).await?; } else { - self.display(task, &env)?; + self.display(task, &env).await?; } } else { bail!( @@ -49,8 +47,8 @@ impl TasksInfo { Ok(()) } - fn display(&self, task: &Task, env: &EnvMap) -> Result<()> { - info::inline_section("Task", task.display_name())?; + async fn display(&self, task: &Task, env: &EnvMap) -> Result<()> { + info::inline_section("Task", &task.display_name)?; if !task.aliases.is_empty() { info::inline_section("Aliases", task.aliases.join(", "))?; } @@ -91,17 +89,17 @@ impl TasksInfo { if !task.env.is_empty() { info::section("Environment Variables", toml::to_string_pretty(&task.env)?)?; } - let (spec, _) = task.parse_usage_spec(None, env)?; + let (spec, _) = task.parse_usage_spec(None, env).await?; if !spec.is_empty() { info::section("Usage Spec", &spec)?; } Ok(()) } - fn display_json(&self, task: &Task, env: &EnvMap) -> Result<()> { - let (spec, _) = task.parse_usage_spec(None, env)?; + async fn display_json(&self, task: &Task, env: &EnvMap) -> Result<()> { + let (spec, _) = task.parse_usage_spec(None, env).await?; let o = json!({ - "name": task.display_name(), + "name": task.display_name, "aliases": task.aliases, "description": task.description, "source": task.config_source, diff --git a/src/cli/tasks/ls.rs b/src/cli/tasks/ls.rs index 8ab45023b7..59c76e9a99 100644 --- a/src/cli/tasks/ls.rs +++ b/src/cli/tasks/ls.rs @@ -85,11 +85,12 @@ pub enum SortOrder { } impl TasksLs { - pub fn run(self) -> Result<()> { - let config = Config::try_get()?; - let ts = config.get_toolset()?; + pub async fn run(self) -> Result<()> { + let config = Config::get().await; + let ts = config.get_toolset().await?; let tasks = config - .tasks()? + .tasks() + .await? .values() .filter(|t| self.hidden || !t.hide) .filter(|t| !self.local || !t.global) @@ -101,7 +102,7 @@ impl TasksLs { if self.complete { return self.complete(tasks); } else if self.usage { - self.display_usage(ts, tasks)?; + self.display_usage(ts, tasks).await?; } else if self.json { self.display_json(tasks)?; } else { @@ -112,7 +113,7 @@ impl TasksLs { fn complete(&self, tasks: Vec) -> Result<()> { for t in tasks { - let name = t.display_name().replace(":", "\\:"); + let name = t.display_name.replace(":", "\\:"); let description = t.description.replace(":", "\\:"); println!("{name}:{description}",); } @@ -134,18 +135,18 @@ impl TasksLs { table.print() } - fn display_usage(&self, ts: &Toolset, tasks: Vec) -> Result<()> { + async fn display_usage(&self, ts: &Toolset, tasks: Vec) -> Result<()> { let mut usage = usage::Spec::default(); for task in tasks { - let env = task.render_env(ts)?; - let (mut task_spec, _) = task.parse_usage_spec(None, &env)?; + let env = task.render_env(ts).await?; + let (mut task_spec, _) = task.parse_usage_spec(None, &env).await?; for (name, complete) in task_spec.complete { task_spec.cmd.complete.insert(name, complete); } usage .cmd .subcommands - .insert(task.display_name(), task_spec.cmd); + .insert(task.display_name.clone(), task_spec.cmd); } miseprintln!("{}", usage.to_string()); Ok(()) @@ -156,7 +157,7 @@ impl TasksLs { .into_iter() .map(|task| { json!({ - "name": task.display_name(), + "name": task.display_name, "aliases": task.aliases, "description": task.description, "source": task.config_source, @@ -197,7 +198,7 @@ impl TasksLs { } fn task_to_row(&self, task: &Task) -> Row { - let mut row = vec![Cell::new(task.display_name()).add_attribute(Attribute::Bold)]; + let mut row = vec![Cell::new(&task.display_name).add_attribute(Attribute::Bold)]; if self.extended { row.push(Cell::new(task.aliases.join(", "))); row.push(Cell::new(display_rel_path(&task.config_source))); diff --git a/src/cli/tasks/mod.rs b/src/cli/tasks/mod.rs index 9c4b2156f2..65a6a23f94 100644 --- a/src/cli/tasks/mod.rs +++ b/src/cli/tasks/mod.rs @@ -34,20 +34,20 @@ enum Commands { } impl Commands { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { match self { - Self::Add(cmd) => cmd.run(), - Self::Deps(cmd) => cmd.run(), - Self::Edit(cmd) => cmd.run(), - Self::Info(cmd) => cmd.run(), - Self::Ls(cmd) => cmd.run(), - Self::Run(cmd) => cmd.run(), + Self::Add(cmd) => cmd.run().await, + Self::Deps(cmd) => cmd.run().await, + Self::Edit(cmd) => cmd.run().await, + Self::Info(cmd) => cmd.run().await, + Self::Ls(cmd) => cmd.run().await, + Self::Run(cmd) => cmd.run().await, } } } impl Tasks { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { let cmd = self .command .or(self.task.map(|t| { @@ -58,6 +58,6 @@ impl Tasks { })) .unwrap_or(Commands::Ls(self.ls)); - cmd.run() + cmd.run().await } } diff --git a/src/cli/test_tool.rs b/src/cli/test_tool.rs index 7ea7ce6d41..b2465034e5 100644 --- a/src/cli/test_tool.rs +++ b/src/cli/test_tool.rs @@ -37,7 +37,7 @@ pub struct TestTool { } impl TestTool { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { let mut errored = vec![]; self.github_summary(vec![ "Tool".to_string(), @@ -49,8 +49,8 @@ impl TestTool { "---".to_string(), "---".to_string(), ])?; - let config = Config::get(); - let ts = ToolsetBuilder::new().build(&config)?; + let config = Config::get().await; + let ts = ToolsetBuilder::new().build(&config).await?; let tools: BTreeSet = ts.versions.keys().map(|t| t.short.clone()).collect(); let mut found = false; for (i, (short, rt)) in REGISTRY.iter().enumerate() { @@ -81,7 +81,7 @@ impl TestTool { continue; }; let start = std::time::Instant::now(); - match self.test(&tool, &cmd, expected) { + match self.test(&tool, &cmd, expected).await { Ok(_) => { info!("{}: OK", tool.short); self.github_summary(vec![ @@ -117,7 +117,7 @@ impl TestTool { Ok(()) } - fn test(&self, tool: &ToolArg, cmd: &str, expected: &str) -> Result<()> { + async fn test(&self, tool: &ToolArg, cmd: &str, expected: &str) -> Result<()> { let mut args = vec![tool.clone()]; args.extend( tool.ba @@ -127,21 +127,23 @@ impl TestTool { .map(|ba| ba.to_string().parse()) .collect::>>()?, ); + let config = Config::get().await; let mut ts = ToolsetBuilder::new() .with_args(&args) .with_default_to_latest(true) - .build(&Config::get())?; + .build(&config) + .await?; let opts = InstallOptions { missing_args_only: false, jobs: self.jobs, raw: self.raw, ..Default::default() }; - ts.install_missing_versions(&opts)?; - ts.notify_if_versions_missing(); + ts.install_missing_versions(&config, &opts).await?; + ts.notify_if_versions_missing().await; let tv = if let Some(tv) = ts .versions - .get(&tool.ba) + .get(tool.ba.as_ref()) .and_then(|tvl| tvl.versions.first()) { tv.clone() @@ -150,11 +152,11 @@ impl TestTool { return Ok(()); }; let backend = tv.backend()?; - let config = Config::get(); - let env = ts.env_with_path(&config)?; + let config = Config::get().await; + let env = ts.env_with_path(&config).await?; let mut which_parts = cmd.split_whitespace().collect::>(); let cmd = which_parts.remove(0); - let mut which_cmd = backend.which(&tv, cmd)?.unwrap_or(PathBuf::from(cmd)); + let mut which_cmd = backend.which(&tv, cmd).await?.unwrap_or(PathBuf::from(cmd)); if cfg!(windows) && which_cmd == PathBuf::from("which") { which_cmd = PathBuf::from("where"); } @@ -174,7 +176,7 @@ impl TestTool { Some(0) => {} Some(code) => { if code == 127 { - let bin_dirs = backend.list_bin_paths(&tv)?; + let bin_dirs = backend.list_bin_paths(&tv).await?; for bin_dir in &bin_dirs { let bins = file::ls(bin_dir)? .into_iter() diff --git a/src/cli/tool.rs b/src/cli/tool.rs index 340ba80464..8af5cab7e8 100644 --- a/src/cli/tool.rs +++ b/src/cli/tool.rs @@ -54,20 +54,27 @@ pub struct ToolInfoFilter { } impl Tool { - pub fn run(self) -> Result<()> { - let mut ts = ToolsetBuilder::new().build(&Config::get())?; - ts.resolve()?; + pub async fn run(self) -> Result<()> { + let config = Config::get().await; + let mut ts = ToolsetBuilder::new().build(&config).await?; + ts.resolve().await?; let tvl = ts.versions.get(&self.tool); let tv = tvl.map(|tvl| tvl.versions.first().unwrap()); let ba = tv.map(|tv| tv.ba()).unwrap_or_else(|| &self.tool); let backend = ba.backend().ok(); + let description = if let Some(backend) = backend { + backend.description().await + } else { + None + }; let info = ToolInfo { backend: ba.full(), - description: backend.and_then(|b| b.description()), + description, installed_versions: ts - .list_installed_versions()? + .list_installed_versions() + .await? .into_iter() - .filter(|(b, _)| b.ba() == ba) + .filter(|(b, _)| b.ba().as_ref() == ba) .map(|(_, tv)| tv.version) .collect::>(), active_versions: tvl.map(|tvl| { diff --git a/src/cli/trust.rs b/src/cli/trust.rs index 3555a6953f..27ed9d7575 100644 --- a/src/cli/trust.rs +++ b/src/cli/trust.rs @@ -46,7 +46,7 @@ pub struct Trust { } impl Trust { - pub fn run(mut self) -> Result<()> { + pub async fn run(mut self) -> Result<()> { if self.show { return self.show(); } diff --git a/src/cli/uninstall.rs b/src/cli/uninstall.rs index 803530213d..5ad0c5e680 100644 --- a/src/cli/uninstall.rs +++ b/src/cli/uninstall.rs @@ -3,7 +3,6 @@ use std::sync::Arc; use console::style; use eyre::{Result, bail, eyre}; use itertools::Itertools; -use rayon::prelude::*; use crate::backend::Backend; use crate::cli::args::ToolArg; @@ -32,12 +31,12 @@ pub struct Uninstall { } impl Uninstall { - pub fn run(self) -> Result<()> { - let config = Config::try_get()?; + pub async fn run(self) -> Result<()> { + let config = Config::try_get().await?; let tool_versions = if self.installed_tool.is_empty() && self.all { - self.get_all_tool_versions(&config)? + self.get_all_tool_versions(&config).await? } else { - self.get_requested_tool_versions()? + self.get_requested_tool_versions(&config).await? }; let tool_versions = tool_versions .into_iter() @@ -49,13 +48,13 @@ impl Uninstall { let mpr = MultiProgressReport::get(); for (plugin, tv) in tool_versions { - if !plugin.is_version_installed(&tv, true) { + if !plugin.is_version_installed(&config, &tv, true) { warn!("{} is not installed", tv.style()); continue; } let pr = mpr.add(&tv.style()); - if let Err(err) = plugin.uninstall_version(&tv, &pr, self.dry_run) { + if let Err(err) = plugin.uninstall_version(&tv, &pr, self.dry_run).await { error!("{err}"); return Err(eyre!(err).wrap_err(format!("failed to uninstall {tv}"))); } @@ -67,61 +66,63 @@ impl Uninstall { } file::touch_dir(&dirs::DATA)?; - config::rebuild_shims_and_runtime_symlinks(&[])?; + config::rebuild_shims_and_runtime_symlinks(&[]).await?; Ok(()) } - fn get_all_tool_versions( + async fn get_all_tool_versions( &self, config: &Config, ) -> Result, ToolVersion)>> { - let ts = ToolsetBuilder::new().build(config)?; + let ts = ToolsetBuilder::new().build(config).await?; let tool_versions = ts - .list_installed_versions()? + .list_installed_versions() + .await? .into_iter() .collect::>(); Ok(tool_versions) } - fn get_requested_tool_versions(&self) -> Result, ToolVersion)>> { + async fn get_requested_tool_versions( + &self, + config: &Config, + ) -> Result, ToolVersion)>> { let runtimes = ToolArg::double_tool_condition(&self.installed_tool)?; - let tool_versions = runtimes - .into_par_iter() - .map(|ta| { - let backend = ta.ba.backend()?; - let query = ta.tvr.as_ref().map(|tvr| tvr.version()).unwrap_or_default(); - let installed_versions = backend.list_installed_versions()?; - let exact_match = installed_versions.iter().find(|v| v == &&query); - let matches = match exact_match { - Some(m) => vec![m], - None => installed_versions - .iter() - .filter(|v| v.starts_with(&query)) - .collect_vec(), - }; - let mut tvs = matches - .into_iter() - .map(|v| { - let tvr = ToolRequest::new(backend.ba().clone(), v, ToolSource::Unknown)?; - let tv = ToolVersion::new(tvr, v.into()); - Ok((backend.clone(), tv)) - }) - .collect::>>()?; - if let Some(tvr) = &ta.tvr { - tvs.push((backend.clone(), tvr.resolve(&Default::default())?)); - } - if tvs.is_empty() { - warn!( - "no versions found for {}", - style(&backend).blue().for_stderr() - ); - } - Ok(tvs) - }) - .collect::>>()? - .into_iter() - .flatten() - .collect::>(); + let mut tool_versions = Vec::new(); + for ta in runtimes { + let backend = ta.ba.backend()?; + let query = ta.tvr.as_ref().map(|tvr| tvr.version()).unwrap_or_default(); + let installed_versions = backend.list_installed_versions()?; + let exact_match = installed_versions.iter().find(|v| v == &&query); + let matches = match exact_match { + Some(m) => vec![m], + None => installed_versions + .iter() + .filter(|v| v.starts_with(&query)) + .collect_vec(), + }; + let mut tvs = matches + .into_iter() + .map(|v| { + let tvr = ToolRequest::new(backend.ba().clone(), v, ToolSource::Unknown)?; + let tv = ToolVersion::new(tvr, v.into()); + Ok((backend.clone(), tv)) + }) + .collect::>>()?; + if let Some(tvr) = &ta.tvr { + tvs.push(( + backend.clone(), + tvr.resolve(config, &Default::default()).await?, + )); + } + if tvs.is_empty() { + warn!( + "no versions found for {}", + style(&backend).blue().for_stderr() + ); + } + tool_versions.extend(tvs); + } Ok(tool_versions) } } diff --git a/src/cli/unset.rs b/src/cli/unset.rs index 62fc02767f..ef74ee9ba8 100644 --- a/src/cli/unset.rs +++ b/src/cli/unset.rs @@ -38,7 +38,7 @@ const AFTER_LONG_HELP: &str = color_print::cstr!( ); impl Unset { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { let filename = if let Some(file) = &self.file { file.clone() } else if self.global { diff --git a/src/cli/unuse.rs b/src/cli/unuse.rs index a1cddf7844..84c40b25b2 100644 --- a/src/cli/unuse.rs +++ b/src/cli/unuse.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; use crate::cli::args::ToolArg; use crate::cli::prune::prune; @@ -51,12 +51,12 @@ pub struct Unuse { } impl Unuse { - pub fn run(self) -> Result<()> { - let mut cf = self.get_config_file()?; + pub async fn run(self) -> Result<()> { + let cf = self.get_config_file().await?; let tools = cf.to_tool_request_set()?.tools; let mut removed: Vec<&ToolArg> = vec![]; for ta in &self.installed_tool { - if let Some(tool_requests) = tools.get(&ta.ba) { + if let Some(tool_requests) = tools.get(ta.ba.as_ref()) { let should_remove = if let Some(v) = &ta.version { tool_requests.iter().any(|tv| &tv.version() == v) } else { @@ -78,14 +78,21 @@ impl Unuse { } if !self.no_prune { - prune(self.installed_tool.iter().map(|ta| &ta.ba).collect(), false)?; - config::rebuild_shims_and_runtime_symlinks(&[])?; + prune( + self.installed_tool + .iter() + .map(|ta| ta.ba.as_ref()) + .collect(), + false, + ) + .await?; + config::rebuild_shims_and_runtime_symlinks(&[]).await?; } Ok(()) } - fn get_config_file(&self) -> Result> { + async fn get_config_file(&self) -> Result> { let cwd = env::current_dir()?; let path = if self.global { config::global_config_path() @@ -106,13 +113,13 @@ impl Unuse { } else if env::in_home_dir() { config::global_config_path() } else { - let config = Config::get(); + let config = Config::get().await; for cf in config.config_files.values() { if cf .to_tool_request_set()? .tools .keys() - .any(|ba| self.installed_tool.iter().any(|ta| ta.ba == *ba)) + .any(|ba| self.installed_tool.iter().any(|ta| &ta.ba == ba)) { return config_file::parse(cf.get_path()); } diff --git a/src/cli/upgrade.rs b/src/cli/upgrade.rs index bb7e11ac7c..216e68ba09 100644 --- a/src/cli/upgrade.rs +++ b/src/cli/upgrade.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use crate::backend::pipx::PIPXBackend; use crate::cli::args::ToolArg; use crate::config::{Config, config_file}; @@ -57,14 +59,21 @@ pub struct Upgrade { } impl Upgrade { - pub fn run(self) -> Result<()> { - let config = Config::try_get()?; - let ts = ToolsetBuilder::new().with_args(&self.tool).build(&config)?; - let mut outdated = ts.list_outdated_versions(self.bump); + pub async fn run(self) -> Result<()> { + let config = Config::get().await; + let ts = ToolsetBuilder::new() + .with_args(&self.tool) + .build(&config) + .await?; + let mut outdated = ts.list_outdated_versions(self.bump).await; if self.interactive && !outdated.is_empty() { outdated = self.get_interactive_tool_set(&outdated)?; } else if !self.tool.is_empty() { - outdated.retain(|o| self.tool.iter().any(|t| &t.ba == o.tool_version.ba())); + outdated.retain(|o| { + self.tool + .iter() + .any(|t| t.ba.as_ref() == o.tool_version.ba()) + }); } if outdated.is_empty() { info!("All tools are up to date"); @@ -76,15 +85,18 @@ impl Upgrade { ); } } else { - self.upgrade(&config, outdated)?; + self.upgrade(&config, outdated).await?; } Ok(()) } - fn upgrade(&self, config: &Config, outdated: Vec) -> Result<()> { + async fn upgrade(&self, config: &Arc, outdated: Vec) -> Result<()> { let mpr = MultiProgressReport::get(); - let mut ts = ToolsetBuilder::new().with_args(&self.tool).build(config)?; + let mut ts = ToolsetBuilder::new() + .with_args(&self.tool) + .build(config) + .await?; let config_file_updates = outdated .iter() @@ -156,7 +168,10 @@ impl Upgrade { let tool_request = outdated_info.tool_request.clone(); let tool_name = outdated_info.name.clone(); - match ts.install_all_versions(vec![tool_request], &mpr, &opts) { + match ts + .install_all_versions(config, vec![tool_request], &opts) + .await + { Ok(versions) => { for version in versions { successful_versions.push(version); @@ -170,7 +185,7 @@ impl Upgrade { } // Only update config files for tools that were successfully installed - for (o, mut cf) in config_file_updates { + for (o, cf) in config_file_updates { if successful_versions .iter() .any(|v| v.ba() == o.tool_version.ba()) @@ -194,16 +209,16 @@ impl Upgrade { .any(|v| v.ba() == o.tool_version.ba()) { let pr = mpr.add(&format!("uninstall {}@{}", o.name, tv)); - if let Err(e) = self.uninstall_old_version(&o.tool_version, &pr) { + if let Err(e) = self.uninstall_old_version(&o.tool_version, &pr).await { warn!("Failed to uninstall old version of {}: {}", o.name, e); } } } - config::rebuild_shims_and_runtime_symlinks(&successful_versions)?; + config::rebuild_shims_and_runtime_symlinks(&successful_versions).await?; if successful_versions.iter().any(|v| v.short() == "python") { - PIPXBackend::reinstall_all().unwrap_or_else(|err| { + PIPXBackend::reinstall_all().await.unwrap_or_else(|err| { warn!("failed to reinstall pipx tools: {err}"); }); } @@ -215,9 +230,14 @@ impl Upgrade { Ok(()) } - fn uninstall_old_version(&self, tv: &ToolVersion, pr: &Box) -> Result<()> { + async fn uninstall_old_version( + &self, + tv: &ToolVersion, + pr: &Box, + ) -> Result<()> { tv.backend()? .uninstall_version(tv, pr, self.dry_run) + .await .wrap_err_with(|| format!("failed to uninstall {tv}"))?; pr.finish(); Ok(()) diff --git a/src/cli/use.rs b/src/cli/use.rs index 2bc5f37b6f..7e66ff66b6 100644 --- a/src/cli/use.rs +++ b/src/cli/use.rs @@ -1,4 +1,7 @@ -use std::path::{Path, PathBuf}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; use console::{Term, style}; use eyre::{Result, bail, eyre}; @@ -14,7 +17,6 @@ use crate::toolset::{ InstallOptions, ResolveOptions, ToolRequest, ToolSource, ToolVersion, ToolsetBuilder, }; use crate::ui::ctrlc; -use crate::ui::multi_progress_report::MultiProgressReport; use crate::{config, env, file}; /// Installs a tool and adds the version to mise.toml. @@ -96,17 +98,17 @@ pub struct Use { } impl Use { - pub fn run(mut self) -> Result<()> { + pub async fn run(mut self) -> Result<()> { if self.tool.is_empty() && self.remove.is_empty() { self.tool = vec![self.tool_selector()?]; } env::TOOL_ARGS.write().unwrap().clone_from(&self.tool); - let config = Config::try_get()?; + let config = Config::get().await; let mut ts = ToolsetBuilder::new() .with_global_only(self.global) - .build(&config)?; - let mpr = MultiProgressReport::get(); - let mut cf = self.get_config_file()?; + .build(&config) + .await?; + let cf = self.get_config_file()?; let mut resolve_options = ResolveOptions { latest_versions: false, use_locked_version: true, @@ -132,17 +134,19 @@ impl Use { ), }) .collect::>()?; - let mut versions = ts.install_all_versions( - versions.clone(), - &mpr, - &InstallOptions { - force: self.force, - jobs: self.jobs, - raw: self.raw, - resolve_options, - ..Default::default() - }, - )?; + let mut versions = ts + .install_all_versions( + &config, + versions.clone(), + &InstallOptions { + force: self.force, + jobs: self.jobs, + raw: self.raw, + resolve_options, + ..Default::default() + }, + ) + .await?; let pin = self.pin || !self.fuzzy && (SETTINGS.pin || SETTINGS.asdf_compat); @@ -174,7 +178,7 @@ impl Use { } if self.global { - self.warn_if_hidden(&config, cf.get_path()); + self.warn_if_hidden(&config, cf.get_path()).await; } for plugin_name in &self.remove { cf.remove_tool(plugin_name)?; @@ -186,13 +190,13 @@ impl Use { tv.request.set_source(cf.source()); } - config::rebuild_shims_and_runtime_symlinks(&versions)?; + config::rebuild_shims_and_runtime_symlinks(&versions).await?; self.render_success_message(cf.as_ref(), &versions)?; Ok(()) } - fn get_config_file(&self) -> Result> { + fn get_config_file(&self) -> Result> { let cwd = env::current_dir()?; let path = if self.global { config::global_config_path() @@ -218,8 +222,11 @@ impl Use { config_file::parse_or_init(&path) } - fn warn_if_hidden(&self, config: &Config, global: &Path) { - let ts = ToolsetBuilder::new().build(config).unwrap_or_default(); + async fn warn_if_hidden(&self, config: &Config, global: &Path) { + let ts = ToolsetBuilder::new() + .build(config) + .await + .unwrap_or_default(); let warn = |targ: &ToolArg, p| { let plugin = &targ.ba; let p = display_path(p); @@ -227,7 +234,7 @@ impl Use { warn!("{plugin} is defined in {p} which overrides the global config ({global})"); }; for targ in &self.tool { - if let Some(tv) = ts.versions.get(&targ.ba) { + if let Some(tv) = ts.versions.get(targ.ba.as_ref()) { if let ToolSource::MiseToml(p) | ToolSource::ToolVersions(p) = &tv.source { if !file::same_file(p, global) { warn(targ, p); diff --git a/src/cli/version.rs b/src/cli/version.rs index dad8df6d37..5bcf17e8c7 100644 --- a/src/cli/version.rs +++ b/src/cli/version.rs @@ -27,20 +27,20 @@ pub struct Version { } impl Version { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { if self.json { - self.json()? + self.json().await? } else { show_version()?; - show_latest(); + show_latest().await; } Ok(()) } - fn json(&self) -> Result<()> { + async fn json(&self) -> Result<()> { let json = serde_json::json!({ "version": *VERSION, - "latest": get_latest_version(duration::DAILY), + "latest": get_latest_version(duration::DAILY).await, "os": *OS, "arch": *ARCH, "build_time": BUILD_TIME.to_string(), @@ -118,11 +118,11 @@ fn show_version() -> std::io::Result<()> { Ok(()) } -pub fn show_latest() { +pub async fn show_latest() { if ci_info::is_ci() && !cfg!(test) { return; } - if let Some(latest) = check_for_new_version(duration::DAILY) { + if let Some(latest) = check_for_new_version(duration::DAILY).await { warn!("mise version {} available", latest); if SelfUpdate::is_available() { let cmd = style("mise self-update").bright().yellow().for_stderr(); @@ -131,8 +131,11 @@ pub fn show_latest() { } } -pub fn check_for_new_version(cache_duration: Duration) -> Option { - if let Some(latest) = get_latest_version(cache_duration).and_then(Versioning::new) { +pub async fn check_for_new_version(cache_duration: Duration) -> Option { + if let Some(latest) = get_latest_version(cache_duration) + .await + .and_then(Versioning::new) + { if *V < latest { return Some(latest.to_string()); } @@ -140,7 +143,7 @@ pub fn check_for_new_version(cache_duration: Duration) -> Option { None } -fn get_latest_version(duration: Duration) -> Option { +async fn get_latest_version(duration: Duration) -> Option { let version_file_path = dirs::CACHE.join("latest-version"); if let Ok(metadata) = modified_duration(&version_file_path) { if metadata < duration { @@ -150,18 +153,18 @@ fn get_latest_version(duration: Duration) -> Option { } } let _ = file::create_dir_all(*dirs::CACHE); - let version = get_latest_version_call(); + let version = get_latest_version_call().await; let _ = file::write(version_file_path, version.clone().unwrap_or_default()); version } #[cfg(test)] -fn get_latest_version_call() -> Option { +async fn get_latest_version_call() -> Option { Some("0.0.0".to_string()) } #[cfg(not(test))] -fn get_latest_version_call() -> Option { +async fn get_latest_version_call() -> Option { let settings = Settings::get(); let url = match settings.paranoid { true => "https://mise.jdx.dev/VERSION", @@ -169,7 +172,7 @@ fn get_latest_version_call() -> Option { false => "http://mise.jdx.dev/VERSION", }; debug!("checking mise version from {}", url); - match crate::http::HTTP_VERSION_CHECK.get_text(url) { + match crate::http::HTTP_VERSION_CHECK.get_text(url).await { Ok(text) => { debug!("got version {text}"); Some(text.trim().to_string()) diff --git a/src/cli/watch.rs b/src/cli/watch.rs index 973d8c2eb2..ed92c3d11f 100644 --- a/src/cli/watch.rs +++ b/src/cli/watch.rs @@ -45,7 +45,7 @@ pub struct Watch { } impl Watch { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { if let Some(task) = &self.task { if task == "-h" { self.get_clap_command().print_help()?; @@ -56,8 +56,8 @@ impl Watch { return Ok(()); } } - let config = Config::try_get()?; - let ts = ToolsetBuilder::new().build(&config)?; + let config = Config::get().await; + let ts = ToolsetBuilder::new().build(&config).await?; if let Err(err) = which::which("watchexec") { let watchexec: BackendArg = "watchexec".into(); if !ts.versions.contains_key(&watchexec) { @@ -75,7 +75,7 @@ impl Watch { if args.is_empty() { bail!("No tasks specified"); } - let tasks = run::get_task_lists(&args, false)?; + let tasks = run::get_task_lists(&args, false).await?; let mut args = vec![]; if let Some(delay_run) = self.watchexec.delay_run { args.push("--delay-run".to_string()); @@ -204,7 +204,7 @@ impl Watch { } debug!("$ watchexec {}", args.join(" ")); let mut cmd = cmd::cmd("watchexec", &args); - for (k, v) in ts.env_with_path(&config)? { + for (k, v) in ts.env_with_path(&config).await? { cmd = cmd.env(k, v); } cmd.run()?; diff --git a/src/cli/where.rs b/src/cli/where.rs index f3f422d4bf..17d76d90fb 100644 --- a/src/cli/where.rs +++ b/src/cli/where.rs @@ -27,24 +27,25 @@ pub struct Where { } impl Where { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { + let config = Config::try_get().await?; let tvr = match self.tool.tvr { Some(tvr) => tvr, None => match self.asdf_version { Some(version) => self.tool.with_version(&version).tvr.unwrap(), None => { - let ts = ToolsetBuilder::new().build(&Config::get())?; + let ts = ToolsetBuilder::new().build(&config).await?; ts.versions - .get(&self.tool.ba) + .get(self.tool.ba.as_ref()) .and_then(|tvr| tvr.requests.first().cloned()) .unwrap_or_else(|| self.tool.with_version("latest").tvr.unwrap()) } }, }; - let tv = tvr.resolve(&Default::default())?; + let tv = tvr.resolve(&config, &Default::default()).await?; - if tv.backend()?.is_version_installed(&tv, true) { + if tv.backend()?.is_version_installed(&config, &tv, true) { miseprintln!("{}", tv.install_path().to_string_lossy()); Ok(()) } else { diff --git a/src/cli/which.rs b/src/cli/which.rs index 3c91f0674f..2c69561e52 100644 --- a/src/cli/which.rs +++ b/src/cli/which.rs @@ -34,21 +34,21 @@ pub struct Which { } impl Which { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { if self.complete { - return self.complete(); + return self.complete().await; } - let ts = self.get_toolset()?; + let ts = self.get_toolset().await?; let bin_name = self.bin_name.clone().unwrap(); - match ts.which(&bin_name) { + match ts.which(&bin_name).await { Some((p, tv)) => { if self.version { miseprintln!("{}", tv.version); } else if self.plugin { miseprintln!("{p}"); } else { - let path = p.which(&tv, &bin_name)?; + let path = p.which(&tv, &bin_name).await?; miseprintln!("{}", path.unwrap().display()); } Ok(()) @@ -64,10 +64,11 @@ impl Which { } } } - fn complete(&self) -> Result<()> { - let ts = self.get_toolset()?; + async fn complete(&self) -> Result<()> { + let ts = self.get_toolset().await?; let bins = ts .list_paths() + .await .into_iter() .flat_map(|p| file::ls(&p).unwrap_or_default()) .map(|p| p.file_name().unwrap().to_string_lossy().to_string()) @@ -79,13 +80,13 @@ impl Which { } Ok(()) } - fn get_toolset(&self) -> Result { - let config = Config::try_get()?; + async fn get_toolset(&self) -> Result { + let config = Config::try_get().await?; let mut tsb = ToolsetBuilder::new(); if let Some(tool) = &self.tool { tsb = tsb.with_args(&[tool.clone()]); } - let ts = tsb.build(&config)?; + let ts = tsb.build(&config).await?; Ok(ts) } fn has_shim(&self, shim: &str) -> bool { diff --git a/src/cmd.rs b/src/cmd.rs index f7935391be..41598e5581 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -104,8 +104,8 @@ pub struct CmdLineRunner<'a> { redactions: IndexSet, raw: bool, pass_signals: bool, - on_stdout: Option>, - on_stderr: Option>, + on_stdout: Option>, + on_stderr: Option>, } static OUTPUT_LOCK: Mutex<()> = Mutex::new(()); @@ -182,12 +182,12 @@ impl<'a> CmdLineRunner<'a> { self } - pub fn with_on_stdout(mut self, on_stdout: F) -> Self { + pub fn with_on_stdout(mut self, on_stdout: F) -> Self { self.on_stdout = Some(Box::new(on_stdout)); self } - pub fn with_on_stderr(mut self, on_stderr: F) -> Self { + pub fn with_on_stderr(mut self, on_stderr: F) -> Self { self.on_stderr = Some(Box::new(on_stderr)); self } diff --git a/src/config/config_file/idiomatic_version.rs b/src/config/config_file/idiomatic_version.rs index da0e3e8405..065a2ccde6 100644 --- a/src/config/config_file/idiomatic_version.rs +++ b/src/config/config_file/idiomatic_version.rs @@ -62,13 +62,13 @@ impl ConfigFile for IdiomaticVersionFile { } #[cfg_attr(coverage_nightly, coverage(off))] - fn remove_tool(&mut self, _fa: &BackendArg) -> Result<()> { + fn remove_tool(&self, _fa: &BackendArg) -> Result<()> { unimplemented!() } #[cfg_attr(coverage_nightly, coverage(off))] fn replace_versions( - &mut self, + &self, _plugin_name: &BackendArg, _versions: Vec, ) -> Result<()> { diff --git a/src/config/config_file/mise_toml.rs b/src/config/config_file/mise_toml.rs index ae3cc6ac68..4778dc4b7f 100644 --- a/src/config/config_file/mise_toml.rs +++ b/src/config/config_file/mise_toml.rs @@ -5,10 +5,13 @@ use once_cell::sync::OnceCell; use serde::de::Visitor; use serde::{Deserializer, de}; use serde_derive::Deserialize; -use std::collections::{BTreeMap, HashMap}; use std::fmt::{Debug, Display, Formatter}; use std::path::{Path, PathBuf}; use std::str::FromStr; +use std::{ + collections::{BTreeMap, HashMap}, + sync::{Mutex, MutexGuard}, +}; use tera::Context as TeraContext; use toml_edit::{Array, DocumentMut, InlineTable, Item, Key, Value, table, value}; use versions::Versioning; @@ -50,11 +53,11 @@ pub struct MiseToml { #[serde(default)] alias: AliasMap, #[serde(skip)] - doc: OnceCell, + doc: Mutex>, #[serde(default)] hooks: IndexMap, #[serde(default)] - tools: IndexMap, + tools: Mutex>, #[serde(default)] plugins: HashMap, #[serde(default)] @@ -128,16 +131,20 @@ impl MiseToml { Ok(rf) } - fn doc(&self) -> eyre::Result<&DocumentMut> { - self.doc.get_or_try_init(|| { - let body = file::read_to_string(&self.path).unwrap_or_default(); - Ok(body.parse()?) - }) + fn doc(&self) -> eyre::Result { + self.doc + .lock() + .unwrap() + .get_or_try_init(|| { + let body = file::read_to_string(&self.path).unwrap_or_default(); + Ok(body.parse()?) + }) + .cloned() } - fn doc_mut(&mut self) -> eyre::Result<&mut DocumentMut> { + fn doc_mut(&self) -> eyre::Result>> { self.doc()?; - Ok(self.doc.get_mut().unwrap()) + Ok(self.doc.lock().unwrap()) } pub fn set_alias(&mut self, fa: &BackendArg, from: &str, to: &str) -> eyre::Result<()> { @@ -147,6 +154,8 @@ impl MiseToml { .versions .insert(from.into(), to.into()); self.doc_mut()? + .get_mut() + .unwrap() .entry("alias") .or_insert_with(table) .as_table_like_mut() @@ -164,11 +173,15 @@ impl MiseToml { } pub fn remove_alias(&mut self, fa: &BackendArg, from: &str) -> eyre::Result<()> { - if let Some(aliases) = self - .doc_mut()? - .get_mut("alias") - .and_then(|v| v.as_table_mut()) - { + if let Some(aliases) = self.alias.get_mut(&fa.short) { + aliases.versions.shift_remove(from); + if aliases.versions.is_empty() && aliases.backend.is_none() { + self.alias.shift_remove(&fa.short); + } + } + let mut doc = self.doc_mut()?; + let doc = doc.get_mut().unwrap(); + if let Some(aliases) = doc.get_mut("alias").and_then(|v| v.as_table_mut()) { if let Some(alias) = aliases .get_mut(&fa.to_string()) .and_then(|v| v.as_table_mut()) @@ -184,21 +197,17 @@ impl MiseToml { } } if aliases.is_empty() { - self.doc_mut()?.as_table_mut().remove("alias"); - } - } - if let Some(aliases) = self.alias.get_mut(&fa.short) { - aliases.versions.shift_remove(from); - if aliases.versions.is_empty() && aliases.backend.is_none() { - self.alias.shift_remove(&fa.short); + doc.as_table_mut().remove("alias"); } } Ok(()) } pub fn update_env>(&mut self, key: &str, value: V) -> eyre::Result<()> { - let mut env_tbl = self - .doc_mut()? + let mut doc = self.doc_mut()?; + let mut env_tbl = doc + .get_mut() + .unwrap() .entry("env") .or_insert_with(table) .as_table_mut() @@ -218,8 +227,10 @@ impl MiseToml { } pub fn remove_env(&mut self, key: &str) -> eyre::Result<()> { - let env_tbl = self - .doc_mut()? + let mut doc = self.doc_mut()?; + let env_tbl = doc + .get_mut() + .unwrap() .entry("env") .or_insert_with(table) .as_table_mut() @@ -317,9 +328,11 @@ impl ConfigFile for MiseToml { self.tasks.0.values().collect() } - fn remove_tool(&mut self, fa: &BackendArg) -> eyre::Result<()> { - self.tools.shift_remove(fa); - let doc = self.doc_mut()?; + fn remove_tool(&self, fa: &BackendArg) -> eyre::Result<()> { + let mut tools = self.tools.lock().unwrap(); + tools.shift_remove(fa); + let mut doc = self.doc_mut()?; + let doc = doc.get_mut().unwrap(); if let Some(tools) = doc.get_mut("tools") { if let Some(tools) = tools.as_table_like_mut() { tools.remove(&fa.to_string()); @@ -331,13 +344,11 @@ impl ConfigFile for MiseToml { Ok(()) } - fn replace_versions( - &mut self, - ba: &BackendArg, - versions: Vec, - ) -> eyre::Result<()> { - let is_tools_sorted = is_tools_sorted(&self.tools); // was it previously sorted (if so we'll keep it sorted) - let existing = self.tools.entry(ba.clone()).or_default(); + fn replace_versions(&self, ba: &BackendArg, versions: Vec) -> eyre::Result<()> { + trace!("replacing versions {ba:?} {versions:?}"); + let mut tools = self.tools.lock().unwrap(); + let is_tools_sorted = is_tools_sorted(&tools); // was it previously sorted (if so we'll keep it sorted) + let existing = tools.entry(ba.clone()).or_default(); let output_empty_opts = |opts: &ToolVersionOptions| { if opts.os.is_some() || !opts.install_env.is_empty() { return false; @@ -354,8 +365,12 @@ impl ConfigFile for MiseToml { .iter() .map(|tr| MiseTomlTool::from(tr.clone())) .collect(); - let tools = self - .doc_mut()? + trace!("done replacing versions"); + let mut doc = self.doc_mut()?; + trace!("got doc"); + let tools = doc + .get_mut() + .unwrap() .entry("tools") .or_insert_with(table) .as_table_mut() @@ -441,7 +456,8 @@ impl ConfigFile for MiseToml { fn to_tool_request_set(&self) -> eyre::Result { let source = ToolSource::MiseToml(self.path.clone()); let mut trs = ToolRequestSet::new(); - for (ba, tvp) in &self.tools { + let tools = self.tools.lock().unwrap(); + for (ba, tvp) in tools.iter() { for tool in &tvp.0 { let version = self.parse_template(&tool.tt.to_string())?; let tvr = if let Some(mut options) = tool.options.clone() { @@ -452,9 +468,9 @@ impl ConfigFile for MiseToml { let mut ba_opts = ba.opts().clone(); ba_opts.merge(&options.opts); ba.set_opts(Some(ba_opts.clone())); - ToolRequest::new_opts(ba, &version, options, source.clone())? + ToolRequest::new_opts(ba.into(), &version, options, source.clone())? } else { - ToolRequest::new(ba.clone(), &version, source.clone())? + ToolRequest::new(ba.clone().into(), &version, source.clone())? }; trs.add_version(tvr, &source); } @@ -587,9 +603,9 @@ impl Clone for MiseToml { env: self.env.clone(), env_path: self.env_path.clone(), alias: self.alias.clone(), - doc: self.doc.clone(), + doc: Mutex::new(self.doc.lock().unwrap().clone()), hooks: self.hooks.clone(), - tools: self.tools.clone(), + tools: Mutex::new(self.tools.lock().unwrap().clone()), redactions: self.redactions.clone(), plugins: self.plugins.clone(), tasks: self.tasks.clone(), @@ -1501,6 +1517,8 @@ fn is_tools_sorted(tools: &IndexMap) -> bool { #[cfg(test)] #[cfg(unix)] mod tests { + use std::sync::Arc; + use indoc::formatdoc; use insta::{assert_debug_snapshot, assert_snapshot}; use test_log::test; @@ -1730,13 +1748,13 @@ mod tests { "#}, ) .unwrap(); - let mut cf = MiseToml::from_file(&p).unwrap(); + let cf = MiseToml::from_file(&p).unwrap(); let node = "node".into(); cf.replace_versions( &node, vec![ - ToolRequest::new("node".into(), "16.0.1", ToolSource::Unknown).unwrap(), - ToolRequest::new("node".into(), "18.0.1", ToolSource::Unknown).unwrap(), + ToolRequest::new(Arc::new("node".into()), "16.0.1", ToolSource::Unknown).unwrap(), + ToolRequest::new(Arc::new("node".into()), "18.0.1", ToolSource::Unknown).unwrap(), ], ) .unwrap(); @@ -1760,7 +1778,7 @@ mod tests { "#}, ) .unwrap(); - let mut cf = MiseToml::from_file(&p).unwrap(); + let cf = MiseToml::from_file(&p).unwrap(); cf.remove_tool(&"node".into()).unwrap(); assert_debug_snapshot!(cf.to_toolset().unwrap()); diff --git a/src/config/config_file/mod.rs b/src/config/config_file/mod.rs index 44fcc347c6..c82ea54ef2 100644 --- a/src/config/config_file/mod.rs +++ b/src/config/config_file/mod.rs @@ -1,9 +1,12 @@ -use std::collections::{HashMap, HashSet}; use std::ffi::OsStr; use std::fmt::{Debug, Display}; use std::hash::Hash; use std::path::{Path, PathBuf}; use std::sync::{Mutex, Once}; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; use eyre::{Result, eyre}; use idiomatic_version::IdiomaticVersionFile; @@ -29,6 +32,8 @@ use crate::ui::{prompt, style}; use crate::watch_files::WatchFile; use crate::{backend, config, dirs, env, file, hash}; +use super::Config; + pub mod idiomatic_version; pub mod mise_toml; pub mod toml; @@ -81,9 +86,8 @@ pub trait ConfigFile: Debug + Send + Sync { fn tasks(&self) -> Vec<&Task> { Default::default() } - fn remove_tool(&mut self, ba: &BackendArg) -> eyre::Result<()>; - fn replace_versions(&mut self, ba: &BackendArg, versions: Vec) - -> eyre::Result<()>; + fn remove_tool(&self, ba: &BackendArg) -> eyre::Result<()>; + fn replace_versions(&self, ba: &BackendArg, versions: Vec) -> eyre::Result<()>; fn save(&self) -> eyre::Result<()>; fn dump(&self) -> eyre::Result; fn source(&self) -> ToolSource; @@ -115,10 +119,12 @@ pub trait ConfigFile: Debug + Send + Sync { } impl dyn ConfigFile { - pub fn add_runtimes(&mut self, tools: &[ToolArg], pin: bool) -> eyre::Result<()> { + pub async fn add_runtimes(&self, tools: &[ToolArg], pin: bool) -> eyre::Result<()> { + let config = Config::get().await; // TODO: this has become a complete mess and could probably be greatly simplified let mut ts = self.to_toolset()?.to_owned(); - ts.resolve()?; + ts.resolve().await?; + trace!("resolved toolset"); let mut plugins_to_update = HashMap::new(); for ta in tools { if let Some(tv) = &ta.tvr { @@ -128,44 +134,47 @@ impl dyn ConfigFile { .push(tv); } } - for (fa, versions) in &plugins_to_update { + trace!("plugins to update: {plugins_to_update:?}"); + for (ba, versions) in &plugins_to_update { let mut tvl = ToolVersionList::new( - fa.clone(), + ba.clone(), ts.source.clone().unwrap_or(ToolSource::Argument), ); for tv in versions { tvl.requests.push((*tv).clone()); } - ts.versions.insert(fa.clone(), tvl); + ts.versions.insert(ba.clone(), tvl); } - ts.resolve()?; + trace!("resolving toolset 2"); + ts.resolve().await?; + trace!("resolved toolset 2"); for (ba, versions) in plugins_to_update { - let versions = versions - .into_iter() - .map(|tr| { - let mut tr = tr.clone(); - if pin { - let tv = tr.resolve(&Default::default())?; - if let ToolRequest::Version { - version: _version, + let mut new = vec![]; + for tr in versions { + let mut tr = tr.clone(); + if pin { + let tv = tr.resolve(&config, &Default::default()).await?; + if let ToolRequest::Version { + version: _version, + source, + options, + backend, + } = tr + { + tr = ToolRequest::Version { + version: tv.version, source, options, backend, - } = tr - { - tr = ToolRequest::Version { - version: tv.version, - source, - options, - backend, - }; - } + }; } - Ok(tr) - }) - .collect::>>()?; - self.replace_versions(&ba, versions)?; + } + new.push(tr); + } + trace!("replacing versions {new:?}"); + self.replace_versions(&ba, new)?; } + trace!("done adding runtimes"); Ok(()) } @@ -205,18 +214,18 @@ impl dyn ConfigFile { } } -fn init(path: &Path) -> Box { +fn init(path: &Path) -> Arc { match detect_config_file_type(path) { - Some(ConfigFileType::MiseToml) => Box::new(MiseToml::init(path)), - Some(ConfigFileType::ToolVersions) => Box::new(ToolVersions::init(path)), + Some(ConfigFileType::MiseToml) => Arc::new(MiseToml::init(path)), + Some(ConfigFileType::ToolVersions) => Arc::new(ToolVersions::init(path)), Some(ConfigFileType::IdiomaticVersion) => { - Box::new(IdiomaticVersionFile::init(path.to_path_buf())) + Arc::new(IdiomaticVersionFile::init(path.to_path_buf())) } _ => panic!("Unknown config file type: {}", path.display()), } } -pub fn parse_or_init(path: &Path) -> eyre::Result> { +pub fn parse_or_init(path: &Path) -> eyre::Result> { let path = if path.is_dir() { path.join("mise.toml") } else { @@ -229,20 +238,20 @@ pub fn parse_or_init(path: &Path) -> eyre::Result> { Ok(cf) } -pub fn parse(path: &Path) -> Result> { +pub fn parse(path: &Path) -> Result> { if let Ok(settings) = Settings::try_get() { if settings.paranoid { trust_check(path)?; } } match detect_config_file_type(path) { - Some(ConfigFileType::MiseToml) => Ok(Box::new(MiseToml::from_file(path)?)), - Some(ConfigFileType::ToolVersions) => Ok(Box::new(ToolVersions::from_file(path)?)), + Some(ConfigFileType::MiseToml) => Ok(Arc::new(MiseToml::from_file(path)?)), + Some(ConfigFileType::ToolVersions) => Ok(Arc::new(ToolVersions::from_file(path)?)), Some(ConfigFileType::IdiomaticVersion) => { - Ok(Box::new(IdiomaticVersionFile::from_file(path)?)) + Ok(Arc::new(IdiomaticVersionFile::from_file(path)?)) } #[allow(clippy::box_default)] - _ => Ok(Box::new(MiseToml::default())), + _ => Ok(Arc::new(MiseToml::default())), } } diff --git a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__remove_plugin.snap b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__remove_plugin.snap index d862e241ef..1af69e5d3e 100644 --- a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__remove_plugin.snap +++ b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__remove_plugin.snap @@ -1,10 +1,11 @@ --- source: src/config/config_file/mise_toml.rs expression: cf.to_toolset().unwrap() -snapshot_kind: text --- Toolset { versions: {}, source: None, - tera_ctx: OnceCell(Uninit), + tera_ctx: OnceCell { + value: None, + }, } diff --git a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__replace_versions.snap b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__replace_versions.snap index b1237b7456..d754d77441 100644 --- a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__replace_versions.snap +++ b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__replace_versions.snap @@ -1,7 +1,6 @@ --- source: src/config/config_file/mise_toml.rs expression: cf.to_toolset().unwrap() -snapshot_kind: text --- Toolset { versions: { @@ -44,5 +43,7 @@ Toolset { "/tmp/.mise.toml", ), ), - tera_ctx: OnceCell(Uninit), + tera_ctx: OnceCell { + value: None, + }, } diff --git a/src/config/config_file/tool_versions.rs b/src/config/config_file/tool_versions.rs index 1922a2b311..a8b06b8d95 100644 --- a/src/config/config_file/tool_versions.rs +++ b/src/config/config_file/tool_versions.rs @@ -1,5 +1,8 @@ -use std::fmt::{Display, Formatter}; use std::path::{Path, PathBuf}; +use std::{ + fmt::{Display, Formatter}, + sync::{Arc, Mutex}, +}; use console::{Alignment, measure_text_width, pad_str}; use eyre::Result; @@ -21,13 +24,13 @@ use super::ConfigFileType; // shfmt 3.6.0 /// represents asdf's .tool-versions file -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default)] pub struct ToolVersions { context: Context, path: PathBuf, pre: String, - plugins: IndexMap, - tools: ToolRequestSet, + plugins: Mutex>, + tools: Mutex, } #[derive(Debug, Clone)] @@ -43,7 +46,7 @@ impl ToolVersions { context.insert("config_root", filename.parent().unwrap().to_str().unwrap()); ToolVersions { context, - tools: ToolRequestSet::new(), + tools: Mutex::new(ToolRequestSet::new()), path: filename.to_path_buf(), ..Default::default() } @@ -66,22 +69,12 @@ impl ToolVersions { cf.pre.push('\n'); } - cf.plugins = Self::parse_plugins(&s); + cf.plugins = Mutex::new(Self::parse_plugins(&s)); cf.populate_toolset()?; trace!("{cf}"); Ok(cf) } - fn get_or_create_plugin(&mut self, fa: &BackendArg) -> &mut ToolVersionPlugin { - self.plugins - .entry(fa.clone()) - .or_insert_with(|| ToolVersionPlugin { - orig_name: fa.short.to_string(), - versions: vec![], - post: "".into(), - }) - } - fn parse_plugins(input: &str) -> IndexMap { let mut plugins: IndexMap = IndexMap::new(); for line in input.lines() { @@ -115,16 +108,21 @@ impl ToolVersions { plugins } - fn add_version(&mut self, fa: &BackendArg, version: String) { - self.get_or_create_plugin(fa).versions.push(version); + fn add_version( + &self, + plugins: &mut IndexMap, + fa: &BackendArg, + version: String, + ) { + get_or_create_plugin(plugins, fa).versions.push(version); } - fn populate_toolset(&mut self) -> eyre::Result<()> { + fn populate_toolset(&self) -> eyre::Result<()> { let source = ToolSource::ToolVersions(self.path.clone()); - for (ba, tvp) in &self.plugins { + for (ba, tvp) in &*self.plugins.lock().unwrap() { for version in &tvp.versions { - let tvr = ToolRequest::new(ba.clone(), version, source.clone())?; - self.tools.add_version(tvr, &source) + let tvr = ToolRequest::new(Arc::new(ba.clone()), version, source.clone())?; + self.tools.lock().unwrap().add_version(tvr, &source) } } Ok(()) @@ -135,6 +133,8 @@ impl Display for ToolVersions { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { let plugins = &self .plugins + .lock() + .unwrap() .iter() .map(|(p, v)| format!("{}@{}", p, v.versions.join("|"))) .collect_vec(); @@ -156,22 +156,19 @@ impl ConfigFile for ToolVersions { self.path.as_path() } - fn remove_tool(&mut self, fa: &BackendArg) -> Result<()> { - self.plugins.shift_remove(fa); + fn remove_tool(&self, fa: &BackendArg) -> Result<()> { + self.plugins.lock().unwrap().shift_remove(fa); Ok(()) } - fn replace_versions( - &mut self, - fa: &BackendArg, - versions: Vec, - ) -> eyre::Result<()> { - self.get_or_create_plugin(fa).versions.clear(); + fn replace_versions(&self, fa: &BackendArg, versions: Vec) -> eyre::Result<()> { + let mut plugins = self.plugins.lock().unwrap(); + get_or_create_plugin(&mut plugins, fa).versions.clear(); for tr in versions { if !tr.options().is_empty() { warn!("tool options are not supported in .tool-versions files"); } - self.add_version(fa, tr.version()); + self.add_version(&mut plugins, fa, tr.version()); } Ok(()) } @@ -184,13 +181,13 @@ impl ConfigFile for ToolVersions { fn dump(&self) -> eyre::Result { let mut s = self.pre.clone(); - let max_plugin_len = self - .plugins + let plugins = self.plugins.lock().unwrap(); + let max_plugin_len = plugins .keys() .map(|p| measure_text_width(&p.to_string())) .max() .unwrap_or_default(); - for (_, tv) in &self.plugins { + for (_, tv) in &*plugins { let mut plugin = tv.orig_name.to_string(); if plugin == "node" { plugin = "nodejs".into(); @@ -209,6 +206,31 @@ impl ConfigFile for ToolVersions { } fn to_tool_request_set(&self) -> eyre::Result { - Ok(self.tools.clone()) + Ok(self.tools.lock().unwrap().clone()) + } +} + +fn get_or_create_plugin<'a>( + plugins: &'a mut IndexMap, + fa: &BackendArg, +) -> &'a mut ToolVersionPlugin { + plugins + .entry(fa.clone()) + .or_insert_with(|| ToolVersionPlugin { + orig_name: fa.short.to_string(), + versions: vec![], + post: "".into(), + }) +} + +impl Clone for ToolVersions { + fn clone(&self) -> Self { + Self { + context: self.context.clone(), + path: self.path.clone(), + pre: self.pre.clone(), + plugins: Mutex::new(self.plugins.lock().unwrap().clone()), + tools: Mutex::new(self.tools.lock().unwrap().clone()), + } } } diff --git a/src/config/env_directive/file.rs b/src/config/env_directive/file.rs index 3423c162c9..a2d3c04ae4 100644 --- a/src/config/env_directive/file.rs +++ b/src/config/env_directive/file.rs @@ -1,4 +1,4 @@ -use crate::config::env_directive::EnvResults; +use crate::config::{Config, env_directive::EnvResults}; use crate::file::display_path; use crate::{Result, file, sops}; use eyre::{WrapErr, bail, eyre}; @@ -19,7 +19,8 @@ struct Env { impl EnvResults { #[allow(clippy::too_many_arguments)] - pub fn file( + pub async fn file( + config: &Config, ctx: &mut tera::Context, tera: &mut tera::Tera, r: &mut EnvResults, @@ -38,16 +39,16 @@ impl EnvResults { .map(|e| e.to_string_lossy().to_string()) .unwrap_or_default(); *env = match ext.as_str() { - "json" => Self::json(&p, parse_template)?, - "yaml" => Self::yaml(&p, parse_template)?, - "toml" => Self::toml(&p)?, - _ => Self::dotenv(&p)?, + "json" => Self::json(config, &p, parse_template).await?, + "yaml" => Self::yaml(config, &p, parse_template).await?, + "toml" => Self::toml(&p).await?, + _ => Self::dotenv(&p).await?, }; } Ok(out) } - fn json(p: &Path, parse_template: PT) -> Result + async fn json(config: &Config, p: &Path, parse_template: PT) -> Result where PT: FnMut(String) -> Result, { @@ -55,7 +56,8 @@ impl EnvResults { if let Ok(raw) = file::read_to_string(p) { let mut f: Env = serde_json::from_str(&raw).wrap_err_with(errfn)?; if !f.sops.is_empty() { - let raw = sops::decrypt::<_, JsonFileFormat>(&raw, parse_template, "json")?; + let raw = sops::decrypt::<_, JsonFileFormat>(config, &raw, parse_template, "json") + .await?; f = serde_json::from_str(&raw).wrap_err_with(errfn)?; } f.env @@ -77,7 +79,7 @@ impl EnvResults { } } - fn yaml(p: &Path, parse_template: PT) -> Result + async fn yaml(config: &Config, p: &Path, parse_template: PT) -> Result where PT: FnMut(String) -> Result, { @@ -85,7 +87,8 @@ impl EnvResults { if let Ok(raw) = file::read_to_string(p) { let mut f: Env = serde_yaml::from_str(&raw).wrap_err_with(errfn)?; if !f.sops.is_empty() { - let raw = sops::decrypt::<_, YamlFileFormat>(&raw, parse_template, "yaml")?; + let raw = sops::decrypt::<_, YamlFileFormat>(config, &raw, parse_template, "yaml") + .await?; f = serde_yaml::from_str(&raw).wrap_err_with(errfn)?; } f.env @@ -107,7 +110,7 @@ impl EnvResults { } } - fn toml(p: &Path) -> Result { + async fn toml(p: &Path) -> Result { let errfn = || eyre!("failed to parse toml file: {}", display_path(p)); // sops does not support toml yet, so no need to parse sops if let Ok(raw) = file::read_to_string(p) { @@ -132,7 +135,7 @@ impl EnvResults { } } - fn dotenv(p: &Path) -> Result { + async fn dotenv(p: &Path) -> Result { let errfn = || eyre!("failed to parse dotenv file: {}", display_path(p)); let mut env = EnvMap::new(); if let Ok(dotenv) = dotenvy::from_path_iter(p) { diff --git a/src/config/env_directive/mod.rs b/src/config/env_directive/mod.rs index 335cf7c208..352213b65e 100644 --- a/src/config/env_directive/mod.rs +++ b/src/config/env_directive/mod.rs @@ -14,6 +14,8 @@ use std::collections::{BTreeSet, HashMap}; use std::fmt::{Debug, Display, Formatter}; use std::path::{Path, PathBuf}; +use super::Config; + mod file; mod module; mod path; @@ -129,7 +131,8 @@ pub struct EnvResolveOptions { } impl EnvResults { - pub fn resolve( + pub async fn resolve( + config: &Config, mut ctx: tera::Context, initial: &EnvMap, input: Vec<(EnvDirective, PathBuf)>, @@ -236,7 +239,7 @@ impl EnvResults { r.env_remove.insert(k); } EnvDirective::Path(input_str, _opts) => { - let path = Self::path(&mut ctx, &mut tera, &mut r, &source, input_str)?; + let path = Self::path(&mut ctx, &mut tera, &mut r, &source, input_str).await?; paths.push((path.clone(), source.clone())); let env_path = env.get(&*env::PATH_KEY).cloned().unwrap_or_default().0; let mut env_path: PathEnv = env_path.parse()?; @@ -245,6 +248,7 @@ impl EnvResults { } EnvDirective::File(input, _opts) => { let files = Self::file( + config, &mut ctx, &mut tera, &mut r, @@ -252,7 +256,8 @@ impl EnvResults { &source, &config_root, input, - )?; + ) + .await?; for (f, new_env) in files { r.env_files.push(f.clone()); for (k, v) in new_env { @@ -304,6 +309,7 @@ impl EnvResults { options: _opts, } => { Self::venv( + config, &mut ctx, &mut tera, &mut env, @@ -317,10 +323,11 @@ impl EnvResults { python, uv_create_args, python_create_args, - )?; + ) + .await?; } EnvDirective::Module(name, value, _opts) => { - Self::module(&mut r, source, name, &value, redact)?; + Self::module(&mut r, source, name, &value, redact).await?; } }; } diff --git a/src/config/env_directive/module.rs b/src/config/env_directive/module.rs index 8acb1e995b..48f29c4eff 100644 --- a/src/config/env_directive/module.rs +++ b/src/config/env_directive/module.rs @@ -7,7 +7,7 @@ use std::path::PathBuf; use toml::Value; impl EnvResults { - pub fn module( + pub async fn module( r: &mut EnvResults, source: PathBuf, name: String, @@ -16,7 +16,7 @@ impl EnvResults { ) -> Result<()> { let path = dirs::PLUGINS.join(name.to_kebab_case()); let plugin = VfoxPlugin::new(name, path); - if let Some(env) = plugin.mise_env(value)? { + if let Some(env) = plugin.mise_env(value).await? { for (k, v) in env { if redact { r.redactions.push(k.clone()); @@ -24,7 +24,7 @@ impl EnvResults { r.env.insert(k, (v, source.clone())); } } - if let Some(path) = plugin.mise_path(value)? { + if let Some(path) = plugin.mise_path(value).await? { for p in path { r.env_paths.push(p.into()); } diff --git a/src/config/env_directive/path.rs b/src/config/env_directive/path.rs index f473127378..5b58324e69 100644 --- a/src/config/env_directive/path.rs +++ b/src/config/env_directive/path.rs @@ -3,7 +3,7 @@ use crate::result; use std::path::{Path, PathBuf}; impl EnvResults { - pub fn path( + pub async fn path( ctx: &mut tera::Context, tera: &mut tera::Tera, r: &mut EnvResults, @@ -19,19 +19,23 @@ impl EnvResults { #[cfg(unix)] mod tests { use super::*; - use crate::config::env_directive::{EnvDirective, EnvResolveOptions}; + use crate::config::{ + Config, + env_directive::{EnvDirective, EnvResolveOptions}, + }; use crate::env_diff::EnvMap; use crate::tera::BASE_CONTEXT; use crate::test::replace_path; use insta::assert_debug_snapshot; - use test_log::test; - #[test] - fn test_env_path() { + #[tokio::test] + async fn test_env_path() { let mut env = EnvMap::new(); env.insert("A".to_string(), "1".to_string()); env.insert("B".to_string(), "2".to_string()); + let config = Config::get().await; let results = EnvResults::resolve( + &config, BASE_CONTEXT.clone(), &env, vec![ @@ -57,6 +61,7 @@ mod tests { ], EnvResolveOptions::default(), ) + .await .unwrap(); assert_debug_snapshot!( results.env_paths.into_iter().map(|p| replace_path(&p.display().to_string())).collect::>(), diff --git a/src/config/env_directive/venv.rs b/src/config/env_directive/venv.rs index 93b46b56aa..3eb1f3715a 100644 --- a/src/config/env_directive/venv.rs +++ b/src/config/env_directive/venv.rs @@ -14,7 +14,8 @@ use std::path::{Path, PathBuf}; impl EnvResults { #[allow(clippy::too_many_arguments)] - pub fn venv( + pub async fn venv( + config: &Config, ctx: &mut tera::Context, tera: &mut tera::Tera, env: &mut IndexMap)>, @@ -37,8 +38,7 @@ impl EnvResults { if !venv.exists() && create { // TODO: the toolset stuff doesn't feel like it's in the right place here // TODO: in fact this should probably be moved to execute at the same time as src/uv.rs runs in ts.env() instead of config.env() - let config = Config::get(); - let ts = ToolsetBuilder::new().build(&config)?; + let ts = Box::pin(ToolsetBuilder::new().build(config)).await?; let ba = BackendArg::from("python"); let tv = ts.versions.get(&ba).and_then(|tv| { // if a python version is specified, check if that version is installed @@ -54,14 +54,13 @@ impl EnvResults { .to_string_lossy() .to_string() }); - let installed = tv - .map(|tv: &crate::toolset::ToolVersion| { - let backend = backend::get(&ba).unwrap(); - backend.is_version_installed(tv, false) - }) - // if no version is specified, we're assuming python3 is provided outside of mise - // so return "true" here - .unwrap_or(true); + let installed = if let Some(tv) = tv { + let backend = backend::get(&ba).unwrap(); + backend.is_version_installed(config, tv, false) + } else { + // if no version is specified, we're assuming python3 is provided outside of mise so return "true" here + true + }; if !installed { warn!( "no venv found at: {p}\n\n\ @@ -71,7 +70,10 @@ impl EnvResults { p = display_path(&venv) ); } else { - let uv_bin = ts.which_bin("uv").or_else(|| which_non_pristine("uv")); + let uv_bin = ts + .which_bin("uv") + .await + .or_else(|| which_non_pristine("uv")); let use_uv = !SETTINGS.python.venv_stdlib && uv_bin.is_some(); let cmd = if use_uv { info!("creating venv with uv at: {}", display_path(&venv)); @@ -151,12 +153,13 @@ mod tests { use crate::tera::BASE_CONTEXT; use crate::test::replace_path; use insta::assert_debug_snapshot; - use test_log::test; - #[test] - fn test_venv_path() { + #[tokio::test] + async fn test_venv_path() { let env = EnvMap::new(); + let config = Config::get().await; let results = EnvResults::resolve( + &config, BASE_CONTEXT.clone(), &env, vec![ @@ -194,6 +197,7 @@ mod tests { ..Default::default() }, ) + .await .unwrap(); // expect order to be reversed as it processes directives from global to dir specific assert_debug_snapshot!( diff --git a/src/config/mod.rs b/src/config/mod.rs index d08921a3e9..acb66bbb9b 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -2,8 +2,6 @@ use config_file::ConfigFileType; use eyre::{Context, Result, bail, eyre}; use indexmap::{IndexMap, IndexSet}; use itertools::Itertools; -use once_cell::sync::OnceCell; -use rayon::prelude::*; pub use settings::Settings; use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::fmt::{Debug, Formatter}; @@ -11,11 +9,11 @@ use std::iter::once; use std::ops::Deref; use std::path::{Path, PathBuf}; use std::sync::LazyLock as Lazy; -use std::sync::{Arc, Mutex, OnceLock, RwLock}; +use std::sync::{Arc, Mutex, RwLock}; use std::time::Duration; +use tokio::{sync::OnceCell, task::JoinSet}; use walkdir::WalkDir; -use crate::cli::version; use crate::config::config_file::idiomatic_version::IdiomaticVersionFile; use crate::config::config_file::mise_toml::{MiseToml, Tasks}; use crate::config::config_file::{ConfigFile, config_trust_root}; @@ -31,6 +29,7 @@ use crate::toolset::{ use crate::ui::style; use crate::{backend, dirs, env, file, lockfile, registry, runtime_symlinks, shims, timeout}; use crate::{backend::ABackend, cli::version::VERSION}; +use crate::{backend::Backend, cli::version}; pub mod config_file; pub mod env_directive; @@ -48,7 +47,7 @@ use crate::wildcard::Wildcard; pub use settings::SETTINGS; type AliasMap = IndexMap; -type ConfigMap = IndexMap>; +type ConfigMap = IndexMap>; pub type EnvWithSources = IndexMap; pub struct Config { @@ -58,10 +57,10 @@ pub struct Config { pub repo_urls: HashMap, pub vars: IndexMap, pub tera_ctx: tera::Context, + pub shorthands: Shorthands, aliases: AliasMap, env: OnceCell, env_with_sources: OnceCell, - shorthands: OnceLock, hooks: OnceCell>, tasks: OnceCell>, tool_request_set: OnceCell, @@ -82,70 +81,81 @@ pub fn is_loaded() -> bool { } impl Config { - pub fn get() -> Arc { - Self::try_get().unwrap() + pub async fn get() -> Arc { + Self::try_get().await.unwrap() } - pub fn try_get() -> Result> { + pub fn get_() -> Arc { + (*_CONFIG.read().unwrap()).clone().unwrap() + } + pub async fn try_get() -> Result> { if let Some(config) = &*_CONFIG.read().unwrap() { return Ok(config.clone()); } - let config = Arc::new(Self::load()?); + let config = measure!("load config", { Arc::new(Self::load().await?) }); *_CONFIG.write().unwrap() = Some(config.clone()); Ok(config) } - pub fn load() -> Result { + pub async fn load() -> Result { reset(); - time!("load start"); - let idiomatic_files = load_idiomatic_files(); - time!("load idiomatic_files"); + let idiomatic_files = measure!("config::load idiomatic_files", { + load_idiomatic_files().await + }); let config_filenames = idiomatic_files .keys() .chain(DEFAULT_CONFIG_FILENAMES.iter()) .cloned() .collect_vec(); - time!("load config_filenames"); - let config_paths = load_config_paths(&config_filenames, false); - time!("load config_paths"); + let config_paths = measure!("config::load config_paths", { + load_config_paths(&config_filenames, false) + }); trace!("config_paths: {config_paths:?}"); - let config_files = load_all_config_files(&config_paths, &idiomatic_files)?; - time!("load config_files"); - warn_about_idiomatic_version_files(&config_files); - - let mut tera_ctx = BASE_CONTEXT.clone(); - let vars_results = load_vars(tera_ctx.clone(), &config_files)?; - let vars: IndexMap = vars_results - .vars - .iter() - .map(|(k, (v, _))| (k.clone(), v.clone())) - .collect(); - tera_ctx.insert("vars", &vars); + let config_files = measure!("config::load config_files", { + load_all_config_files(&config_paths, &idiomatic_files)? + }); + measure!("config::load warn_about_idiomatic_version_files", { + warn_about_idiomatic_version_files(&config_files); + }); let mut config = Self { - aliases: load_aliases(&config_files)?, - project_root: get_project_root(&config_files), - repo_urls: load_plugins(&config_files)?, - tera_ctx, - vars: vars.clone(), + tera_ctx: BASE_CONTEXT.clone(), config_files, env: OnceCell::new(), env_with_sources: OnceCell::new(), - shorthands: OnceLock::new(), + shorthands: get_shorthands(&SETTINGS), hooks: OnceCell::new(), tasks: OnceCell::new(), tool_request_set: OnceCell::new(), toolset: OnceCell::new(), all_aliases: Default::default(), + aliases: Default::default(), + project_root: Default::default(), + repo_urls: Default::default(), + vars: Default::default(), }; - time!("load build"); - - config.validate()?; - time!("load validate"); + let vars_results = measure!("config::load vars_results", { load_vars(&config).await? }); + let vars: IndexMap = vars_results + .vars + .iter() + .map(|(k, (v, _))| (k.clone(), v.clone())) + .collect(); + config.tera_ctx.insert("vars", &vars); + + config.vars = vars; + config.aliases = load_aliases(&config.config_files)?; + config.project_root = get_project_root(&config.config_files); + config.repo_urls = load_plugins(&config.config_files)?; + measure!("config::load validate", { + config.validate()?; + }); - config.all_aliases = config.load_all_aliases(); - time!("load all aliases"); + config.all_aliases = measure!("config::load all_aliases", { config.load_all_aliases() }); - config.add_redactions(config.redaction_keys(), &vars.into_iter().collect()); - time!("load redactions"); + measure!("config::load redactions", { + config.add_redactions( + config.redaction_keys(), + &config.vars.clone().into_iter().collect(), + ); + }); if log::log_enabled!(log::Level::Trace) { trace!("config: {config:#?}"); @@ -157,24 +167,28 @@ impl Config { time!("load done"); - for (plugin, url) in &config.repo_urls { - let plugin_type = match url.contains("vfox-") { - true => PluginType::Vfox, - false => PluginType::Asdf, - }; - install_state::add_plugin(plugin, plugin_type)?; - } + measure!("config::load install_state", { + for (plugin, url) in &config.repo_urls { + let plugin_type = match url.contains("vfox-") { + true => PluginType::Vfox, + false => PluginType::Asdf, + }; + install_state::add_plugin(plugin, plugin_type)?; + } + }); - for short in config - .all_aliases - .iter() - .filter(|(_, a)| a.backend.is_some()) - .map(|(s, _)| s) - .chain(config.repo_urls.keys()) - { - // we need to remove aliased tools so they get re-added with updated "full" values - backend::remove(short); - } + measure!("config::load remove_aliased_tools", { + for short in config + .all_aliases + .iter() + .filter(|(_, a)| a.backend.is_some()) + .map(|(s, _)| s) + .chain(config.repo_urls.keys()) + { + // we need to remove aliased tools so they get re-added with updated "full" values + backend::remove(short); + } + }); Ok(config) } @@ -185,53 +199,58 @@ impl Config { .collect() }) } - pub fn env(&self) -> eyre::Result> { + pub async fn env(&self) -> eyre::Result> { Ok(self - .env_with_sources()? + .env_with_sources() + .await? .iter() .map(|(k, (v, _))| (k.clone(), v.clone())) .collect()) } - pub fn env_with_sources(&self) -> eyre::Result<&EnvWithSources> { - self.env_with_sources.get_or_try_init(|| { - let mut env = self.env_results()?.env.clone(); - for env_file in SETTINGS.env_files() { - match dotenvy::from_path_iter(&env_file) { - Ok(iter) => { - for item in iter { - let (k, v) = item.unwrap_or_else(|err| { - warn!("env_file: {err}"); - Default::default() - }); - env.insert(k, (v, env_file.clone())); + pub async fn env_with_sources(&self) -> eyre::Result<&EnvWithSources> { + self.env_with_sources + .get_or_try_init(async || { + let mut env = self.env_results().await?.env.clone(); + for env_file in SETTINGS.env_files() { + match dotenvy::from_path_iter(&env_file) { + Ok(iter) => { + for item in iter { + let (k, v) = item.unwrap_or_else(|err| { + warn!("env_file: {err}"); + Default::default() + }); + env.insert(k, (v, env_file.clone())); + } } + Err(err) => trace!("env_file: {err}"), } - Err(err) => trace!("env_file: {err}"), } - } - Ok(env) - }) - } - pub fn env_results(&self) -> Result<&EnvResults> { - self.env.get_or_try_init(|| self.load_env()) + Ok(env) + }) + .await } - pub fn path_dirs(&self) -> eyre::Result<&Vec> { - Ok(&self.env_results()?.env_paths) + pub async fn env_results(&self) -> Result<&EnvResults> { + self.env + .get_or_try_init(|| async { self.load_env().await }) + .await } - pub fn get_shorthands(&self) -> &Shorthands { - self.shorthands.get_or_init(|| get_shorthands(&SETTINGS)) + pub async fn path_dirs(&self) -> eyre::Result<&Vec> { + Ok(&self.env_results().await?.env_paths) } - pub fn get_tool_request_set(&self) -> eyre::Result<&ToolRequestSet> { + pub async fn get_tool_request_set(&self) -> eyre::Result<&ToolRequestSet> { self.tool_request_set - .get_or_try_init(|| ToolRequestSetBuilder::new().build()) + .get_or_try_init(async || ToolRequestSetBuilder::new().build(self).await) + .await } - pub fn get_toolset(&self) -> Result<&Toolset> { - self.toolset.get_or_try_init(|| { - let mut ts = Toolset::from(self.get_tool_request_set()?.clone()); - ts.resolve()?; - Ok(ts) - }) + pub async fn get_toolset(&self) -> Result<&Toolset> { + self.toolset + .get_or_try_init(|| async { + let mut ts = Toolset::from(self.get_tool_request_set().await?.clone()); + ts.resolve().await?; + Ok(ts) + }) + .await } pub fn get_repo_url(&self, plugin_name: &str) -> Option { @@ -243,7 +262,7 @@ impl Config { .unwrap_or(plugin_name.to_string()); let plugin_name = plugin_name.strip_prefix("asdf:").unwrap_or(&plugin_name); let plugin_name = plugin_name.strip_prefix("vfox:").unwrap_or(plugin_name); - self.get_shorthands() + self.shorthands .get(plugin_name) .map(|full| registry::full_to_url(&full[0])) .or_else(|| { @@ -255,13 +274,16 @@ impl Config { }) } - pub fn tasks(&self) -> Result<&BTreeMap> { - self.tasks.get_or_try_init(|| self.load_all_tasks()) + pub async fn tasks(&self) -> Result<&BTreeMap> { + self.tasks + .get_or_try_init(|| async { self.load_all_tasks().await }) + .await } - pub fn tasks_with_aliases(&self) -> Result> { + pub async fn tasks_with_aliases(&self) -> Result> { Ok(self - .tasks()? + .tasks() + .await? .iter() .flat_map(|(_, t)| { t.aliases @@ -273,7 +295,7 @@ impl Config { .collect()) } - pub fn resolve_alias(&self, backend: &ABackend, v: &str) -> Result { + pub async fn resolve_alias(&self, backend: &ABackend, v: &str) -> Result { if let Some(plugin_aliases) = self.all_aliases.get(&backend.ba().short) { if let Some(alias) = plugin_aliases.versions.get(v) { return Ok(alias.clone()); @@ -288,7 +310,7 @@ impl Config { fn load_all_aliases(&self) -> AliasMap { let mut aliases: AliasMap = self.aliases.clone(); let plugin_aliases: Vec<_> = backend::list() - .into_par_iter() + .into_iter() .map(|backend| { let aliases = backend.get_aliases().unwrap_or_else(|err| { warn!("get_aliases: {err}"); @@ -320,23 +342,17 @@ impl Config { aliases } - fn load_all_tasks(&self) -> Result> { + async fn load_all_tasks(&self) -> Result> { time!("load_all_tasks"); - let mut file_tasks = None; - let mut global_tasks = None; - let mut system_tasks = None; - rayon::scope(|s| { - s.spawn(|_| { - file_tasks = Some(self.load_local_tasks()); - }); - global_tasks = Some(self.load_global_tasks()); - system_tasks = Some(self.load_system_tasks()); - }); - let tasks: BTreeMap = file_tasks - .unwrap()? + let (file_tasks, global_tasks, system_tasks) = tokio::join!( + load_local_tasks(self), + load_global_tasks(self), + load_system_tasks(self), + ); + let mut tasks: BTreeMap = file_tasks? .into_iter() - .chain(global_tasks.unwrap()?) - .chain(system_tasks.unwrap()?) + .chain(global_tasks) + .chain(system_tasks) .rev() .inspect(|t| { trace!( @@ -347,201 +363,17 @@ impl Config { }) .map(|t| (t.name.clone(), t)) .collect(); - time!("load_all_tasks {count}", count = tasks.len(),); - Ok(tasks) - } - - pub fn task_includes_for_dir(&self, dir: &Path) -> Vec { - self.configs_at_root(dir) - .iter() - .rev() - .find_map(|cf| cf.task_config().includes.clone()) - .unwrap_or_else(default_task_includes) - .into_par_iter() - .map(|p| if p.is_absolute() { p } else { dir.join(p) }) - .filter(|p| p.exists()) - .collect::>() - .into_iter() - .unique() - .collect::>() - } - - pub fn load_tasks_in_dir(&self, dir: &Path) -> Result> { - let configs = self.configs_at_root(dir); - let config_tasks = configs - .par_iter() - .flat_map(|cf| self.load_config_tasks(&Some(*cf), dir)) - .collect::>(); - let includes = self.task_includes_for_dir(dir); - let extra_tasks = includes - .par_iter() - .filter(|p| { - p.is_file() && p.extension().unwrap_or_default().to_string_lossy() == "toml" - }) - .map(|p| { - self.load_task_file(p, dir) - .wrap_err_with(|| format!("loading tasks in {}", display_path(p))) - }) - .collect::>>()? - .into_iter() - .flatten() - .collect::>(); - let file_tasks = includes - .into_par_iter() - .flat_map(|p| { - self.load_tasks_includes(&p, dir).unwrap_or_else(|err| { - warn!("loading tasks in {}: {err}", display_path(&p)); - vec![] - }) - }) - .collect::>(); - Ok(file_tasks - .into_iter() - .chain(config_tasks) - .chain(extra_tasks) - .sorted_by_cached_key(|t| t.name.clone()) - .collect()) - } - - fn load_task_file(&self, path: &Path, config_root: &Path) -> Result> { - let raw = file::read_to_string(path)?; - let mut tasks = toml::from_str::(&raw) - .wrap_err_with(|| format!("Error parsing task file: {}", display_path(path)))? - .0; - for (name, task) in &mut tasks { - task.name = name.clone(); - task.config_source = path.to_path_buf(); - task.config_root = Some(config_root.to_path_buf()); + let all_tasks = tasks.clone(); + for task in tasks.values_mut() { + task.display_name = task.display_name(&all_tasks); } - Ok(tasks - .into_values() - .map(|mut t| { - if let Err(err) = t.render(config_root) { - warn!("rendering task: {err:?}"); - } - t - }) - .collect()) - } - - fn load_local_tasks(&self) -> Result> { - Ok(file::all_dirs()? - .into_par_iter() - .filter(|d| { - if cfg!(test) { - d.starts_with(*dirs::HOME) - } else { - true - } - }) - .map(|d| self.load_tasks_in_dir(&d)) - .collect::>>()? - .into_iter() - .flatten() - .collect()) - } - - fn load_global_tasks(&self) -> Result> { - let tasks = self - .config_files - .values() - .filter(|cf| is_global_config(cf.get_path())) - .flat_map(|cf| self.load_config_and_file_tasks(cf.as_ref())) - .collect(); - Ok(tasks) - } - - fn load_system_tasks(&self) -> Result> { - let tasks = self - .config_files - .values() - .filter(|cf| is_system_config(cf.get_path())) - .flat_map(|cf| self.load_config_and_file_tasks(cf.as_ref())) - .collect(); + time!("load_all_tasks {count}", count = tasks.len(),); Ok(tasks) } - fn load_config_and_file_tasks(&self, cf: &dyn ConfigFile) -> Vec { - let project_root = cf.project_root().unwrap_or(&*env::HOME); - let cf = Some(cf); - let tasks = self.load_config_tasks(&cf, project_root); - let file_tasks = self.load_file_tasks(&cf, project_root); - tasks.into_iter().chain(file_tasks).collect() - } - - fn load_config_tasks(&self, cf: &Option<&dyn ConfigFile>, config_root: &Path) -> Vec { - let Some(cf) = cf else { - return vec![]; - }; - let is_global = is_global_config(cf.get_path()); - cf.tasks() - .into_iter() - .cloned() - .map(|mut t| { - if let Err(err) = t.render(config_root) { - warn!("rendering task: {err:?}"); - } - t.global = is_global; - t - }) - .collect() - } - - fn load_file_tasks(&self, cf: &Option<&dyn ConfigFile>, config_root: &Path) -> Vec { - let includes = match cf { - Some(cf) => cf - .task_config() - .includes - .clone() - .unwrap_or(vec!["tasks".into()]) - .into_iter() - .map(|p| cf.get_path().parent().unwrap().join(p)) - .collect(), - None => vec![dirs::CONFIG.join("tasks")], - }; - includes - .into_iter() - .flat_map(|p| { - self.load_tasks_includes(&p, config_root) - .unwrap_or_else(|err| { - warn!("loading tasks in {}: {err}", display_path(&p)); - vec![] - }) - }) - .collect() - } - - fn load_tasks_includes(&self, root: &Path, config_root: &Path) -> Result> { - if !root.is_dir() { - return Ok(vec![]); - } - WalkDir::new(root) - .follow_links(true) - .into_iter() - // skip hidden directories (if the root is hidden that's ok) - .filter_entry(|e| e.path() == root || !e.file_name().to_string_lossy().starts_with('.')) - .filter_ok(|e| e.file_type().is_file()) - .map_ok(|e| e.path().to_path_buf()) - .try_collect::<_, Vec, _>()? - .into_par_iter() - .filter(|p| file::is_executable(p)) - .filter(|p| !SETTINGS.task_disable_paths.iter().any(|d| p.starts_with(d))) - .map(|path| Task::from_path(&path, root, config_root)) - .collect() - } - - fn configs_at_root(&self, dir: &Path) -> Vec<&dyn ConfigFile> { - DEFAULT_CONFIG_FILENAMES - .iter() - .rev() - .map(|f| dir.join(f)) - .filter_map(|f| self.config_files.get(&f).map(|cf| cf.as_ref())) - .collect() - } - pub fn get_tracked_config_files(&self) -> Result { let config_files = Tracker::list_all()? - .into_par_iter() + .into_iter() .map(|path| match config_file::parse(&path) { Ok(cf) => Some((path, cf)), Err(err) => { @@ -589,7 +421,7 @@ impl Config { Ok(()) } - fn load_env(&self) -> Result { + async fn load_env(&self) -> Result { time!("load_env start"); let entries = self .config_files @@ -605,11 +437,13 @@ impl Config { .collect(); // trace!("load_env: entries: {:#?}", entries); let env_results = EnvResults::resolve( + self, self.tera_ctx.clone(), &env::PRISTINE_ENV, entries, EnvResolveOptions::default(), - )?; + ) + .await?; let redact_keys = self .redaction_keys() .into_iter() @@ -631,21 +465,23 @@ impl Config { Ok(env_results) } - pub fn hooks(&self) -> Result<&Vec<(PathBuf, Hook)>> { - self.hooks.get_or_try_init(|| { - self.config_files - .values() - .map(|cf| Ok((cf.project_root(), cf.hooks()?))) - .filter_map_ok(|(root, hooks)| root.map(|r| (r.to_path_buf(), hooks))) - .map_ok(|(root, hooks)| { - hooks - .into_iter() - .map(|h| (root.clone(), h)) - .collect::>() - }) - .flatten_ok() - .collect() - }) + pub async fn hooks(&self) -> Result<&Vec<(PathBuf, Hook)>> { + self.hooks + .get_or_try_init(|| async { + self.config_files + .values() + .map(|cf| Ok((cf.project_root(), cf.hooks()?))) + .filter_map_ok(|(root, hooks)| root.map(|r| (r.to_path_buf(), hooks))) + .map_ok(|(root, hooks)| { + hooks + .into_iter() + .map(|h| (root.clone(), h)) + .collect::>() + }) + .flatten_ok() + .collect() + }) + .await } pub fn watch_file_hooks(&self) -> Result> { @@ -665,8 +501,8 @@ impl Config { .collect()) } - pub fn watch_files(&self) -> Result> { - let env_results = self.env_results()?; + pub async fn watch_files(&self) -> Result> { + let env_results = self.env_results().await?; Ok(self .config_files .iter() @@ -755,6 +591,15 @@ impl Config { } } +fn configs_at_root<'a>(dir: &Path, config_files: &'a ConfigMap) -> Vec<&'a Arc> { + DEFAULT_CONFIG_FILENAMES + .iter() + .rev() + .map(|f| dir.join(f)) + .filter_map(|f| config_files.get(&f)) + .collect() +} + fn get_project_root(config_files: &ConfigMap) -> Option { let project_root = config_files .values() @@ -764,7 +609,7 @@ fn get_project_root(config_files: &ConfigMap) -> Option { project_root } -fn load_idiomatic_files() -> BTreeMap> { +async fn load_idiomatic_files() -> BTreeMap> { if !SETTINGS.idiomatic_version_file { return BTreeMap::new(); } @@ -774,35 +619,41 @@ fn load_idiomatic_files() -> BTreeMap> { "is deprecated, use idiomatic_version_file_enable_tools instead" ); } - let idiomatic = backend::list() - .into_par_iter() - .filter(|tool| { - if let Some(enable_tools) = &SETTINGS.idiomatic_version_file_enable_tools { - enable_tools.contains(tool.id()) - } else if !SETTINGS.idiomatic_version_file_disable_tools.is_empty() { - !SETTINGS - .idiomatic_version_file_disable_tools - .contains(tool.id()) - } else { - true + let mut jset = JoinSet::new(); + let tool_is_enabled = |tool: &dyn Backend| { + if let Some(enable_tools) = &SETTINGS.idiomatic_version_file_enable_tools { + enable_tools.contains(tool.id()) + } else if !SETTINGS.idiomatic_version_file_disable_tools.is_empty() { + !SETTINGS + .idiomatic_version_file_disable_tools + .contains(tool.id()) + } else { + true + } + }; + for tool in backend::list() { + jset.spawn(async move { + if !tool_is_enabled(&*tool) { + return vec![]; } - }) - .filter_map(|tool| match tool.idiomatic_filenames() { - Ok(filenames) => Some( - filenames + match tool.idiomatic_filenames() { + Ok(filenames) => filenames .iter() .map(|f| (f.to_string(), tool.id().to_string())) - .collect_vec(), - ), - Err(err) => { - eprintln!("Error: {err}"); - None + .collect::>(), + Err(err) => { + eprintln!("Error: {err}"); + vec![] + } } - }) - .collect::>>() + }); + } + let idiomatic = jset + .join_all() + .await .into_iter() .flatten() - .collect::>(); + .collect::>(); let mut idiomatic_filenames = BTreeMap::new(); for (filename, plugin) in idiomatic { @@ -948,7 +799,7 @@ pub fn load_config_paths(config_filenames: &[String], include_ignored: bool) -> let dirs = file::all_dirs().unwrap_or_default(); let mut config_files = dirs - .par_iter() + .iter() .flat_map(|dir| { if !include_ignored && env::MISE_IGNORED_CONFIG_PATHS @@ -1103,8 +954,6 @@ fn load_all_config_files( Ok(config_filenames .iter() .unique() - .collect_vec() - .into_par_iter() .map(|f| { if f.is_dir() { return Ok(None); @@ -1136,7 +985,7 @@ fn load_all_config_files( fn parse_config_file( f: &PathBuf, idiomatic_filenames: &BTreeMap>, -) -> Result> { +) -> Result> { match idiomatic_filenames.get(&f.file_name().unwrap().to_string_lossy().to_string()) { Some(plugin) => { trace!("idiomatic version file: {}", display_path(f)); @@ -1144,7 +993,7 @@ fn parse_config_file( .into_iter() .filter(|f| plugin.contains(&f.to_string())) .collect::>(); - IdiomaticVersionFile::parse(f.into(), tools).map(|f| Box::new(f) as Box) + IdiomaticVersionFile::parse(f.into(), tools).map(|f| Arc::new(f) as Arc) } None => config_file::parse(f), } @@ -1180,9 +1029,10 @@ fn load_plugins(config_files: &ConfigMap) -> Result> { Ok(plugins) } -fn load_vars(ctx: tera::Context, config_files: &ConfigMap) -> Result { +async fn load_vars(config: &Config) -> Result { time!("load_vars start"); - let entries = config_files + let entries = config + .config_files .iter() .rev() .map(|(source, cf)| { @@ -1194,14 +1044,16 @@ fn load_vars(ctx: tera::Context, config_files: &ConfigMap) -> Result .flatten() .collect(); let vars_results = EnvResults::resolve( - ctx, + config, + config.tera_ctx.clone(), &env::PRISTINE_ENV, entries, EnvResolveOptions { vars: true, ..Default::default() }, - )?; + ) + .await?; time!("load_vars done"); if log::log_enabled!(log::Level::Trace) { trace!("{vars_results:#?}"); @@ -1260,16 +1112,23 @@ fn default_task_includes() -> Vec { ] } -pub fn rebuild_shims_and_runtime_symlinks(new_versions: &[ToolVersion]) -> Result<()> { - let config = Config::load()?; - let ts = ToolsetBuilder::new().build(&config)?; - trace!("rebuilding shims"); - shims::reshim(&ts, false).wrap_err("failed to rebuild shims")?; - trace!("rebuilding runtime symlinks"); - runtime_symlinks::rebuild(&config).wrap_err("failed to rebuild runtime symlinks")?; - trace!("updating lockfiles"); - lockfile::update_lockfiles(&config, &ts, new_versions) - .wrap_err("failed to update lockfiles")?; +pub async fn rebuild_shims_and_runtime_symlinks(new_versions: &[ToolVersion]) -> Result<()> { + let config = Arc::new(Config::load().await?); + let ts = measure!("build_toolset", { + ToolsetBuilder::new().build(&config).await? + }); + measure!("rebuilding shims", { + shims::reshim(&ts, false) + .await + .wrap_err("failed to rebuild shims")?; + }); + measure!("rebuilding runtime symlinks", { + runtime_symlinks::rebuild(&config).wrap_err("failed to rebuild runtime symlinks")?; + }); + measure!("updating lockfiles", { + lockfile::update_lockfiles(&config, &ts, new_versions) + .wrap_err("failed to update lockfiles")?; + }); Ok(()) } @@ -1332,6 +1191,249 @@ fn reset() { } } +async fn load_local_tasks(config: &Config) -> Result> { + let mut tasks = vec![]; + for d in file::all_dirs()? { + if cfg!(test) && !d.starts_with(*dirs::HOME) { + continue; + } + tasks.extend(load_tasks_in_dir(&d, &config.config_files).await?); + } + Ok(tasks) +} + +async fn load_global_tasks(config: &Config) -> Vec { + let global_config_files = config + .config_files + .values() + .filter(|cf| is_global_config(cf.get_path())) + .collect::>(); + let mut jset = JoinSet::new(); + for (i, cf) in global_config_files.into_iter().enumerate() { + let cf = cf.clone(); + jset.spawn(async move { + let tasks = load_config_and_file_tasks(cf).await; + (i, tasks) + }); + } + jset.join_all() + .await + .into_iter() + .sorted_by_key(|(i, _)| *i) + .flat_map(|(_, tasks)| tasks) + .collect() +} + +async fn load_system_tasks(config: &Config) -> Vec { + let system_config_files = config + .config_files + .values() + .filter(|cf| is_system_config(cf.get_path())) + .collect::>(); + let mut jset = JoinSet::new(); + for (i, cf) in system_config_files.into_iter().enumerate() { + let cf = cf.clone(); + jset.spawn(async move { + let tasks = load_config_and_file_tasks(cf).await; + (i, tasks) + }); + } + jset.join_all() + .await + .into_iter() + .sorted_by_key(|(i, _)| *i) + .flat_map(|(_, tasks)| tasks) + .collect() +} + +async fn load_config_and_file_tasks(cf: Arc) -> Vec { + let project_root = cf.project_root().unwrap_or(&*env::HOME); + let tasks = load_config_tasks(cf.clone(), project_root).await; + let file_tasks = load_file_tasks(cf.clone(), project_root).await; + tasks.into_iter().chain(file_tasks).collect() +} + +async fn load_config_tasks(cf: Arc, config_root: &Path) -> Vec { + let is_global = is_global_config(cf.get_path()); + let mut jset = JoinSet::new(); + let config_root = Arc::new(config_root.to_path_buf()); + for (i, t) in cf.tasks().into_iter().enumerate() { + let config_root = config_root.clone(); + let mut t = t.clone(); + jset.spawn(async move { + if is_global { + t.global = true; + } + match t.render(&config_root).await { + Ok(()) => Some((i, t)), + Err(e) => { + warn!("rendering task: {e:?}"); + None + } + } + }); + } + jset.join_all() + .await + .into_iter() + .flatten() + .sorted_by_key(|(i, _)| *i) + .map(|(_, t)| t) + .collect() +} + +async fn load_tasks_includes(root: &Path, config_root: &Path) -> Result> { + if !root.is_dir() { + return Ok(vec![]); + } + let files = WalkDir::new(root) + .follow_links(true) + .into_iter() + // skip hidden directories (if the root is hidden that's ok) + .filter_entry(|e| e.path() == root || !e.file_name().to_string_lossy().starts_with('.')) + .filter_ok(|e| e.file_type().is_file()) + .map_ok(|e| e.path().to_path_buf()) + .try_collect::<_, Vec, _>()? + .into_iter() + .filter(|p| file::is_executable(p)) + .filter(|p| !SETTINGS.task_disable_paths.iter().any(|d| p.starts_with(d))) + .collect::>(); + let mut jset = JoinSet::new(); + let root = Arc::new(root.to_path_buf()); + let config_root = Arc::new(config_root.to_path_buf()); + for path in files { + let root = root.clone(); + let config_root = config_root.clone(); + jset.spawn(async move { Task::from_path(&path, &root, &config_root).await }); + } + jset.join_all().await.into_iter().collect() +} + +async fn load_file_tasks(cf: Arc, config_root: &Path) -> Vec { + let includes = cf + .task_config() + .includes + .clone() + .unwrap_or(vec!["tasks".into()]) + .into_iter() + .map(|p| cf.get_path().parent().unwrap().join(p)) + .collect::>(); + let mut jset = JoinSet::new(); + let config_root = Arc::new(config_root.to_path_buf()); + for p in includes { + let config_root = config_root.clone(); + jset.spawn(async move { + match load_tasks_includes(&p, &config_root).await { + Ok(tasks) => tasks, + Err(e) => { + warn!("loading tasks in {}: {e}", display_path(&p)); + vec![] + } + } + }); + } + jset.join_all().await.into_iter().flatten().collect() +} + +pub fn task_includes_for_dir(dir: &Path, config_files: &ConfigMap) -> Vec { + configs_at_root(dir, config_files) + .iter() + .rev() + .find_map(|cf| cf.task_config().includes.clone()) + .unwrap_or_else(default_task_includes) + .into_iter() + .map(|p| if p.is_absolute() { p } else { dir.join(p) }) + .filter(|p| p.exists()) + .collect::>() + .into_iter() + .unique() + .collect::>() +} + +pub async fn load_tasks_in_dir(dir: &Path, config_files: &ConfigMap) -> Result> { + let configs = configs_at_root(dir, config_files); + let mut config_tasks = vec![]; + for cf in configs { + let dir = dir.to_path_buf(); + config_tasks.extend(load_config_tasks(cf.clone(), &dir).await); + } + let includes = task_includes_for_dir(dir, config_files); + let extra_tasks = includes + .iter() + .filter(|p| p.is_file() && p.extension().unwrap_or_default().to_string_lossy() == "toml"); + let mut jset = JoinSet::new(); + for p in extra_tasks { + let p = p.clone(); + let dir = dir.to_path_buf(); + jset.spawn(async move { + load_task_file(&p, &dir) + .await + .wrap_err_with(|| format!("loading tasks in {}", display_path(&p))) + }); + } + let extra_tasks = jset + .join_all() + .await + .into_iter() + .collect::>>()? + .into_iter() + .flatten() + .collect::>(); + let mut jset = JoinSet::new(); + for p in includes { + let dir = dir.to_path_buf(); + let p = p.clone(); + jset.spawn(async move { + load_tasks_includes(&p, &dir).await.unwrap_or_else(|err| { + warn!("loading tasks in {}: {err}", display_path(&p)); + vec![] + }) + }); + } + let file_tasks = jset + .join_all() + .await + .into_iter() + .flatten() + .collect::>(); + let mut tasks = file_tasks + .into_iter() + .chain(config_tasks) + .chain(extra_tasks) + .sorted_by_cached_key(|t| t.name.clone()) + .collect::>(); + let all_tasks = tasks + .clone() + .into_iter() + .map(|t| (t.name.clone(), t)) + .collect::>(); + for task in tasks.iter_mut() { + task.display_name = task.display_name(&all_tasks); + } + Ok(tasks) +} + +async fn load_task_file(path: &Path, config_root: &Path) -> Result> { + let raw = file::read_to_string(path)?; + let mut tasks = toml::from_str::(&raw) + .wrap_err_with(|| format!("Error parsing task file: {}", display_path(path)))? + .0; + for (name, task) in &mut tasks { + task.name = name.clone(); + task.config_source = path.to_path_buf(); + task.config_root = Some(config_root.to_path_buf()); + } + let mut out = vec![]; + for (_, mut task) in tasks { + let config_root = config_root.to_path_buf(); + if let Err(err) = task.render(&config_root).await { + warn!("rendering task: {err:?}"); + } + out.push(task); + } + Ok(out) +} + #[cfg(test)] #[cfg(unix)] mod tests { @@ -1342,14 +1444,14 @@ mod tests { use super::*; - #[test] - fn test_load() { - let config = Config::load().unwrap(); + #[tokio::test] + async fn test_load() { + let config = Config::load().await.unwrap(); assert_debug_snapshot!(config); } - #[test] - fn test_load_all_config_files_skips_directories() -> Result<()> { + #[tokio::test] + async fn test_load_all_config_files_skips_directories() -> Result<()> { let temp_dir = TempDir::new()?; let temp_path = temp_dir.path(); diff --git a/src/file.rs b/src/file.rs index 19f585a3d6..a2b5a5e482 100644 --- a/src/file.rs +++ b/src/file.rs @@ -16,7 +16,6 @@ use eyre::bail; use filetime::{FileTime, set_file_times}; use flate2::read::GzDecoder; use itertools::Itertools; -use rayon::prelude::*; use std::sync::LazyLock as Lazy; use tar::Archive; use walkdir::WalkDir; @@ -93,6 +92,14 @@ pub fn remove_file>(path: P) -> Result<()> { fs::remove_file(path).wrap_err_with(|| format!("failed rm: {}", display_path(path))) } +pub async fn remove_file_async>(path: P) -> Result<()> { + let path = path.as_ref(); + trace!("rm {}", display_path(path)); + tokio::fs::remove_file(path) + .await + .wrap_err_with(|| format!("failed rm: {}", display_path(path))) +} + pub fn remove_dir>(path: P) -> Result<()> { let path = path.as_ref(); (|| -> Result<()> { @@ -170,6 +177,13 @@ pub fn write, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> trace!("write {}", display_path(path)); fs::write(path, contents).wrap_err_with(|| format!("failed write: {}", display_path(path))) } +pub async fn write_async, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> { + let path = path.as_ref(); + trace!("write {}", display_path(path)); + tokio::fs::write(path, contents) + .await + .wrap_err_with(|| format!("failed write: {}", display_path(path))) +} pub fn read_to_string>(path: P) -> Result { let path = path.as_ref(); @@ -384,11 +398,6 @@ pub fn resolve_symlink(link: &Path) -> Result { #[cfg(unix)] pub fn make_symlink_or_file(target: &Path, link: &Path) -> Result<()> { - trace!("ln -sf {} {}", target.display(), link.display()); - if link.is_file() || link.is_symlink() { - // remove existing file if exists - fs::remove_file(link)?; - } make_symlink(target, link)?; Ok(()) } @@ -456,6 +465,22 @@ pub fn make_executable>(_path: P) -> Result<()> { Ok(()) } +#[cfg(unix)] +pub async fn make_executable_async>(path: P) -> Result<()> { + trace!("chmod +x {}", display_path(&path)); + let path = path.as_ref(); + let mut perms = path.metadata()?.permissions(); + perms.set_mode(perms.mode() | 0o111); + tokio::fs::set_permissions(path, perms) + .await + .wrap_err_with(|| format!("failed to chmod +x: {}", display_path(path))) +} + +#[cfg(windows)] +pub async fn make_executable_async>(_path: P) -> Result<()> { + Ok(()) +} + pub fn all_dirs() -> Result> { let mut output = vec![]; let mut cwd = dirs::CWD.as_ref().map(|p| p.as_path()); @@ -551,7 +576,7 @@ pub fn which_non_pristine>(name: P) -> Option { fn _which>(name: P, paths: &[PathBuf]) -> Option { let name = name.as_ref(); - paths.par_iter().find_map_first(|path| { + paths.iter().find_map(|path| { let bin = path.join(name); if is_executable(&bin) { Some(bin) } else { None } }) diff --git a/src/github.rs b/src/github.rs index 07c700fb3a..9d8e9730fb 100644 --- a/src/github.rs +++ b/src/github.rs @@ -8,7 +8,8 @@ use serde_derive::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; use std::sync::LazyLock as Lazy; -use std::sync::{RwLock, RwLockReadGuard}; +use tokio::sync::RwLock; +use tokio::sync::RwLockReadGuard; use xx::regex; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -45,73 +46,77 @@ static TAGS_CACHE: Lazy>>> = Lazy::new(Default::de pub static API_URL: &str = "https://api.github.com"; -fn get_tags_cache(key: &str) -> RwLockReadGuard<'_, CacheGroup>> { +async fn get_tags_cache(key: &str) -> RwLockReadGuard<'_, CacheGroup>> { TAGS_CACHE .write() - .unwrap() + .await .entry(key.to_string()) .or_insert_with(|| { CacheManagerBuilder::new(cache_dir().join(format!("{key}-tags.msgpack.z"))) .with_fresh_duration(Some(duration::DAILY)) .build() }); - TAGS_CACHE.read().unwrap() + TAGS_CACHE.read().await } -fn get_releases_cache(key: &str) -> RwLockReadGuard<'_, CacheGroup>> { +async fn get_releases_cache(key: &str) -> RwLockReadGuard<'_, CacheGroup>> { RELEASES_CACHE .write() - .unwrap() + .await .entry(key.to_string()) .or_insert_with(|| { CacheManagerBuilder::new(cache_dir().join(format!("{key}-releases.msgpack.z"))) .with_fresh_duration(Some(duration::DAILY)) .build() }); - RELEASES_CACHE.read().unwrap() + RELEASES_CACHE.read().await } -fn get_release_cache<'a>(key: &str) -> RwLockReadGuard<'a, CacheGroup> { +async fn get_release_cache<'a>(key: &str) -> RwLockReadGuard<'a, CacheGroup> { RELEASE_CACHE .write() - .unwrap() + .await .entry(key.to_string()) .or_insert_with(|| { CacheManagerBuilder::new(cache_dir().join(format!("{key}.msgpack.z"))) .with_fresh_duration(Some(duration::DAILY)) .build() }); - RELEASE_CACHE.read().unwrap() + RELEASE_CACHE.read().await } -pub fn list_releases(repo: &str) -> Result> { +pub async fn list_releases(repo: &str) -> Result> { let key = repo.to_kebab_case(); - let cache = get_releases_cache(&key); + let cache = get_releases_cache(&key).await; let cache = cache.get(&key).unwrap(); Ok(cache - .get_or_try_init(|| list_releases_(API_URL, repo))? + .get_or_try_init_async(async || list_releases_(API_URL, repo).await) + .await? .to_vec()) } -pub fn list_releases_from_url(api_url: &str, repo: &str) -> Result> { +pub async fn list_releases_from_url(api_url: &str, repo: &str) -> Result> { let key = format!("{api_url}-{repo}").to_kebab_case(); - let cache = get_releases_cache(&key); + let cache = get_releases_cache(&key).await; let cache = cache.get(&key).unwrap(); Ok(cache - .get_or_try_init(|| list_releases_(api_url, repo))? + .get_or_try_init_async(async || list_releases_(api_url, repo).await) + .await? .to_vec()) } -fn list_releases_(api_url: &str, repo: &str) -> Result> { +async fn list_releases_(api_url: &str, repo: &str) -> Result> { let url = format!("{api_url}/repos/{repo}/releases"); let headers = get_headers(&url); let (mut releases, mut headers) = crate::http::HTTP_FETCH - .json_headers_with_headers::, _>(url, &headers)?; + .json_headers_with_headers::, _>(url, &headers) + .await?; if *env::MISE_LIST_ALL_VERSIONS { while let Some(next) = next_page(&headers) { let (more, h) = crate::http::HTTP_FETCH - .json_headers_with_headers::, _>(next, &headers)?; + .json_headers_with_headers::, _>(next, &headers) + .await?; releases.extend(more); headers = h; } @@ -121,34 +126,38 @@ fn list_releases_(api_url: &str, repo: &str) -> Result> { Ok(releases) } -pub fn list_tags(repo: &str) -> Result> { +pub async fn list_tags(repo: &str) -> Result> { let key = repo.to_kebab_case(); - let cache = get_tags_cache(&key); + let cache = get_tags_cache(&key).await; let cache = cache.get(&key).unwrap(); Ok(cache - .get_or_try_init(|| list_tags_(API_URL, repo))? + .get_or_try_init_async(async || list_tags_(API_URL, repo).await) + .await? .to_vec()) } -pub fn list_tags_from_url(api_url: &str, repo: &str) -> Result> { +pub async fn list_tags_from_url(api_url: &str, repo: &str) -> Result> { let key = format!("{api_url}-{repo}").to_kebab_case(); - let cache = get_tags_cache(&key); + let cache = get_tags_cache(&key).await; let cache = cache.get(&key).unwrap(); Ok(cache - .get_or_try_init(|| list_tags_(api_url, repo))? + .get_or_try_init_async(async || list_tags_(api_url, repo).await) + .await? .to_vec()) } -fn list_tags_(api_url: &str, repo: &str) -> Result> { +async fn list_tags_(api_url: &str, repo: &str) -> Result> { let url = format!("{api_url}/repos/{repo}/tags"); let headers = get_headers(&url); - let (mut tags, mut headers) = - crate::http::HTTP_FETCH.json_headers_with_headers::, _>(url, &headers)?; + let (mut tags, mut headers) = crate::http::HTTP_FETCH + .json_headers_with_headers::, _>(url, &headers) + .await?; if *env::MISE_LIST_ALL_VERSIONS { while let Some(next) = next_page(&headers) { let (more, h) = crate::http::HTTP_FETCH - .json_headers_with_headers::, _>(next, &headers)?; + .json_headers_with_headers::, _>(next, &headers) + .await?; tags.extend(more); headers = h; } @@ -157,28 +166,32 @@ fn list_tags_(api_url: &str, repo: &str) -> Result> { Ok(tags.into_iter().map(|t| t.name).collect()) } -pub fn get_release(repo: &str, tag: &str) -> Result { +pub async fn get_release(repo: &str, tag: &str) -> Result { let key = format!("{repo}-{tag}").to_kebab_case(); - let cache = get_release_cache(&key); + let cache = get_release_cache(&key).await; let cache = cache.get(&key).unwrap(); Ok(cache - .get_or_try_init(|| get_release_(API_URL, repo, tag))? + .get_or_try_init_async(async || get_release_(API_URL, repo, tag).await) + .await? .clone()) } -pub fn get_release_for_url(api_url: &str, repo: &str, tag: &str) -> Result { +pub async fn get_release_for_url(api_url: &str, repo: &str, tag: &str) -> Result { let key = format!("{api_url}-{repo}-{tag}").to_kebab_case(); - let cache = get_release_cache(&key); + let cache = get_release_cache(&key).await; let cache = cache.get(&key).unwrap(); Ok(cache - .get_or_try_init(|| get_release_(api_url, repo, tag))? + .get_or_try_init_async(async || get_release_(api_url, repo, tag).await) + .await? .clone()) } -fn get_release_(api_url: &str, repo: &str, tag: &str) -> Result { +async fn get_release_(api_url: &str, repo: &str, tag: &str) -> Result { let url = format!("{api_url}/repos/{repo}/releases/tags/{tag}"); let headers = get_headers(&url); - crate::http::HTTP_FETCH.json_with_headers(url, &headers) + crate::http::HTTP_FETCH + .json_with_headers(url, &headers) + .await } fn next_page(headers: &HeaderMap) -> Option { diff --git a/src/gitlab.rs b/src/gitlab.rs index 2526948759..514be5f6a6 100644 --- a/src/gitlab.rs +++ b/src/gitlab.rs @@ -6,7 +6,7 @@ use serde_derive::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; use std::sync::LazyLock as Lazy; -use std::sync::{RwLock, RwLockReadGuard}; +use tokio::sync::{RwLock, RwLockReadGuard}; use xx::regex; use crate::cache::{CacheManager, CacheManagerBuilder}; @@ -56,65 +56,67 @@ static TAGS_CACHE: Lazy>>> = Lazy::new(Default::de pub static API_URL: &str = "https://gitlab.com/api/v4"; -fn get_tags_cache(key: &str) -> RwLockReadGuard<'_, CacheGroup>> { +async fn get_tags_cache(key: &str) -> RwLockReadGuard<'_, CacheGroup>> { TAGS_CACHE .write() - .unwrap() + .await .entry(key.to_string()) .or_insert_with(|| { CacheManagerBuilder::new(cache_dir().join(format!("{key}-tags.msgpack.z"))) .with_fresh_duration(Some(duration::DAILY)) .build() }); - TAGS_CACHE.read().unwrap() + TAGS_CACHE.read().await } -fn get_releases_cache(key: &str) -> RwLockReadGuard<'_, CacheGroup>> { +async fn get_releases_cache(key: &str) -> RwLockReadGuard<'_, CacheGroup>> { RELEASES_CACHE .write() - .unwrap() + .await .entry(key.to_string()) .or_insert_with(|| { CacheManagerBuilder::new(cache_dir().join(format!("{key}-releases.msgpack.z"))) .with_fresh_duration(Some(duration::DAILY)) .build() }); - RELEASES_CACHE.read().unwrap() + RELEASES_CACHE.read().await } -fn get_release_cache(key: &str) -> RwLockReadGuard<'_, CacheGroup> { +async fn get_release_cache(key: &str) -> RwLockReadGuard<'_, CacheGroup> { RELEASE_CACHE .write() - .unwrap() + .await .entry(key.to_string()) .or_insert_with(|| { CacheManagerBuilder::new(cache_dir().join(format!("{key}.msgpack.z"))) .with_fresh_duration(Some(duration::DAILY)) .build() }); - RELEASE_CACHE.read().unwrap() + RELEASE_CACHE.read().await } #[allow(dead_code)] -pub fn list_releases(repo: &str) -> Result> { +pub async fn list_releases(repo: &str) -> Result> { let key = repo.to_kebab_case(); - let cache = get_releases_cache(&key); + let cache = get_releases_cache(&key).await; let cache = cache.get(&key).unwrap(); Ok(cache - .get_or_try_init(|| list_releases_(API_URL, repo))? + .get_or_try_init_async(async || list_releases_(API_URL, repo).await) + .await? .to_vec()) } -pub fn list_releases_from_url(api_url: &str, repo: &str) -> Result> { +pub async fn list_releases_from_url(api_url: &str, repo: &str) -> Result> { let key = format!("{api_url}-{repo}").to_kebab_case(); - let cache = get_releases_cache(&key); + let cache = get_releases_cache(&key).await; let cache = cache.get(&key).unwrap(); Ok(cache - .get_or_try_init(|| list_releases_(api_url, repo))? + .get_or_try_init_async(async || list_releases_(api_url, repo).await) + .await? .to_vec()) } -fn list_releases_(api_url: &str, repo: &str) -> Result> { +async fn list_releases_(api_url: &str, repo: &str) -> Result> { let url = format!( "{}/projects/{}/releases", api_url, @@ -123,12 +125,14 @@ fn list_releases_(api_url: &str, repo: &str) -> Result> { let headers = get_headers(&url); let (mut releases, mut headers) = crate::http::HTTP_FETCH - .json_headers_with_headers::, _>(url, &headers)?; + .json_headers_with_headers::, _>(url, &headers) + .await?; if *env::MISE_LIST_ALL_VERSIONS { while let Some(next) = next_page(&headers) { let (more, h) = crate::http::HTTP_FETCH - .json_headers_with_headers::, _>(next, &headers)?; + .json_headers_with_headers::, _>(next, &headers) + .await?; releases.extend(more); headers = h; } @@ -138,38 +142,42 @@ fn list_releases_(api_url: &str, repo: &str) -> Result> { } #[allow(dead_code)] -pub fn list_tags(repo: &str) -> Result> { +pub async fn list_tags(repo: &str) -> Result> { let key = repo.to_kebab_case(); - let cache = get_tags_cache(&key); + let cache = get_tags_cache(&key).await; let cache = cache.get(&key).unwrap(); Ok(cache - .get_or_try_init(|| list_tags_(API_URL, repo))? + .get_or_try_init_async(async || list_tags_(API_URL, repo).await) + .await? .to_vec()) } -pub fn list_tags_from_url(api_url: &str, repo: &str) -> Result> { +pub async fn list_tags_from_url(api_url: &str, repo: &str) -> Result> { let key = format!("{api_url}-{repo}").to_kebab_case(); - let cache = get_tags_cache(&key); + let cache = get_tags_cache(&key).await; let cache = cache.get(&key).unwrap(); Ok(cache - .get_or_try_init(|| list_tags_(api_url, repo))? + .get_or_try_init_async(async || list_tags_(api_url, repo).await) + .await? .to_vec()) } -fn list_tags_(api_url: &str, repo: &str) -> Result> { +async fn list_tags_(api_url: &str, repo: &str) -> Result> { let url = format!( "{}/projects/{}/repository/tags", api_url, urlencoding::encode(repo) ); let headers = get_headers(&url); - let (mut tags, mut headers) = - crate::http::HTTP_FETCH.json_headers_with_headers::, _>(url, &headers)?; + let (mut tags, mut headers) = crate::http::HTTP_FETCH + .json_headers_with_headers::, _>(url, &headers) + .await?; if *env::MISE_LIST_ALL_VERSIONS { while let Some(next) = next_page(&headers) { let (more, h) = crate::http::HTTP_FETCH - .json_headers_with_headers::, _>(next, &headers)?; + .json_headers_with_headers::, _>(next, &headers) + .await?; tags.extend(more); headers = h; } @@ -179,25 +187,27 @@ fn list_tags_(api_url: &str, repo: &str) -> Result> { } #[allow(dead_code)] -pub fn get_release(repo: &str, tag: &str) -> Result { +pub async fn get_release(repo: &str, tag: &str) -> Result { let key = format!("{repo}-{tag}").to_kebab_case(); - let cache = get_release_cache(&key); + let cache = get_release_cache(&key).await; let cache = cache.get(&key).unwrap(); Ok(cache - .get_or_try_init(|| get_release_(API_URL, repo, tag))? + .get_or_try_init_async(async || get_release_(API_URL, repo, tag).await) + .await? .clone()) } -pub fn get_release_for_url(api_url: &str, repo: &str, tag: &str) -> Result { +pub async fn get_release_for_url(api_url: &str, repo: &str, tag: &str) -> Result { let key = format!("{api_url}-{repo}-{tag}").to_kebab_case(); - let cache = get_release_cache(&key); + let cache = get_release_cache(&key).await; let cache = cache.get(&key).unwrap(); Ok(cache - .get_or_try_init(|| get_release_(api_url, repo, tag))? + .get_or_try_init_async(async || get_release_(api_url, repo, tag).await) + .await? .clone()) } -fn get_release_(api_url: &str, repo: &str, tag: &str) -> Result { +async fn get_release_(api_url: &str, repo: &str, tag: &str) -> Result { let url = format!( "{}/projects/{}/releases/{}", api_url, @@ -205,7 +215,9 @@ fn get_release_(api_url: &str, repo: &str, tag: &str) -> Result { tag ); let headers = get_headers(&url); - crate::http::HTTP_FETCH.json_with_headers(url, &headers) + crate::http::HTTP_FETCH + .json_with_headers(url, &headers) + .await } fn next_page(headers: &HeaderMap) -> Option { diff --git a/src/hash.rs b/src/hash.rs index 0457c90d04..3905f9090a 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -9,7 +9,6 @@ use crate::ui::progress_report::SingleReport; use digest::Digest; use eyre::{Result, bail}; use md5::Md5; -use rayon::prelude::*; use sha1::Sha1; use sha2::{Sha256, Sha512}; use siphasher::sip::SipHasher; @@ -109,7 +108,7 @@ pub fn ensure_checksum( } pub fn parse_shasums(text: &str) -> HashMap { - text.par_lines() + text.lines() .map(|l| { let mut parts = l.split_whitespace(); let hash = parts.next().unwrap(); diff --git a/src/hook_env.rs b/src/hook_env.rs index fb69cc0323..b27260b8a2 100644 --- a/src/hook_env.rs +++ b/src/hook_env.rs @@ -165,12 +165,12 @@ pub fn deserialize(raw: String) -> Result { Ok(rmp_serde::from_slice(&writer[..])?) } -pub fn build_session( +pub async fn build_session( env: EnvMap, loaded_tools: IndexSet, watch_files: BTreeSet, ) -> Result { - let config = Config::get(); + let config = Config::get().await; let mut max_modtime = UNIX_EPOCH; for cf in get_watch_files(watch_files)? { if let Ok(Ok(modified)) = cf.metadata().map(|m| m.modified()) { @@ -178,7 +178,7 @@ pub fn build_session( } } - let config_paths = if let Ok(paths) = config.path_dirs() { + let config_paths = if let Ok(paths) = config.path_dirs().await { paths.iter().cloned().collect() } else { IndexSet::new() diff --git a/src/hooks.rs b/src/hooks.rs index 45f58acf6f..eab556a834 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -10,6 +10,7 @@ use std::iter::once; use std::path::{Path, PathBuf}; use std::sync::LazyLock as Lazy; use std::sync::Mutex; +use tokio::sync::OnceCell; #[derive( Debug, @@ -47,31 +48,39 @@ pub fn schedule_hook(hook: Hooks) { mu.insert(hook); } -pub fn run_all_hooks(ts: &Toolset, shell: &dyn Shell) { - let mut mu = SCHEDULED_HOOKS.lock().unwrap(); - for hook in mu.drain(..) { - run_one_hook(ts, hook, Some(shell)); +pub async fn run_all_hooks(ts: &Toolset, shell: &dyn Shell) { + let hooks = { + let mut mu = SCHEDULED_HOOKS.lock().unwrap(); + mu.drain(..).collect::>() + }; + for hook in hooks { + run_one_hook(ts, hook, Some(shell)).await; } } -static ALL_HOOKS: Lazy> = Lazy::new(|| { - let config = Config::get(); - let mut hooks = config.hooks().cloned().unwrap_or_default(); - let cur_configs = config.config_files.keys().cloned().collect::>(); - let prev_configs = &hook_env::PREV_SESSION.loaded_configs; - let old_configs = prev_configs.difference(&cur_configs); - for p in old_configs { - if let Ok(cf) = config_file::parse(p) { - if let Ok(h) = cf.hooks() { - hooks.extend(h.into_iter().map(|h| (cf.config_root(), h))); +async fn all_hooks() -> &'static Vec<(PathBuf, Hook)> { + static ALL_HOOKS: OnceCell> = OnceCell::const_new(); + ALL_HOOKS + .get_or_init(async || { + let config = Config::get().await; + let mut hooks = config.hooks().await.cloned().unwrap_or_default(); + let cur_configs = config.config_files.keys().cloned().collect::>(); + let prev_configs = &hook_env::PREV_SESSION.loaded_configs; + let old_configs = prev_configs.difference(&cur_configs); + for p in old_configs { + if let Ok(cf) = config_file::parse(p) { + if let Ok(h) = cf.hooks() { + hooks.extend(h.into_iter().map(|h| (cf.config_root(), h))); + } + } } - } - } - hooks -}); + hooks + }) + .await +} -pub fn run_one_hook(ts: &Toolset, hook: Hooks, shell: Option<&dyn Shell>) { - for (root, h) in &*ALL_HOOKS { +pub async fn run_one_hook(ts: &Toolset, hook: Hooks, shell: Option<&dyn Shell>) { + for (root, h) in all_hooks().await { if hook != h.hook || (h.shell.is_some() && h.shell != shell.map(|s| s.to_string())) { continue; } @@ -102,7 +111,7 @@ pub fn run_one_hook(ts: &Toolset, hook: Hooks, shell: Option<&dyn Shell>) { } if h.shell.is_some() { println!("{}", h.script); - } else if let Err(e) = execute(ts, root, h) { + } else if let Err(e) = execute(ts, root, h).await { warn!("error executing hook: {e}"); } } @@ -145,7 +154,7 @@ impl Hook { } } -fn execute(ts: &Toolset, root: &Path, hook: &Hook) -> Result<()> { +async fn execute(ts: &Toolset, root: &Path, hook: &Hook) -> Result<()> { SETTINGS.ensure_experimental("hooks")?; let shell = SETTINGS.default_inline_shell()?; @@ -155,8 +164,8 @@ fn execute(ts: &Toolset, root: &Path, hook: &Hook) -> Result<()> { .map(|s| s.as_str()) .chain(once(hook.script.as_str())) .collect_vec(); - let config = Config::get(); - let mut env = ts.full_env(&config)?; + let config = Config::get().await; + let mut env = ts.full_env(&config).await?; if let Some(cwd) = dirs::CWD.as_ref() { env.insert( "MISE_ORIGINAL_CWD".to_string(), diff --git a/src/http.rs b/src/http.rs index 30be7fde7d..f7bfd61a3e 100644 --- a/src/http.rs +++ b/src/http.rs @@ -11,7 +11,6 @@ use url::Url; use crate::cli::version; use crate::config::SETTINGS; use crate::file::display_path; -use crate::tokio::RUNTIME; use crate::ui::progress_report::SingleReport; use crate::{env, file}; @@ -48,12 +47,10 @@ impl Client { .zstd(true) } - pub fn get_bytes(&self, url: U) -> Result> { + pub async fn get_bytes(&self, url: U) -> Result> { let url = url.into_url().unwrap(); - RUNTIME.block_on(async { - let resp = self.get_async(url.clone()).await?; - Ok(resp.bytes().await?) - }) + let resp = self.get_async(url.clone()).await?; + Ok(resp.bytes().await?) } pub async fn get_async(&self, url: U) -> Result { @@ -95,12 +92,7 @@ impl Client { Ok(resp) } - pub fn head(&self, url: U) -> Result { - let url = url.into_url().unwrap(); - RUNTIME.block_on(self.head_async(url)) - } - - pub async fn head_async(&self, url: U) -> Result { + pub async fn head(&self, url: U) -> Result { let url = url.into_url().unwrap(); let headers = github_headers(&url); self.head_async_with_headers(url, &headers).await @@ -139,49 +131,43 @@ impl Client { Ok(resp) } - pub fn get_text(&self, url: U) -> Result { + pub async fn get_text(&self, url: U) -> Result { let mut url = url.into_url().unwrap(); - let text = RUNTIME.block_on(async { - let resp = self.get_async(url.clone()).await?; - Ok::(resp.text().await?) - })?; + let resp = self.get_async(url.clone()).await?; + let text = resp.text().await?; if text.starts_with("") { if url.scheme() == "http" { // try with https since http may be blocked url.set_scheme("https").unwrap(); - return self.get_text(url); + return Box::pin(self.get_text(url)).await; } bail!("Got HTML instead of text from {}", url); } Ok(text) } - pub fn get_html(&self, url: U) -> Result { + pub async fn get_html(&self, url: U) -> Result { let url = url.into_url().unwrap(); - let html = RUNTIME.block_on(async { - let resp = self.get_async(url.clone()).await?; - Ok::(resp.text().await?) - })?; + let resp = self.get_async(url.clone()).await?; + let html = resp.text().await?; if !html.starts_with("") { bail!("Got non-HTML text from {}", url); } Ok(html) } - pub fn json_headers(&self, url: U) -> Result<(T, HeaderMap)> + pub async fn json_headers(&self, url: U) -> Result<(T, HeaderMap)> where T: serde::de::DeserializeOwned, { let url = url.into_url().unwrap(); - let (json, headers) = RUNTIME.block_on(async { - let resp = self.get_async(url).await?; - let headers = resp.headers().clone(); - Ok::<(T, HeaderMap), eyre::Error>((resp.json().await?, headers)) - })?; + let resp = self.get_async(url).await?; + let headers = resp.headers().clone(); + let json = resp.json().await?; Ok((json, headers)) } - pub fn json_headers_with_headers( + pub async fn json_headers_with_headers( &self, url: U, headers: &HeaderMap, @@ -190,30 +176,29 @@ impl Client { T: serde::de::DeserializeOwned, { let url = url.into_url().unwrap(); - let (json, headers) = RUNTIME.block_on(async { - let resp = self.get_async_with_headers(url, headers).await?; - let headers = resp.headers().clone(); - Ok::<(T, HeaderMap), eyre::Error>((resp.json().await?, headers)) - })?; + let resp = self.get_async_with_headers(url, headers).await?; + let headers = resp.headers().clone(); + let json = resp.json().await?; Ok((json, headers)) } - pub fn json(&self, url: U) -> Result + pub async fn json(&self, url: U) -> Result where T: serde::de::DeserializeOwned, { - self.json_headers(url).map(|(json, _)| json) + self.json_headers(url).await.map(|(json, _)| json) } - pub fn json_with_headers(&self, url: U, headers: &HeaderMap) -> Result + pub async fn json_with_headers(&self, url: U, headers: &HeaderMap) -> Result where T: serde::de::DeserializeOwned, { self.json_headers_with_headers(url, headers) + .await .map(|(json, _)| json) } - pub fn download_file( + pub async fn download_file( &self, url: U, path: &Path, @@ -222,26 +207,23 @@ impl Client { let url = url.into_url()?; debug!("GET Downloading {} to {}", &url, display_path(path)); - RUNTIME.block_on(async { - let mut resp = self.get_async(url).await?; - if let Some(length) = resp.content_length() { - if let Some(pr) = pr { - pr.set_length(length); - } + let mut resp = self.get_async(url).await?; + if let Some(length) = resp.content_length() { + if let Some(pr) = pr { + pr.set_length(length); } + } - let parent = path.parent().unwrap(); - file::create_dir_all(parent)?; - let mut file = tempfile::NamedTempFile::with_prefix_in(path, parent)?; - while let Some(chunk) = resp.chunk().await? { - file.write_all(&chunk)?; - if let Some(pr) = pr { - pr.inc(chunk.len() as u64); - } + let parent = path.parent().unwrap(); + file::create_dir_all(parent)?; + let mut file = tempfile::NamedTempFile::with_prefix_in(path, parent)?; + while let Some(chunk) = resp.chunk().await? { + file.write_all(&chunk)?; + if let Some(pr) = pr { + pr.inc(chunk.len() as u64); } - file.persist(path)?; - Ok::<(), eyre::Error>(()) - })?; + } + file.persist(path)?; Ok(()) } } diff --git a/src/install_context.rs b/src/install_context.rs index 20b6f05fa9..3f1b89c7c0 100644 --- a/src/install_context.rs +++ b/src/install_context.rs @@ -1,8 +1,10 @@ +use std::sync::Arc; + use crate::toolset::Toolset; use crate::ui::progress_report::SingleReport; -pub struct InstallContext<'a> { - pub ts: &'a Toolset, +pub struct InstallContext { + pub ts: Arc, pub pr: Box, pub force: bool, } diff --git a/src/lockfile.rs b/src/lockfile.rs index c57190aab7..35ed0a9a7c 100644 --- a/src/lockfile.rs +++ b/src/lockfile.rs @@ -6,10 +6,13 @@ use crate::toolset::{ToolSource, ToolVersion, ToolVersionList, Toolset}; use eyre::{Report, Result, bail}; use itertools::Itertools; use serde_derive::{Deserialize, Serialize}; -use std::collections::{BTreeMap, HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::sync::LazyLock as Lazy; use std::sync::Mutex; +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + sync::Arc, +}; use toml_edit::DocumentMut; #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -102,7 +105,7 @@ pub fn update_lockfiles(config: &Config, ts: &Toolset, new_versions: &[ToolVersi for (backend, group) in &new_versions.iter().chunk_by(|tv| tv.ba()) { let tvs = group.cloned().collect_vec(); let source = tvs[0].request.source().clone(); - let mut tvl = ToolVersionList::new(backend.clone(), source.clone()); + let mut tvl = ToolVersionList::new(Arc::new(backend.clone()), source.clone()); tvl.versions.extend(tvs); tools_by_source .entry(source) @@ -158,8 +161,8 @@ pub fn update_lockfiles(config: &Config, ts: &Toolset, new_versions: &[ToolVersi Ok(()) } -fn read_all_lockfiles() -> Lockfile { - Config::get() +fn read_all_lockfiles(config: &Config) -> Lockfile { + config .config_files .iter() .rev() @@ -192,6 +195,7 @@ fn read_lockfile_for(path: &Path) -> Result { } pub fn get_locked_version( + config: &Config, path: Option<&Path>, short: &str, prefix: &str, @@ -210,7 +214,7 @@ pub fn get_locked_version( } None => { trace!("[{short}@{prefix}] reading all lockfiles"); - read_all_lockfiles() + read_all_lockfiles(config) } }; diff --git a/src/logger.rs b/src/logger.rs index 1abf09dc9a..544163501b 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -76,7 +76,7 @@ impl Logger { fn render(&self, record: &Record, level: LevelFilter) -> String { let mut args = record.args().to_string(); if config::is_loaded() { - let config = Config::get(); + let config = Config::get_(); args = config.redact(args); } match level { diff --git a/src/main.rs b/src/main.rs index 4de47d82cf..049ceab6d2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -70,7 +70,6 @@ mod sysconfig; pub(crate) mod task; pub(crate) mod tera; pub(crate) mod timeout; -mod tokio; mod toml; mod toolset; mod ui; @@ -85,18 +84,26 @@ pub(crate) use crate::toolset::install_state; use crate::ui::multi_progress_report::MultiProgressReport; fn main() -> eyre::Result<()> { - color_eyre::install()?; - measure!("main", { - let args = env::args().collect_vec(); - match Cli::run(&args).with_section(|| VERSION.to_string().header("Version:")) { - Ok(()) => Ok(()), - Err(err) => handle_err(err), - }?; - }); - if let Some(mpr) = MultiProgressReport::try_get() { - mpr.stop()?; - } - Ok(()) + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build()?; + rt.block_on(async { + color_eyre::install()?; + measure!("main", { + let args = env::args().collect_vec(); + match Cli::run(&args) + .await + .with_section(|| VERSION.to_string().header("Version:")) + { + Ok(()) => Ok(()), + Err(err) => handle_err(err), + }?; + }); + if let Some(mpr) = MultiProgressReport::try_get() { + mpr.stop()?; + } + Ok(()) + }) } fn handle_err(err: Report) -> eyre::Result<()> { diff --git a/src/migrate.rs b/src/migrate.rs index a0b2029a46..301496f8ab 100644 --- a/src/migrate.rs +++ b/src/migrate.rs @@ -4,28 +4,25 @@ use std::path::Path; use crate::dirs::*; use crate::file; use eyre::Result; -use rayon::Scope; -pub fn run() { - rayon::scope(|s| { - task(s, || rename_plugin("nodejs", "node")); - task(s, || rename_plugin("golang", "go")); - task(s, migrate_trusted_configs); - task(s, migrate_tracked_configs); - task(s, || remove_deprecated_plugin("node", "rtx-nodejs")); - task(s, || remove_deprecated_plugin("go", "rtx-golang")); - task(s, || remove_deprecated_plugin("java", "rtx-java")); - task(s, || remove_deprecated_plugin("python", "rtx-python")); - task(s, || remove_deprecated_plugin("ruby", "rtx-ruby")); - }); +pub async fn run() { + tokio::join!( + task(|| rename_plugin("nodejs", "node")), + task(|| rename_plugin("golang", "go")), + task(migrate_trusted_configs), + task(migrate_tracked_configs), + task(|| remove_deprecated_plugin("node", "rtx-nodejs")), + task(|| remove_deprecated_plugin("go", "rtx-golang")), + task(|| remove_deprecated_plugin("java", "rtx-java")), + task(|| remove_deprecated_plugin("python", "rtx-python")), + task(|| remove_deprecated_plugin("ruby", "rtx-ruby")), + ); } -fn task(s: &Scope, job: impl FnOnce() -> Result<()> + Send + 'static) { - s.spawn(|_| { - if let Err(err) = job() { - eprintln!("[WARN] migrate: {err}"); - } - }); +async fn task(job: impl FnOnce() -> Result<()> + Send + 'static) { + if let Err(err) = job() { + eprintln!("[WARN] migrate: {err}"); + } } fn move_subdirs(from: &Path, to: &Path) -> Result<()> { diff --git a/src/plugins/asdf_plugin.rs b/src/plugins/asdf_plugin.rs index 2e7e26e4f8..fe9a9f3ca3 100644 --- a/src/plugins/asdf_plugin.rs +++ b/src/plugins/asdf_plugin.rs @@ -2,13 +2,14 @@ use crate::config::{Config, SETTINGS, Settings}; use crate::errors::Error::PluginNotInstalled; use crate::file::{display_path, remove_all}; use crate::git::{CloneOptions, Git}; -use crate::plugins::{Plugin, PluginType, Script, ScriptManager}; +use crate::plugins::{Plugin, Script, ScriptManager}; use crate::result::Result; use crate::timeout::run_with_timeout; use crate::ui::multi_progress_report::MultiProgressReport; use crate::ui::progress_report::SingleReport; use crate::ui::prompt; use crate::{dirs, env, exit, lock_file, registry}; +use async_trait::async_trait; use clap::Command; use console::style; use contracts::requires; @@ -25,8 +26,8 @@ pub struct AsdfPlugin { pub name: String, pub plugin_path: PathBuf, pub repo: Mutex, - pub repo_url: Option, pub script_man: ScriptManager, + repo_url: Mutex>, } impl AsdfPlugin { @@ -36,7 +37,7 @@ impl AsdfPlugin { Self { script_man: build_script_man(&name, &plugin_path), name, - repo_url: None, + repo_url: Mutex::new(None), repo: Mutex::new(repo), plugin_path, } @@ -48,6 +49,8 @@ impl AsdfPlugin { fn get_repo_url(&self, config: &Config) -> eyre::Result { self.repo_url + .lock() + .unwrap() .clone() .or_else(|| self.repo().get_remote_url()) .or_else(|| config.get_repo_url(&self.name)) @@ -179,6 +182,7 @@ impl AsdfPlugin { } } +#[async_trait] impl Plugin for AsdfPlugin { fn name(&self) -> &str { &self.name @@ -188,17 +192,13 @@ impl Plugin for AsdfPlugin { self.plugin_path.clone() } - fn get_plugin_type(&self) -> PluginType { - PluginType::Asdf - } - fn get_remote_url(&self) -> eyre::Result> { let url = self.repo().get_remote_url(); - Ok(url.or(self.repo_url.clone())) + Ok(url.or(self.repo_url.lock().unwrap().clone())) } - fn set_remote_url(&mut self, url: String) { - self.repo_url = Some(url); + fn set_remote_url(&self, url: String) { + *self.repo_url.lock().unwrap() = Some(url); } fn current_abbrev_ref(&self) -> eyre::Result> { @@ -227,14 +227,14 @@ impl Plugin for AsdfPlugin { .wrap_err("run with --yes to install plugin automatically")) } - fn ensure_installed(&self, mpr: &MultiProgressReport, force: bool) -> Result<()> { - let config = Config::get(); + async fn ensure_installed(&self, mpr: &MultiProgressReport, force: bool) -> Result<()> { + let config = Config::try_get().await?; let settings = Settings::try_get()?; if !force { if self.is_installed() { return Ok(()); } - if !settings.yes && self.repo_url.is_none() { + if !settings.yes && self.repo_url.lock().unwrap().is_none() { let url = self.get_repo_url(&config).unwrap_or_default(); if !registry::is_trusted_plugin(self.name(), &url) { warn!( @@ -259,10 +259,10 @@ impl Plugin for AsdfPlugin { let prefix = format!("plugin:{}", style(&self.name).blue().for_stderr()); let pr = mpr.add(&prefix); let _lock = lock_file::get(&self.plugin_path, force)?; - self.install(&pr) + self.install(&pr).await } - fn update(&self, pr: &Box, gitref: Option) -> Result<()> { + async fn update(&self, pr: &Box, gitref: Option) -> Result<()> { let plugin_path = self.plugin_path.to_path_buf(); if plugin_path.is_symlink() { warn!( @@ -291,7 +291,7 @@ impl Plugin for AsdfPlugin { Ok(()) } - fn uninstall(&self, pr: &Box) -> Result<()> { + async fn uninstall(&self, pr: &Box) -> Result<()> { if !self.is_installed() { return Ok(()); } @@ -316,14 +316,14 @@ impl Plugin for AsdfPlugin { Ok(()) } - fn install(&self, pr: &Box) -> eyre::Result<()> { - let config = Config::get(); + async fn install(&self, pr: &Box) -> eyre::Result<()> { + let config = Config::try_get().await?; let repository = self.get_repo_url(&config)?; let (repo_url, repo_ref) = Git::split_url_and_ref(&repository); debug!("asdf_plugin[{}]:install {:?}", self.name, repository); if self.is_installed() { - self.uninstall(pr)?; + self.uninstall(pr).await?; } if regex!(r"^[/~]").is_match(&repo_url) { diff --git a/src/plugins/core/bun.rs b/src/plugins/core/bun.rs index e3368d8ac4..0bcc4400cf 100644 --- a/src/plugins/core/bun.rs +++ b/src/plugins/core/bun.rs @@ -1,5 +1,9 @@ -use std::path::{Path, PathBuf}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; +use async_trait::async_trait; use eyre::Result; use itertools::Itertools; use versions::Versioning; @@ -16,13 +20,13 @@ use crate::{file, github, plugins}; #[derive(Debug)] pub struct BunPlugin { - ba: BackendArg, + ba: Arc, } impl BunPlugin { pub fn new() -> Self { Self { - ba: plugins::core::new_backend_arg("bun"), + ba: Arc::new(plugins::core::new_backend_arg("bun")), } } @@ -38,7 +42,7 @@ impl BunPlugin { .execute() } - fn download(&self, tv: &ToolVersion, pr: &Box) -> Result { + async fn download(&self, tv: &ToolVersion, pr: &Box) -> Result { let url = format!( "https://github.com/oven-sh/bun/releases/download/bun-v{}/bun-{}-{}.zip", tv.version, @@ -49,7 +53,7 @@ impl BunPlugin { let tarball_path = tv.download_path().join(filename); pr.set_message(format!("download {filename}")); - HTTP.download_file(&url, &tarball_path, Some(pr))?; + HTTP.download_file(&url, &tarball_path, Some(pr)).await?; Ok(tarball_path) } @@ -78,13 +82,15 @@ impl BunPlugin { } } +#[async_trait] impl Backend for BunPlugin { - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } - fn _list_remote_versions(&self) -> Result> { - let versions = github::list_releases("oven-sh/bun")? + async fn _list_remote_versions(&self) -> Result> { + let versions = github::list_releases("oven-sh/bun") + .await? .into_iter() .map(|r| r.tag_name) .filter_map(|v| v.strip_prefix("bun-v").map(|v| v.to_string())) @@ -98,8 +104,12 @@ impl Backend for BunPlugin { Ok(vec![".bun-version".into()]) } - fn install_version_(&self, ctx: &InstallContext, mut tv: ToolVersion) -> Result { - let tarball_path = self.download(&tv, &ctx.pr)?; + async fn install_version_( + &self, + ctx: &InstallContext, + mut tv: ToolVersion, + ) -> Result { + let tarball_path = self.download(&tv, &ctx.pr).await?; self.verify_checksum(ctx, &mut tv, &tarball_path)?; self.install(ctx, &tv, &tarball_path)?; self.verify(ctx, &tv)?; diff --git a/src/plugins/core/deno.rs b/src/plugins/core/deno.rs index 83dab03bcf..c36c28dec1 100644 --- a/src/plugins/core/deno.rs +++ b/src/plugins/core/deno.rs @@ -1,6 +1,10 @@ use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; +use async_trait::async_trait; use eyre::Result; use itertools::Itertools; use serde::Deserialize; @@ -19,13 +23,13 @@ use crate::{file, plugins}; #[derive(Debug)] pub struct DenoPlugin { - ba: BackendArg, + ba: Arc, } impl DenoPlugin { pub fn new() -> Self { Self { - ba: plugins::core::new_backend_arg("deno"), + ba: Arc::new(plugins::core::new_backend_arg("deno")), } } @@ -45,7 +49,7 @@ impl DenoPlugin { .execute() } - fn download(&self, tv: &ToolVersion, pr: &Box) -> Result { + async fn download(&self, tv: &ToolVersion, pr: &Box) -> Result { let url = format!( "https://dl.deno.land/release/v{}/deno-{}-{}.zip", tv.version, @@ -56,7 +60,7 @@ impl DenoPlugin { let tarball_path = tv.download_path().join(filename); pr.set_message(format!("download {filename}")); - HTTP.download_file(&url, &tarball_path, Some(pr))?; + HTTP.download_file(&url, &tarball_path, Some(pr)).await?; // TODO: hash::ensure_checksum_sha256(&tarball_path, &m.sha256)?; @@ -91,13 +95,14 @@ impl DenoPlugin { } } +#[async_trait] impl Backend for DenoPlugin { - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } - fn _list_remote_versions(&self) -> Result> { - let versions: DenoVersions = HTTP_FETCH.json("https://deno.com/versions.json")?; + async fn _list_remote_versions(&self) -> Result> { + let versions: DenoVersions = HTTP_FETCH.json("https://deno.com/versions.json").await?; let versions = versions .cli .into_iter() @@ -113,8 +118,12 @@ impl Backend for DenoPlugin { Ok(vec![".deno-version".into()]) } - fn install_version_(&self, ctx: &InstallContext, mut tv: ToolVersion) -> Result { - let tarball_path = self.download(&tv, &ctx.pr)?; + async fn install_version_( + &self, + ctx: &InstallContext, + mut tv: ToolVersion, + ) -> Result { + let tarball_path = self.download(&tv, &ctx.pr).await?; self.verify_checksum(ctx, &mut tv, &tarball_path)?; self.install(&tv, &ctx.pr, &tarball_path)?; self.verify(&tv, &ctx.pr)?; @@ -122,7 +131,7 @@ impl Backend for DenoPlugin { Ok(tv) } - fn list_bin_paths(&self, tv: &ToolVersion) -> Result> { + async fn list_bin_paths(&self, tv: &ToolVersion) -> Result> { if let ToolRequest::System { .. } = tv.request { return Ok(vec![]); } @@ -133,7 +142,7 @@ impl Backend for DenoPlugin { Ok(bin_paths) } - fn exec_env( + async fn exec_env( &self, _config: &Config, _ts: &Toolset, diff --git a/src/plugins/core/elixir.rs b/src/plugins/core/elixir.rs index 477e1e7c74..1807e26843 100644 --- a/src/plugins/core/elixir.rs +++ b/src/plugins/core/elixir.rs @@ -1,4 +1,7 @@ -use std::path::{Path, PathBuf}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; use crate::backend::Backend; use crate::cli::args::BackendArg; @@ -9,6 +12,7 @@ use crate::plugins::VERSION_REGEX; use crate::toolset::ToolVersion; use crate::ui::progress_report::SingleReport; use crate::{file, plugins}; +use async_trait::async_trait; use eyre::Result; use itertools::Itertools; use versions::Versioning; @@ -16,13 +20,13 @@ use xx::regex; #[derive(Debug)] pub struct ElixirPlugin { - ba: BackendArg, + ba: Arc, } impl ElixirPlugin { pub fn new() -> Self { Self { - ba: plugins::core::new_backend_arg("elixir"), + ba: Arc::new(plugins::core::new_backend_arg("elixir")), } } @@ -30,16 +34,16 @@ impl ElixirPlugin { tv.install_path().join("bin").join(elixir_bin_name()) } - fn test_elixir(&self, ctx: &InstallContext, tv: &ToolVersion) -> Result<()> { + async fn test_elixir(&self, ctx: &InstallContext, tv: &ToolVersion) -> Result<()> { ctx.pr.set_message("elixir --version".into()); CmdLineRunner::new(self.elixir_bin(tv)) .with_pr(&ctx.pr) - .envs(self.dependency_env()?) + .envs(self.dependency_env().await?) .arg("--version") .execute() } - fn download(&self, tv: &ToolVersion, pr: &Box) -> Result { + async fn download(&self, tv: &ToolVersion, pr: &Box) -> Result { let version = &tv.version; let version = if regex!(r"^[0-9]").is_match(version) { &format!("v{version}") @@ -53,13 +57,18 @@ impl ElixirPlugin { pr.set_message(format!("download {filename}")); if !tarball_path.exists() { - HTTP.download_file(&url, &tarball_path, Some(pr))?; + HTTP.download_file(&url, &tarball_path, Some(pr)).await?; } Ok(tarball_path) } - fn install(&self, ctx: &InstallContext, tv: &ToolVersion, tarball_path: &Path) -> Result<()> { + async fn install( + &self, + ctx: &InstallContext, + tv: &ToolVersion, + tarball_path: &Path, + ) -> Result<()> { let filename = tarball_path.file_name().unwrap().to_string_lossy(); ctx.pr.set_message(format!("extract {filename}")); file::remove_all(tv.install_path())?; @@ -68,19 +77,21 @@ impl ElixirPlugin { Ok(()) } - fn verify(&self, ctx: &InstallContext, tv: &ToolVersion) -> Result<()> { - self.test_elixir(ctx, tv) + async fn verify(&self, ctx: &InstallContext, tv: &ToolVersion) -> Result<()> { + self.test_elixir(ctx, tv).await } } +#[async_trait] impl Backend for ElixirPlugin { - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } - fn _list_remote_versions(&self) -> Result> { + async fn _list_remote_versions(&self) -> Result> { let versions: Vec = HTTP_FETCH - .get_text("https://builds.hex.pm/builds/elixir/builds.txt")? + .get_text("https://builds.hex.pm/builds/elixir/builds.txt") + .await? .lines() .unique() .filter_map(|s| s.split_once(' ').map(|(v, _)| v.trim_start_matches('v'))) @@ -103,15 +114,19 @@ impl Backend for ElixirPlugin { Ok(vec!["erlang"]) } - fn install_version_(&self, ctx: &InstallContext, mut tv: ToolVersion) -> Result { - let tarball_path = self.download(&tv, &ctx.pr)?; + async fn install_version_( + &self, + ctx: &InstallContext, + mut tv: ToolVersion, + ) -> Result { + let tarball_path = self.download(&tv, &ctx.pr).await?; self.verify_checksum(ctx, &mut tv, &tarball_path)?; - self.install(ctx, &tv, &tarball_path)?; - self.verify(ctx, &tv)?; + self.install(ctx, &tv, &tarball_path).await?; + self.verify(ctx, &tv).await?; Ok(tv) } - fn list_bin_paths(&self, tv: &ToolVersion) -> Result> { + async fn list_bin_paths(&self, tv: &ToolVersion) -> Result> { Ok(["bin", ".mix/escripts"] .iter() .map(|p| tv.install_path().join(p)) diff --git a/src/plugins/core/erlang.rs b/src/plugins/core/erlang.rs index c38493fe46..f8770020bf 100644 --- a/src/plugins/core/erlang.rs +++ b/src/plugins/core/erlang.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; use crate::backend::Backend; use crate::cli::args::BackendArg; @@ -11,12 +11,13 @@ use crate::install_context::InstallContext; use crate::lock_file::LockFile; use crate::toolset::{ToolRequest, ToolVersion}; use crate::{cmd, file, github, plugins}; +use async_trait::async_trait; use eyre::Result; use xx::regex; #[derive(Debug)] pub struct ErlangPlugin { - ba: BackendArg, + ba: Arc, } const KERL_VERSION: &str = "4.1.1"; @@ -24,7 +25,7 @@ const KERL_VERSION: &str = "4.1.1"; impl ErlangPlugin { pub fn new() -> Self { Self { - ba: plugins::core::new_backend_arg("erlang"), + ba: Arc::new(plugins::core::new_backend_arg("erlang")), } } @@ -44,33 +45,35 @@ impl ErlangPlugin { .lock() } - fn update_kerl(&self) -> Result<()> { + async fn update_kerl(&self) -> Result<()> { let _lock = self.lock_build_tool(); if self.kerl_path().exists() { // TODO: find a way to not have to do this #1209 file::remove_all(self.kerl_base_dir())?; return Ok(()); } - self.install_kerl()?; + self.install_kerl().await?; cmd!(self.kerl_path(), "update", "releases") .env("KERL_BASE_DIR", self.kerl_base_dir()) .run()?; Ok(()) } - fn install_kerl(&self) -> Result<()> { + async fn install_kerl(&self) -> Result<()> { debug!("Installing kerl to {}", display_path(self.kerl_path())); - HTTP_FETCH.download_file( - format!("https://raw.githubusercontent.com/kerl/kerl/{KERL_VERSION}/kerl"), - &self.kerl_path(), - None, - )?; + HTTP_FETCH + .download_file( + format!("https://raw.githubusercontent.com/kerl/kerl/{KERL_VERSION}/kerl"), + &self.kerl_path(), + None, + ) + .await?; file::make_executable(self.kerl_path())?; Ok(()) } #[cfg(not(windows))] - fn install_precompiled( + async fn install_precompiled( &self, ctx: &InstallContext, mut tv: ToolVersion, @@ -79,7 +82,7 @@ impl ErlangPlugin { return Ok(None); } let release_tag = format!("OTP-{}", tv.version); - let gh_release = match github::get_release("erlef/otp_builds", &release_tag) { + let gh_release = match github::get_release("erlef/otp_builds", &release_tag).await { Ok(release) => release, Err(e) => { debug!("Failed to get release: {}", e); @@ -96,7 +99,8 @@ impl ErlangPlugin { }; ctx.pr.set_message(format!("Downloading {tarball_name}")); let tarball_path = tv.download_path().join(&tarball_name); - HTTP.download_file(&asset.browser_download_url, &tarball_path, Some(&ctx.pr))?; + HTTP.download_file(&asset.browser_download_url, &tarball_path, Some(&ctx.pr)) + .await?; self.verify_checksum(ctx, &mut tv, &tarball_path)?; ctx.pr.set_message(format!("Extracting {tarball_name}")); file::untar( @@ -112,7 +116,7 @@ impl ErlangPlugin { } #[cfg(windows)] - fn install_precompiled( + async fn install_precompiled( &self, ctx: &InstallContext, mut tv: ToolVersion, @@ -121,7 +125,7 @@ impl ErlangPlugin { return Ok(None); } let release_tag = format!("OTP-{}", tv.version); - let gh_release = match github::get_release("erlang/otp", &release_tag) { + let gh_release = match github::get_release("erlang/otp", &release_tag).await { Ok(release) => release, Err(e) => { debug!("Failed to get release: {}", e); @@ -138,15 +142,20 @@ impl ErlangPlugin { }; ctx.pr.set_message(format!("Downloading {}", zip_name)); let zip_path = tv.download_path().join(&zip_name); - HTTP.download_file(&asset.browser_download_url, &zip_path, Some(&ctx.pr))?; + HTTP.download_file(&asset.browser_download_url, &zip_path, Some(&ctx.pr)) + .await?; self.verify_checksum(ctx, &mut tv, &zip_path)?; ctx.pr.set_message(format!("Extracting {}", zip_name)); file::unzip(&zip_path, &tv.install_path())?; Ok(Some(tv)) } - fn install_via_kerl(&self, _ctx: &InstallContext, tv: ToolVersion) -> Result { - self.update_kerl()?; + async fn install_via_kerl( + &self, + _ctx: &InstallContext, + tv: ToolVersion, + ) -> Result { + self.update_kerl().await?; file::remove_all(tv.install_path())?; match &tv.request { @@ -171,18 +180,21 @@ impl ErlangPlugin { } } +#[async_trait] impl Backend for ErlangPlugin { - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } - fn _list_remote_versions(&self) -> Result> { + + async fn _list_remote_versions(&self) -> Result> { let versions = if SETTINGS.erlang.compile == Some(false) { - github::list_releases("erlef/otp_builds")? + github::list_releases("erlef/otp_builds") + .await? .into_iter() .filter_map(|r| r.tag_name.strip_prefix("OTP-").map(|s| s.to_string())) .collect() } else { - self.update_kerl()?; + self.update_kerl().await?; plugins::core::run_fetch_task_with_timeout(move || { let output = cmd!(self.kerl_path(), "list", "releases", "all") .env("KERL_BASE_DIR", self.ba.cache_path.join("kerl")) @@ -198,11 +210,11 @@ impl Backend for ErlangPlugin { Ok(versions) } - fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { - if let Some(tv) = self.install_precompiled(ctx, tv.clone())? { + async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { + if let Some(tv) = self.install_precompiled(ctx, tv.clone()).await? { return Ok(tv); } - self.install_via_kerl(ctx, tv) + self.install_via_kerl(ctx, tv).await } } diff --git a/src/plugins/core/go.rs b/src/plugins/core/go.rs index f102be1ceb..f3ae179175 100644 --- a/src/plugins/core/go.rs +++ b/src/plugins/core/go.rs @@ -1,7 +1,7 @@ -use std::collections::BTreeMap; use std::path::{Path, PathBuf}; -use std::thread; +use std::{collections::BTreeMap, sync::Arc}; +use crate::Result; use crate::backend::Backend; use crate::cli::args::BackendArg; use crate::cli::version::OS; @@ -13,6 +13,7 @@ use crate::install_context::InstallContext; use crate::toolset::{ToolRequest, ToolVersion, Toolset}; use crate::ui::progress_report::SingleReport; use crate::{cmd, env, file, plugins}; +use async_trait::async_trait; use itertools::Itertools; use tempfile::tempdir_in; use versions::Versioning; @@ -20,13 +21,13 @@ use xx::regex; #[derive(Debug)] pub struct GoPlugin { - ba: BackendArg, + ba: Arc, } impl GoPlugin { pub fn new() -> Self { Self { - ba: plugins::core::new_backend_arg("go"), + ba: Arc::new(plugins::core::new_backend_arg("go")), } } @@ -91,26 +92,30 @@ impl GoPlugin { .execute() } - fn download(&self, tv: &mut ToolVersion, pr: &Box) -> eyre::Result { + async fn download( + &self, + tv: &mut ToolVersion, + pr: &Box, + ) -> eyre::Result { let settings = Settings::get(); let filename = format!("go{}.{}-{}.{}", tv.version, platform(), arch(), ext()); - let tarball_url = format!("{}/{}", &settings.go_download_mirror, &filename); + let tarball_url = Arc::new(format!("{}/{}", &settings.go_download_mirror, &filename)); let tarball_path = tv.download_path().join(&filename); - thread::scope(|s| { - let checksum_handle = s.spawn(|| { - let checksum_url = format!("{}.sha256", &tarball_url); - HTTP.get_text(checksum_url) - }); - pr.set_message(format!("download {filename}")); - HTTP.download_file(&tarball_url, &tarball_path, Some(pr))?; + let tarball_url_ = tarball_url.clone(); + let checksum_handle = tokio::spawn(async move { + let checksum_url = format!("{}.sha256", &tarball_url_); + HTTP.get_text(checksum_url).await + }); + pr.set_message(format!("download {filename}")); + HTTP.download_file(&*tarball_url, &tarball_path, Some(pr)) + .await?; - if !settings.go_skip_checksum && !tv.checksums.contains_key(&filename) { - let checksum = checksum_handle.join().unwrap()?; - tv.checksums.insert(filename, format!("sha256:{checksum}")); - } - Ok(tarball_path) - }) + if !settings.go_skip_checksum && !tv.checksums.contains_key(&filename) { + let checksum = checksum_handle.await.unwrap()?; + tv.checksums.insert(filename, format!("sha256:{checksum}")); + } + Ok(tarball_path) } fn install( @@ -176,11 +181,12 @@ impl GoPlugin { } } +#[async_trait] impl Backend for GoPlugin { - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } - fn _list_remote_versions(&self) -> eyre::Result> { + async fn _list_remote_versions(&self) -> eyre::Result> { plugins::core::run_fetch_task_with_timeout(move || { let output = cmd!("git", "ls-remote", "--tags", &SETTINGS.go_repo, "go*").read()?; let lines = output.split('\n'); @@ -197,12 +203,12 @@ impl Backend for GoPlugin { Ok(vec![".go-version".into()]) } - fn install_version_( + async fn install_version_( &self, ctx: &InstallContext, mut tv: ToolVersion, - ) -> eyre::Result { - let tarball_path = self.download(&mut tv, &ctx.pr)?; + ) -> Result { + let tarball_path = self.download(&mut tv, &ctx.pr).await?; self.verify_checksum(ctx, &mut tv, &tarball_path)?; self.install(&tv, &ctx.pr, &tarball_path)?; self.verify(&tv, &ctx.pr)?; @@ -210,7 +216,7 @@ impl Backend for GoPlugin { Ok(tv) } - fn uninstall_version_impl( + async fn uninstall_version_impl( &self, _pr: &Box, tv: &ToolVersion, @@ -222,7 +228,7 @@ impl Backend for GoPlugin { Ok(()) } - fn list_bin_paths(&self, tv: &ToolVersion) -> eyre::Result> { + async fn list_bin_paths(&self, tv: &ToolVersion) -> eyre::Result> { if let ToolRequest::System { .. } = tv.request { return Ok(vec![]); } @@ -230,7 +236,7 @@ impl Backend for GoPlugin { Ok(vec![self.gobin(tv)]) } - fn exec_env( + async fn exec_env( &self, _config: &Config, _ts: &Toolset, diff --git a/src/plugins/core/java.rs b/src/plugins/core/java.rs index 7cd94b3cb7..5238fa18ee 100644 --- a/src/plugins/core/java.rs +++ b/src/plugins/core/java.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt::Display; use std::fs::{self}; use std::path::{Path, PathBuf}; +use std::sync::Arc; use crate::backend::Backend; use crate::cache::{CacheManager, CacheManagerBuilder}; @@ -16,6 +17,7 @@ use crate::plugins::VERSION_REGEX; use crate::toolset::{ToolVersion, Toolset}; use crate::ui::progress_report::SingleReport; use crate::{file, plugins}; +use async_trait::async_trait; use color_eyre::eyre::{Result, eyre}; use indoc::formatdoc; use itertools::Itertools; @@ -27,14 +29,14 @@ use xx::regex; #[derive(Debug)] pub struct JavaPlugin { - ba: BackendArg, + ba: Arc, java_metadata_ea_cache: CacheManager>, java_metadata_ga_cache: CacheManager>, } impl JavaPlugin { pub fn new() -> Self { - let ba = plugins::core::new_backend_arg("java"); + let ba = Arc::new(plugins::core::new_backend_arg("java")); let java_metadata_ga_cache_filename = format!("java_metadata_ga_{}_{}.msgpack.z", os(), arch()); let java_metadata_ea_cache_filename = @@ -54,26 +56,31 @@ impl JavaPlugin { } } - fn fetch_java_metadata(&self, release_type: &str) -> Result<&HashMap> { + async fn fetch_java_metadata( + &self, + release_type: &str, + ) -> Result<&HashMap> { let cache = if release_type == "ea" { &self.java_metadata_ea_cache } else { &self.java_metadata_ga_cache }; let release_type = release_type.to_string(); - cache.get_or_try_init(|| { - let mut metadata = HashMap::new(); - - for m in self.download_java_metadata(&release_type)?.into_iter() { - // add openjdk short versions like "java@17.0.0" which default to openjdk - if m.vendor == "openjdk" { - metadata.insert(m.version.to_string(), m.clone()); + cache + .get_or_try_init_async(async || { + let mut metadata = HashMap::new(); + + for m in self.download_java_metadata(&release_type).await? { + // add openjdk short versions like "java@17.0.0" which default to openjdk + if m.vendor == "openjdk" { + metadata.insert(m.version.to_string(), m.clone()); + } + metadata.insert(m.to_string(), m); } - metadata.insert(m.to_string(), m); - } - Ok(metadata) - }) + Ok(metadata) + }) + .await } fn java_bin(&self, tv: &ToolVersion) -> PathBuf { @@ -88,7 +95,7 @@ impl JavaPlugin { .execute() } - fn download( + async fn download( &self, ctx: &InstallContext, tv: &mut ToolVersion, @@ -99,7 +106,7 @@ impl JavaPlugin { let tarball_path = tv.download_path().join(filename); pr.set_message(format!("download {filename}")); - HTTP.download_file(&m.url, &tarball_path, Some(pr))?; + HTTP.download_file(&m.url, &tarball_path, Some(pr)).await?; if !tv.checksums.contains_key(filename) && m.checksum.is_some() { tv.checksums @@ -245,17 +252,18 @@ impl JavaPlugin { } } - fn tv_to_metadata(&self, tv: &ToolVersion) -> Result<&JavaMetadata> { + async fn tv_to_metadata(&self, tv: &ToolVersion) -> Result<&JavaMetadata> { let v: String = self.tv_to_java_version(tv); let release_type = self.tv_release_type(tv); let m = self - .fetch_java_metadata(&release_type)? + .fetch_java_metadata(&release_type) + .await? .get(&v) .ok_or_else(|| eyre!("no metadata found for version {}", tv.version))?; Ok(m) } - fn download_java_metadata(&self, release_type: &str) -> Result> { + async fn download_java_metadata(&self, release_type: &str) -> Result> { let url = format!( "https://mise-java.jdx.dev/jvm/{}/{}/{}.json", release_type, @@ -264,7 +272,8 @@ impl JavaPlugin { ); let metadata = HTTP_FETCH - .json::, _>(url)? + .json::, _>(url) + .await? .into_iter() .filter(|m| { m.file_type @@ -276,12 +285,13 @@ impl JavaPlugin { } } +#[async_trait] impl Backend for JavaPlugin { - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } - fn _list_remote_versions(&self) -> Result> { + async fn _list_remote_versions(&self) -> Result> { // TODO: find out how to get this to work for different os/arch // See https://github.com/jdx/mise/issues/1196 // match self.core.fetch_remote_versions_from_mise() { @@ -290,7 +300,8 @@ impl Backend for JavaPlugin { // Err(e) => warn!("failed to fetch remote versions: {}", e), // } let versions = self - .fetch_java_metadata("ga")? + .fetch_java_metadata("ga") + .await? .iter() .sorted_by_cached_key(|(v, m)| { let is_shorthand = regex!(r"^\d").is_match(v); @@ -322,8 +333,8 @@ impl Backend for JavaPlugin { self.fuzzy_match_filter(versions, query) } - fn list_versions_matching(&self, query: &str) -> eyre::Result> { - let versions = self.list_remote_versions()?; + async fn list_versions_matching(&self, query: &str) -> eyre::Result> { + let versions = self.list_remote_versions().await?; self.fuzzy_match_filter(versions, query) } @@ -373,20 +384,20 @@ impl Backend for JavaPlugin { } } - fn install_version_( + async fn install_version_( &self, ctx: &InstallContext, mut tv: ToolVersion, ) -> eyre::Result { - let metadata = self.tv_to_metadata(&tv)?; - let tarball_path = self.download(ctx, &mut tv, &ctx.pr, metadata)?; + let metadata = self.tv_to_metadata(&tv).await?; + let tarball_path = self.download(ctx, &mut tv, &ctx.pr, metadata).await?; self.install(&tv, &ctx.pr, &tarball_path, metadata)?; self.verify(&tv, &ctx.pr)?; Ok(tv) } - fn exec_env( + async fn exec_env( &self, _config: &Config, _ts: &Toolset, diff --git a/src/plugins/core/node.rs b/src/plugins/core/node.rs index ddaab9997d..84c9dc115e 100644 --- a/src/plugins/core/node.rs +++ b/src/plugins/core/node.rs @@ -11,48 +11,54 @@ use crate::install_context::InstallContext; use crate::toolset::ToolVersion; use crate::ui::progress_report::SingleReport; use crate::{env, file, gpg, hash, http, plugins}; +use async_trait::async_trait; use eyre::{Result, bail, ensure}; use serde_derive::Deserialize; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex, OnceLock}; +use std::sync::Arc; +use std::sync::OnceLock; use tempfile::tempdir_in; +use tokio::sync::Mutex; use url::Url; use xx::regex; #[derive(Debug)] pub struct NodePlugin { - ba: BackendArg, + ba: Arc, } impl NodePlugin { pub fn new() -> Self { Self { - ba: plugins::core::new_backend_arg("node"), + ba: plugins::core::new_backend_arg("node").into(), } } - fn install_precompiled( + async fn install_precompiled( &self, ctx: &InstallContext, tv: &mut ToolVersion, opts: &BuildOpts, ) -> Result<()> { let settings = Settings::get(); - match self.fetch_tarball( - ctx, - tv, - &ctx.pr, - &opts.binary_tarball_url, - &opts.binary_tarball_path, - &opts.version, - ) { + match self + .fetch_tarball( + ctx, + tv, + &ctx.pr, + &opts.binary_tarball_url, + &opts.binary_tarball_path, + &opts.version, + ) + .await + { Err(e) if settings.node.compile != Some(false) && matches!(http::error_code(&e), Some(404)) => { debug!("precompiled node not found"); - return self.install_compiled(ctx, tv, opts); + return self.install_compiled(ctx, tv, opts).await; } e => e, }?; @@ -71,20 +77,23 @@ impl NodePlugin { Ok(()) } - fn install_windows( + async fn install_windows( &self, ctx: &InstallContext, tv: &mut ToolVersion, opts: &BuildOpts, ) -> Result<()> { - match self.fetch_tarball( - ctx, - tv, - &ctx.pr, - &opts.binary_tarball_url, - &opts.binary_tarball_path, - &opts.version, - ) { + match self + .fetch_tarball( + ctx, + tv, + &ctx.pr, + &opts.binary_tarball_url, + &opts.binary_tarball_path, + &opts.version, + ) + .await + { Err(e) if matches!(http::error_code(&e), Some(404)) => { bail!("precompiled node not found {e}"); } @@ -102,7 +111,7 @@ impl NodePlugin { Ok(()) } - fn install_compiled( + async fn install_compiled( &self, ctx: &InstallContext, tv: &mut ToolVersion, @@ -116,7 +125,8 @@ impl NodePlugin { &opts.source_tarball_url, &opts.source_tarball_path, &opts.version, - )?; + ) + .await?; ctx.pr.set_message(format!("extract {tarball_name}")); file::remove_all(&opts.build_dir)?; file::untar( @@ -134,7 +144,7 @@ impl NodePlugin { Ok(()) } - fn fetch_tarball( + async fn fetch_tarball( &self, ctx: &InstallContext, tv: &mut ToolVersion, @@ -148,11 +158,11 @@ impl NodePlugin { pr.set_message(format!("using previously downloaded {tarball_name}")); } else { pr.set_message(format!("download {tarball_name}")); - HTTP.download_file(url.clone(), local, Some(pr))?; + HTTP.download_file(url.clone(), local, Some(pr)).await?; } if *env::MISE_NODE_VERIFY && !tv.checksums.contains_key(&tarball_name) { tv.checksums - .insert(tarball_name, self.get_checksum(ctx, local, version)?); + .insert(tarball_name, self.get_checksum(ctx, local, version).await?); } self.verify_checksum(ctx, tv, local)?; Ok(()) @@ -180,12 +190,18 @@ impl NodePlugin { self.sh(ctx, opts)?.arg(&opts.make_install_cmd).execute() } - fn get_checksum(&self, ctx: &InstallContext, tarball: &Path, version: &str) -> Result { + async fn get_checksum( + &self, + ctx: &InstallContext, + tarball: &Path, + version: &str, + ) -> Result { let tarball_name = tarball.file_name().unwrap().to_string_lossy().to_string(); let shasums_file = tarball.parent().unwrap().join("SHASUMS256.txt"); - HTTP.download_file(self.shasums_url(version)?, &shasums_file, Some(&ctx.pr))?; + HTTP.download_file(self.shasums_url(version)?, &shasums_file, Some(&ctx.pr)) + .await?; if SETTINGS.node.gpg_verify != Some(false) && version.starts_with("2") { - self.verify_with_gpg(ctx, &shasums_file, version)?; + self.verify_with_gpg(ctx, &shasums_file, version).await?; } let shasums = file::read_to_string(&shasums_file)?; let shasums = hash::parse_shasums(&shasums); @@ -193,14 +209,19 @@ impl NodePlugin { Ok(format!("sha256:{shasum}")) } - fn verify_with_gpg(&self, ctx: &InstallContext, shasums_file: &Path, v: &str) -> Result<()> { + async fn verify_with_gpg( + &self, + ctx: &InstallContext, + shasums_file: &Path, + v: &str, + ) -> Result<()> { if file::which_non_pristine("gpg").is_none() && SETTINGS.node.gpg_verify.is_none() { warn!("gpg not found, skipping verification"); return Ok(()); } let sig_file = shasums_file.with_extension("asc"); let sig_url = format!("{}.sig", self.shasums_url(v)?); - if let Err(e) = HTTP.download_file(sig_url, &sig_file, Some(&ctx.pr)) { + if let Err(e) = HTTP.download_file(sig_url, &sig_file, Some(&ctx.pr)).await { if matches!(http::error_code(&e), Some(404)) { warn!("gpg signature not found, skipping verification"); return Ok(()); @@ -244,7 +265,7 @@ impl NodePlugin { } } - fn install_default_packages( + async fn install_default_packages( &self, config: &Config, tv: &ToolVersion, @@ -263,7 +284,7 @@ impl NodePlugin { .arg("install") .arg("--global") .arg(package) - .envs(config.env()?) + .envs(config.env().await?) .env(&*env::PATH_KEY, plugins::core::path_env_with_tv_path(tv)?) .execute()?; } @@ -292,7 +313,7 @@ impl NodePlugin { Ok(()) } - fn test_node( + async fn test_node( &self, config: &Config, tv: &ToolVersion, @@ -302,11 +323,11 @@ impl NodePlugin { CmdLineRunner::new(self.node_path(tv)) .with_pr(pr) .arg("-v") - .envs(config.env()?) + .envs(config.env().await?) .execute() } - fn test_npm( + async fn test_npm( &self, config: &Config, tv: &ToolVersion, @@ -317,7 +338,7 @@ impl NodePlugin { .env(&*env::PATH_KEY, plugins::core::path_env_with_tv_path(tv)?) .with_pr(pr) .arg("-v") - .envs(config.env()?) + .envs(config.env().await?) .execute() } @@ -332,15 +353,17 @@ impl NodePlugin { } } +#[async_trait] impl Backend for NodePlugin { - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } - fn _list_remote_versions(&self) -> Result> { + async fn _list_remote_versions(&self) -> Result> { let base = SETTINGS.node.mirror_url(); let versions = HTTP_FETCH - .json::, _>(base.join("index.json")?)? + .json::, _>(base.join("index.json")?) + .await? .into_iter() .filter(|v| { if let Some(flavor) = &SETTINGS.node.flavor { @@ -408,7 +431,7 @@ impl Backend for NodePlugin { Ok(body) } - fn install_version_( + async fn install_version_( &self, ctx: &InstallContext, mut tv: ToolVersion, @@ -417,23 +440,23 @@ impl Backend for NodePlugin { tv.version != "latest", "version should not be 'latest' for node, something is wrong" ); - let config = Config::get(); + let config = Config::get().await; let settings = Settings::get(); - let opts = BuildOpts::new(ctx, &tv)?; + let opts = BuildOpts::new(ctx, &tv).await?; trace!("node build opts: {:#?}", opts); if cfg!(windows) { - self.install_windows(ctx, &mut tv, &opts)?; + self.install_windows(ctx, &mut tv, &opts).await?; } else if settings.node.compile == Some(true) { - self.install_compiled(ctx, &mut tv, &opts)?; + self.install_compiled(ctx, &mut tv, &opts).await?; } else { - self.install_precompiled(ctx, &mut tv, &opts)?; + self.install_precompiled(ctx, &mut tv, &opts).await?; } - self.test_node(&config, &tv, &ctx.pr)?; + self.test_node(&config, &tv, &ctx.pr).await?; if !cfg!(windows) { self.install_npm_shim(&tv)?; } - self.test_npm(&config, &tv, &ctx.pr)?; - if let Err(err) = self.install_default_packages(&config, &tv, &ctx.pr) { + self.test_npm(&config, &tv, &ctx.pr).await?; + if let Err(err) = self.install_default_packages(&config, &tv, &ctx.pr).await { warn!("failed to install default npm packages: {err:#}"); } if *env::MISE_NODE_COREPACK && self.corepack_path(&tv).exists() { @@ -444,7 +467,7 @@ impl Backend for NodePlugin { } #[cfg(windows)] - fn list_bin_paths(&self, tv: &ToolVersion) -> eyre::Result> { + async fn list_bin_paths(&self, tv: &ToolVersion) -> eyre::Result> { Ok(vec![tv.install_path()]) } @@ -485,7 +508,7 @@ struct BuildOpts { } impl BuildOpts { - fn new(ctx: &InstallContext, tv: &ToolVersion) -> Result { + async fn new(ctx: &InstallContext, tv: &ToolVersion) -> Result { let v = &tv.version; let install_path = tv.install_path(); let source_tarball_name = format!("node-v{v}.tar.gz"); @@ -498,7 +521,7 @@ impl BuildOpts { Ok(Self { version: v.clone(), - path: ctx.ts.list_paths(), + path: ctx.ts.list_paths().await, build_dir: env::MISE_TMP_DIR.join(format!("node-v{v}")), configure_cmd: configure_cmd(&install_path), make_cmd: make_cmd(), diff --git a/src/plugins/core/python.rs b/src/plugins/core/python.rs index d43fb35d61..89005e9c2f 100644 --- a/src/plugins/core/python.rs +++ b/src/plugins/core/python.rs @@ -12,20 +12,22 @@ use crate::toolset::{ToolRequest, ToolVersion, Toolset}; use crate::ui::progress_report::SingleReport; use crate::{Result, lock_file::LockFile}; use crate::{cmd, dirs, file, plugins, sysconfig}; +use async_trait::async_trait; use eyre::{bail, eyre}; use flate2::read::GzDecoder; use itertools::Itertools; use std::collections::BTreeMap; use std::io::Read; use std::path::{Path, PathBuf}; +use std::sync::LazyLock as Lazy; use std::sync::{Arc, OnceLock}; -use std::sync::{LazyLock as Lazy, Mutex}; +use tokio::sync::Mutex; use versions::Versioning; use xx::regex; #[derive(Debug)] pub struct PythonPlugin { - ba: BackendArg, + ba: Arc, } pub fn python_path(tv: &ToolVersion) -> PathBuf { @@ -38,7 +40,7 @@ pub fn python_path(tv: &ToolVersion) -> PathBuf { impl PythonPlugin { pub fn new() -> Self { - let ba = plugins::core::new_backend_arg("python"); + let ba = Arc::new(plugins::core::new_backend_arg("python")); Self { ba } } @@ -93,7 +95,9 @@ impl PythonPlugin { Ok(()) } - fn fetch_precompiled_remote_versions(&self) -> eyre::Result<&Vec<(String, String, String)>> { + async fn fetch_precompiled_remote_versions( + &self, + ) -> eyre::Result<&Vec<(String, String, String)>> { static PRECOMPILED_CACHE: Lazy>> = Lazy::new(|| { CacheManagerBuilder::new(dirs::CACHE.join("python").join("precompiled.msgpack.z")) @@ -101,56 +105,70 @@ impl PythonPlugin { .with_cache_key(python_precompiled_platform()) .build() }); - PRECOMPILED_CACHE.get_or_try_init(|| { - let url_path = python_precompiled_url_path(); - let rsp = match SETTINGS.paranoid { - true => HTTP_FETCH.get_bytes(format!("https://mise-versions.jdx.dev/{url_path}")), - // using http is not a security concern and enabling tls makes mise significantly slower - false => HTTP_FETCH.get_bytes(format!("http://mise-versions.jdx.dev/{url_path}")), - }?; - let mut decoder = GzDecoder::new(rsp.as_ref()); - let mut raw = String::new(); - decoder.read_to_string(&mut raw)?; - let platform = python_precompiled_platform(); - // order by version, whether it is a release candidate, date, and in the preferred order of install types - let rank = |v: &str, date: &str, name: &str| { - let rc = if regex!(r"rc\d+$").is_match(v) { 0 } else { 1 }; - let v = Versioning::new(v); - let date = date.parse::().unwrap_or_default(); - let install_type = if name.contains("install_only_stripped") { - 0 - } else if name.contains("install_only") { - 1 - } else { - 2 + PRECOMPILED_CACHE + .get_or_try_init_async(async || { + let url_path = python_precompiled_url_path(); + let rsp = match SETTINGS.paranoid { + true => { + HTTP_FETCH + .get_bytes(format!("https://mise-versions.jdx.dev/{url_path}")) + .await + } + // using http is not a security concern and enabling tls makes mise significantly slower + false => { + HTTP_FETCH + .get_bytes(format!("http://mise-versions.jdx.dev/{url_path}")) + .await + } + }?; + let mut decoder = GzDecoder::new(rsp.as_ref()); + let mut raw = String::new(); + decoder.read_to_string(&mut raw)?; + let platform = python_precompiled_platform(); + // order by version, whether it is a release candidate, date, and in the preferred order of install types + let rank = |v: &str, date: &str, name: &str| { + let rc = if regex!(r"rc\d+$").is_match(v) { 0 } else { 1 }; + let v = Versioning::new(v); + let date = date.parse::().unwrap_or_default(); + let install_type = if name.contains("install_only_stripped") { + 0 + } else if name.contains("install_only") { + 1 + } else { + 2 + }; + (v, rc, -date, install_type) }; - (v, rc, -date, install_type) - }; - let versions = raw - .lines() - .filter(|v| v.contains(&platform)) - .flat_map(|v| { - // cpython-3.9.5+20210525 or cpython-3.9.5rc3+20210525 - regex!(r"^cpython-(\d+\.\d+\.[\da-z]+)\+(\d+).*") - .captures(v) - .map(|caps| { - ( - caps[1].to_string(), - caps[2].to_string(), - caps[0].to_string(), - ) - }) - }) - // multiple dates can have the same version, so sort by date and remove duplicates by unique - .sorted_by_cached_key(|(v, date, name)| rank(v, date, name)) - .unique_by(|(v, _, _)| v.to_string()) - .collect_vec(); - Ok(versions) - }) + let versions = raw + .lines() + .filter(|v| v.contains(&platform)) + .flat_map(|v| { + // cpython-3.9.5+20210525 or cpython-3.9.5rc3+20210525 + regex!(r"^cpython-(\d+\.\d+\.[\da-z]+)\+(\d+).*") + .captures(v) + .map(|caps| { + ( + caps[1].to_string(), + caps[2].to_string(), + caps[0].to_string(), + ) + }) + }) + // multiple dates can have the same version, so sort by date and remove duplicates by unique + .sorted_by_cached_key(|(v, date, name)| rank(v, date, name)) + .unique_by(|(v, _, _)| v.to_string()) + .collect_vec(); + Ok(versions) + }) + .await } - fn install_precompiled(&self, ctx: &InstallContext, tv: &ToolVersion) -> eyre::Result<()> { - let precompiled_versions = self.fetch_precompiled_remote_versions()?; + async fn install_precompiled( + &self, + ctx: &InstallContext, + tv: &ToolVersion, + ) -> eyre::Result<()> { + let precompiled_versions = self.fetch_precompiled_remote_versions().await?; let precompile_info = precompiled_versions .iter() .rev() @@ -182,7 +200,7 @@ impl PythonPlugin { "available precompiled versions: {}", available.into_iter().join(", ") ); - return self.install_compiled(ctx, tv); + return self.install_compiled(ctx, tv).await; } }; @@ -204,7 +222,8 @@ impl PythonPlugin { let tarball_path = download.join(filename); ctx.pr.set_message(format!("download {filename}")); - HTTP.download_file(&url, &tarball_path, Some(&ctx.pr))?; + HTTP.download_file(&url, &tarball_path, Some(&ctx.pr)) + .await?; file::remove_all(&install)?; file::untar( @@ -254,8 +273,8 @@ impl PythonPlugin { Ok(()) } - fn install_compiled(&self, ctx: &InstallContext, tv: &ToolVersion) -> eyre::Result<()> { - let config = Config::get(); + async fn install_compiled(&self, ctx: &InstallContext, tv: &ToolVersion) -> eyre::Result<()> { + let config = Config::get().await; self.install_or_update_python_build(Some(ctx))?; if matches!(&tv.request, ToolRequest::Ref { .. }) { return Err(eyre!("Ref versions not supported for python")); @@ -266,14 +285,14 @@ impl PythonPlugin { .arg(tv.version.as_str()) .arg(tv.install_path()) .env("PIP_REQUIRE_VIRTUALENV", "false") - .envs(config.env()?); + .envs(config.env().await?); if SETTINGS.verbose { cmd = cmd.arg("--verbose"); } if let Some(patch_url) = &SETTINGS.python.patch_url { ctx.pr .set_message(format!("with patch file from: {patch_url}")); - let patch = HTTP.get_text(patch_url)?; + let patch = HTTP.get_text(patch_url).await?; cmd = cmd.arg("--patch").stdin_string(patch) } if let Some(patches_dir) = &SETTINGS.python.patches_directory { @@ -291,7 +310,7 @@ impl PythonPlugin { Ok(()) } - fn install_default_packages( + async fn install_default_packages( &self, config: &Config, packages_file: &Path, @@ -311,11 +330,11 @@ impl PythonPlugin { .arg("-r") .arg(packages_file) .env("PIP_REQUIRE_VIRTUALENV", "false") - .envs(config.env()?) + .envs(config.env().await?) .execute() } - fn get_virtualenv( + async fn get_virtualenv( &self, config: &Config, tv: &ToolVersion, @@ -342,7 +361,7 @@ impl PythonPlugin { .arg("-m") .arg("venv") .arg(&virtualenv) - .envs(config.env()?); + .envs(config.env().await?); if let Some(pr) = pr { cmd = cmd.with_pr(pr); } @@ -379,7 +398,7 @@ impl PythonPlugin { // Ok(()) // } - fn test_python( + async fn test_python( &self, config: &Config, tv: &ToolVersion, @@ -389,20 +408,22 @@ impl PythonPlugin { CmdLineRunner::new(python_path(tv)) .with_pr(pr) .arg("--version") - .envs(config.env()?) + .envs(config.env().await?) .execute() } } +#[async_trait] impl Backend for PythonPlugin { - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } - fn _list_remote_versions(&self) -> eyre::Result> { + async fn _list_remote_versions(&self) -> eyre::Result> { if cfg!(windows) || SETTINGS.python.compile == Some(false) { Ok(self - .fetch_precompiled_remote_versions()? + .fetch_precompiled_remote_versions() + .await? .iter() .map(|(v, _, _)| v.clone()) .collect()) @@ -430,20 +451,23 @@ impl Backend for PythonPlugin { ]) } - fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> eyre::Result { - let config = Config::get(); + async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { + let config = Config::get().await; if cfg!(windows) || SETTINGS.python.compile != Some(true) { - self.install_precompiled(ctx, &tv)?; + self.install_precompiled(ctx, &tv).await?; } else { - self.install_compiled(ctx, &tv)?; + self.install_compiled(ctx, &tv).await?; } - self.test_python(&config, &tv, &ctx.pr)?; - if let Err(e) = self.get_virtualenv(&config, &tv, Some(&ctx.pr)) { + self.test_python(&config, &tv, &ctx.pr).await?; + if let Err(e) = self.get_virtualenv(&config, &tv, Some(&ctx.pr)).await { warn!("failed to get virtualenv: {e:#}"); } if let Some(default_file) = &SETTINGS.python.default_packages_file { let default_file = file::replace_path(default_file); - if let Err(err) = self.install_default_packages(&config, &default_file, &tv, &ctx.pr) { + if let Err(err) = self + .install_default_packages(&config, &default_file, &tv, &ctx.pr) + .await + { warn!("failed to install default python packages: {err:#}"); } } @@ -451,18 +475,18 @@ impl Backend for PythonPlugin { } #[cfg(windows)] - fn list_bin_paths(&self, tv: &ToolVersion) -> eyre::Result> { + async fn list_bin_paths(&self, tv: &ToolVersion) -> eyre::Result> { Ok(vec![tv.install_path()]) } - fn exec_env( + async fn exec_env( &self, config: &Config, _ts: &Toolset, tv: &ToolVersion, ) -> eyre::Result> { let mut hm = BTreeMap::new(); - match self.get_virtualenv(config, tv, None) { + match self.get_virtualenv(config, tv, None).await { Err(e) => warn!("failed to get virtualenv: {e}"), Ok(Some(virtualenv)) => { let bin = virtualenv.join("bin"); @@ -478,15 +502,14 @@ impl Backend for PythonPlugin { static CACHE: OnceLock>> = OnceLock::new(); CACHE .get_or_init(|| { - Mutex::new( + Arc::new(Mutex::new( CacheManagerBuilder::new( self.ba().cache_path.join("remote_versions.msgpack.z"), ) .with_fresh_duration(SETTINGS.fetch_remote_versions_cache()) .with_cache_key((SETTINGS.python.compile == Some(false)).to_string()) .build(), - ) - .into() + )) }) .clone() } diff --git a/src/plugins/core/ruby.rs b/src/plugins/core/ruby.rs index bb90124ecf..ae0628be8f 100644 --- a/src/plugins/core/ruby.rs +++ b/src/plugins/core/ruby.rs @@ -1,6 +1,6 @@ -use std::collections::BTreeMap; use std::env::temp_dir; use std::path::{Path, PathBuf}; +use std::{collections::BTreeMap, sync::Arc}; use crate::backend::Backend; use crate::cli::args::BackendArg; @@ -16,19 +16,20 @@ use crate::lock_file::LockFile; use crate::toolset::{ToolVersion, Toolset}; use crate::ui::progress_report::SingleReport; use crate::{cmd, file, plugins, timeout}; +use async_trait::async_trait; use eyre::{Result, WrapErr}; use itertools::Itertools; use xx::regex; #[derive(Debug)] pub struct RubyPlugin { - ba: BackendArg, + ba: Arc, } impl RubyPlugin { pub fn new() -> Self { Self { - ba: plugins::core::new_backend_arg("ruby"), + ba: Arc::new(plugins::core::new_backend_arg("ruby")), } } @@ -61,13 +62,15 @@ impl RubyPlugin { .lock() } - fn update_build_tool(&self, ctx: Option<&InstallContext>) -> Result<()> { + async fn update_build_tool(&self, ctx: Option<&InstallContext>) -> Result<()> { let pr = ctx.map(|ctx| &ctx.pr); if SETTINGS.ruby.ruby_install { self.update_ruby_install(pr) + .await .wrap_err("failed to update ruby-install") } else { self.update_ruby_build(pr) + .await .wrap_err("failed to update ruby-build") } } @@ -94,11 +97,11 @@ impl RubyPlugin { file::remove_all(&tmp)?; Ok(()) } - fn update_ruby_build(&self, pr: Option<&Box>) -> Result<()> { + async fn update_ruby_build(&self, pr: Option<&Box>) -> Result<()> { let _lock = self.lock_build_tool(); if self.ruby_build_bin().exists() { let cur = self.ruby_build_version()?; - let latest = self.latest_ruby_build_version(); + let latest = self.latest_ruby_build_version().await; match (cur, latest) { // ruby-build is up-to-date (cur, Ok(latest)) if cur == latest => return Ok(()), @@ -141,7 +144,7 @@ impl RubyPlugin { file::remove_all(&tmp)?; Ok(()) } - fn update_ruby_install(&self, pr: Option<&Box>) -> Result<()> { + async fn update_ruby_install(&self, pr: Option<&Box>) -> Result<()> { let _lock = self.lock_build_tool(); let ruby_install_path = self.ruby_install_path(); if !ruby_install_path.exists() { @@ -170,7 +173,7 @@ impl RubyPlugin { tv.install_path().join("bin/gem") } - fn install_default_gems( + async fn install_default_gems( &self, config: &Config, tv: &ToolVersion, @@ -189,7 +192,7 @@ impl RubyPlugin { let mut cmd = CmdLineRunner::new(gem) .with_pr(pr) .arg("install") - .envs(config.env()?); + .envs(config.env().await?); match package.split_once(' ') { Some((name, "--pre")) => cmd = cmd.arg(name).arg("--pre"), Some((name, version)) => cmd = cmd.arg(name).arg("--version").arg(version), @@ -208,9 +211,10 @@ impl RubyPlugin { Ok(caps.get(1).unwrap().as_str().to_string()) } - fn latest_ruby_build_version(&self) -> Result { - let release: GithubRelease = - HTTP_FETCH.json("https://api.github.com/repos/rbenv/ruby-build/releases/latest")?; + async fn latest_ruby_build_version(&self) -> Result { + let release: GithubRelease = HTTP_FETCH + .json("https://api.github.com/repos/rbenv/ruby-build/releases/latest") + .await?; Ok(release.tag_name.trim_start_matches('v').to_string()) } @@ -222,7 +226,7 @@ impl RubyPlugin { Ok(()) } - fn install_cmd<'a>( + async fn install_cmd<'a>( &self, config: &Config, tv: &ToolVersion, @@ -234,9 +238,9 @@ impl RubyPlugin { } else { CmdLineRunner::new(self.ruby_build_bin()) .args(self.install_args_ruby_build(tv)?) - .stdin_string(self.fetch_patches()?) + .stdin_string(self.fetch_patches().await?) }; - Ok(cmd.with_pr(pr).envs(config.env()?)) + Ok(cmd.with_pr(pr).envs(config.env().await?)) } fn install_args_ruby_build(&self, tv: &ToolVersion) -> Result> { let settings = Settings::get(); @@ -293,12 +297,12 @@ impl RubyPlugin { .collect() } - fn fetch_patches(&self) -> Result { + async fn fetch_patches(&self) -> Result { let mut patches = vec![]; let re = regex!(r#"^[Hh][Tt][Tt][Pp][Ss]?://"#); for f in &self.fetch_patch_sources() { if re.is_match(f) { - patches.push(HTTP.get_text(f)?); + patches.push(HTTP.get_text(f).await?); } else { patches.push(file::read_to_string(f)?); } @@ -307,14 +311,15 @@ impl RubyPlugin { } } +#[async_trait] impl Backend for RubyPlugin { - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } - fn _list_remote_versions(&self) -> Result> { - timeout::run_with_timeout( - || { - if let Err(err) = self.update_build_tool(None) { + async fn _list_remote_versions(&self) -> Result> { + timeout::run_with_timeout_async( + async || { + if let Err(err) = self.update_build_tool(None).await { warn!("{err}"); } let ruby_build_bin = self.ruby_build_bin(); @@ -331,6 +336,7 @@ impl Backend for RubyPlugin { }, SETTINGS.fetch_remote_versions_timeout(), ) + .await } fn idiomatic_filenames(&self) -> Result> { @@ -352,22 +358,22 @@ impl Backend for RubyPlugin { Ok(v) } - fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> eyre::Result { - if let Err(err) = self.update_build_tool(Some(ctx)) { + async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { + if let Err(err) = self.update_build_tool(Some(ctx)).await { warn!("ruby build tool update error: {err:#}"); } ctx.pr.set_message("ruby-build".into()); - let config = Config::get(); - self.install_cmd(&config, &tv, &ctx.pr)?.execute()?; + let config = Config::get().await; + self.install_cmd(&config, &tv, &ctx.pr).await?.execute()?; self.install_rubygems_hook(&tv)?; - if let Err(err) = self.install_default_gems(&config, &tv, &ctx.pr) { + if let Err(err) = self.install_default_gems(&config, &tv, &ctx.pr).await { warn!("failed to install default ruby gems {err:#}"); } Ok(tv) } - fn exec_env( + async fn exec_env( &self, _config: &Config, _ts: &Toolset, diff --git a/src/plugins/core/ruby_windows.rs b/src/plugins/core/ruby_windows.rs index 7556fe895d..63fe50a1b0 100644 --- a/src/plugins/core/ruby_windows.rs +++ b/src/plugins/core/ruby_windows.rs @@ -1,5 +1,8 @@ -use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, + sync::Arc, +}; use crate::backend::Backend; use crate::cli::args::BackendArg; @@ -12,6 +15,7 @@ use crate::install_context::InstallContext; use crate::toolset::{ToolVersion, Toolset}; use crate::ui::progress_report::SingleReport; use crate::{file, github, plugins}; +use async_trait::async_trait; use eyre::Result; use itertools::Itertools; use versions::Versioning; @@ -19,13 +23,13 @@ use xx::regex; #[derive(Debug)] pub struct RubyPlugin { - ba: BackendArg, + ba: Arc, } impl RubyPlugin { pub fn new() -> Self { Self { - ba: plugins::core::new_backend_arg("ruby"), + ba: plugins::core::new_backend_arg("ruby").into(), } } @@ -37,7 +41,7 @@ impl RubyPlugin { tv.install_path().join("bin").join("gem.cmd") } - fn install_default_gems( + async fn install_default_gems( &self, config: &Config, tv: &ToolVersion, @@ -56,7 +60,7 @@ impl RubyPlugin { let mut cmd = CmdLineRunner::new(gem) .with_pr(pr) .arg("install") - .envs(config.env()?); + .envs(config.env().await?); match package.split_once(' ') { Some((name, "--pre")) => cmd = cmd.arg(name).arg("--pre"), Some((name, version)) => cmd = cmd.arg(name).arg("--version").arg(version), @@ -68,16 +72,17 @@ impl RubyPlugin { Ok(()) } - fn test_ruby(&self, tv: &ToolVersion, pr: &Box) -> Result<()> { + async fn test_ruby(&self, tv: &ToolVersion, pr: &Box) -> Result<()> { + let config = Config::get().await; pr.set_message("ruby -v".into()); CmdLineRunner::new(self.ruby_path(tv)) .with_pr(pr) .arg("-v") - .envs(Config::get().env()?) + .envs(config.env().await?) .execute() } - fn test_gem( + async fn test_gem( &self, config: &Config, tv: &ToolVersion, @@ -87,7 +92,7 @@ impl RubyPlugin { CmdLineRunner::new(self.gem_path(tv)) .with_pr(pr) .arg("-v") - .envs(config.env()?) + .envs(config.env().await?) .env(&*PATH_KEY, plugins::core::path_env_with_tv_path(tv)?) .execute() } @@ -100,22 +105,27 @@ impl RubyPlugin { Ok(()) } - fn download(&self, tv: &ToolVersion, pr: &Box) -> Result { + async fn download(&self, tv: &ToolVersion, pr: &Box) -> Result { let arch = arch(); let url = format!( "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-{version}-1/rubyinstaller-{version}-1-{arch}.7z", version = tv.version, ); - let filename = url.split('/').last().unwrap(); + let filename = url.split('/').next_back().unwrap(); let tarball_path = tv.download_path().join(filename); pr.set_message(format!("downloading {filename}")); - HTTP.download_file(&url, &tarball_path, Some(pr))?; + HTTP.download_file(&url, &tarball_path, Some(pr)).await?; Ok(tarball_path) } - fn install(&self, ctx: &InstallContext, tv: &ToolVersion, tarball_path: &Path) -> Result<()> { + async fn install( + &self, + ctx: &InstallContext, + tv: &ToolVersion, + tarball_path: &Path, + ) -> Result<()> { let arch = arch(); let filename = tarball_path.file_name().unwrap().to_string_lossy(); ctx.pr.set_message(format!("extract {filename}")); @@ -129,23 +139,24 @@ impl RubyPlugin { Ok(()) } - fn verify(&self, ctx: &InstallContext, tv: &ToolVersion) -> Result<()> { - self.test_ruby(tv, &ctx.pr) + async fn verify(&self, ctx: &InstallContext, tv: &ToolVersion) -> Result<()> { + self.test_ruby(tv, &ctx.pr).await } } +#[async_trait] impl Backend for RubyPlugin { - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } - fn _list_remote_versions(&self) -> Result> { + async fn _list_remote_versions(&self) -> Result> { // TODO: use windows set of versions // match self.core.fetch_remote_versions_from_mise() { // Ok(Some(versions)) => return Ok(versions), // Ok(None) => {} // Err(e) => warn!("failed to fetch remote versions: {}", e), // } - let releases: Vec = github::list_releases("oneclick/rubyinstaller2")?; + let releases: Vec = github::list_releases("oneclick/rubyinstaller2").await?; let versions = releases .into_iter() .map(|r| r.tag_name) @@ -180,25 +191,25 @@ impl Backend for RubyPlugin { Ok(v) } - fn install_version_( + async fn install_version_( &self, ctx: &InstallContext, mut tv: ToolVersion, ) -> eyre::Result { - let config = Config::get(); - let tarball = self.download(&tv, &ctx.pr)?; + let config = Config::get().await; + let tarball = self.download(&tv, &ctx.pr).await?; self.verify_checksum(ctx, &mut tv, &tarball)?; - self.install(ctx, &tv, &tarball)?; - self.verify(ctx, &tv)?; + self.install(ctx, &tv, &tarball).await?; + self.verify(ctx, &tv).await?; self.install_rubygems_hook(&tv)?; - self.test_gem(&config, &tv, &ctx.pr)?; - if let Err(err) = self.install_default_gems(&config, &tv, &ctx.pr) { + self.test_gem(&config, &tv, &ctx.pr).await?; + if let Err(err) = self.install_default_gems(&config, &tv, &ctx.pr).await { warn!("failed to install default ruby gems {err:#}"); } Ok(tv) } - fn exec_env( + async fn exec_env( &self, _config: &Config, _ts: &Toolset, @@ -253,16 +264,17 @@ mod tests { use super::*; - #[test] - fn test_list_versions_matching() { + #[tokio::test] + async fn test_list_versions_matching() { let plugin = RubyPlugin::new(); assert!( - !plugin.list_versions_matching("3").unwrap().is_empty(), + !plugin.list_versions_matching("3").await.unwrap().is_empty(), "versions for 3 should not be empty" ); assert!( !plugin .list_versions_matching("truffleruby-24") + .await .unwrap() .is_empty(), "versions for truffleruby-24 should not be empty" @@ -270,6 +282,7 @@ mod tests { assert!( !plugin .list_versions_matching("truffleruby+graalvm-24") + .await .unwrap() .is_empty(), "versions for truffleruby+graalvm-24 should not be empty" diff --git a/src/plugins/core/rust.rs b/src/plugins/core/rust.rs index 3c23d3fbf1..86de6017f6 100644 --- a/src/plugins/core/rust.rs +++ b/src/plugins/core/rust.rs @@ -1,5 +1,5 @@ -use std::collections::BTreeMap; use std::path::{Path, PathBuf}; +use std::{collections::BTreeMap, sync::Arc}; use crate::backend::Backend; use crate::build_time::TARGET; @@ -13,47 +13,53 @@ use crate::toolset::outdated_info::OutdatedInfo; use crate::toolset::{ToolVersion, Toolset}; use crate::ui::progress_report::SingleReport; use crate::{dirs, env, file, github, plugins}; +use async_trait::async_trait; use eyre::Result; use xx::regex; #[derive(Debug)] pub struct RustPlugin { - ba: BackendArg, + ba: Arc, } impl RustPlugin { pub fn new() -> Self { Self { - ba: plugins::core::new_backend_arg("rust"), + ba: plugins::core::new_backend_arg("rust").into(), } } - fn setup_rustup(&self, ctx: &InstallContext, tv: &ToolVersion) -> Result<()> { + async fn setup_rustup(&self, ctx: &InstallContext, tv: &ToolVersion) -> Result<()> { if rustup_home().join("settings.toml").exists() && cargo_bin().exists() { return Ok(()); } ctx.pr.set_message("Downloading rustup-init".into()); - HTTP.download_file(rustup_url(), &rustup_path(), Some(&ctx.pr))?; + HTTP.download_file(rustup_url(), &rustup_path(), Some(&ctx.pr)) + .await?; file::make_executable(rustup_path())?; file::create_dir_all(rustup_home())?; + let config = Config::get().await; + let ts = config.get_toolset().await?; let cmd = CmdLineRunner::new(rustup_path()) .with_pr(&ctx.pr) .arg("--no-modify-path") .arg("--default-toolchain") .arg("none") .arg("-y") - .envs(self.exec_env(&Config::get(), Config::get().get_toolset()?, tv)?); + .envs(self.exec_env(&config, ts, tv).await?); cmd.execute()?; Ok(()) } - fn test_rust(&self, ctx: &InstallContext, tv: &ToolVersion) -> Result<()> { + async fn test_rust(&self, ctx: &InstallContext, tv: &ToolVersion) -> Result<()> { ctx.pr.set_message(format!("{RUSTC_BIN} -V")); + let config = Config::get().await; + let ts = config.get_toolset().await?; CmdLineRunner::new(RUSTC_BIN) .with_pr(&ctx.pr) .arg("-V") - .envs(self.exec_env(&Config::get(), Config::get().get_toolset()?, tv)?) - .prepend_path(self.list_bin_paths(tv)?)? + .envs(self.exec_env(&config, ts, tv).await?) + .prepend_path(self.list_bin_paths(tv).await?)? .execute() } @@ -62,13 +68,15 @@ impl RustPlugin { } } +#[async_trait] impl Backend for RustPlugin { - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } - fn _list_remote_versions(&self) -> Result> { - let versions = github::list_releases("rust-lang/rust")? + async fn _list_remote_versions(&self) -> Result> { + let versions = github::list_releases("rust-lang/rust") + .await? .into_iter() .map(|r| r.tag_name) .rev() @@ -90,8 +98,10 @@ impl Backend for RustPlugin { Ok(rt.channel) } - fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { - self.setup_rustup(ctx, &tv)?; + async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { + self.setup_rustup(ctx, &tv).await?; + let config = Config::try_get().await?; + let ts = config.get_toolset().await?; let (profile, components, targets) = get_args(&tv); @@ -104,36 +114,42 @@ impl Backend for RustPlugin { .opt_arg(profile) .opt_args("--component", components) .opt_args("--target", targets) - .prepend_path(self.list_bin_paths(&tv)?)? - .envs(self.exec_env(&Config::get(), Config::get().get_toolset()?, &tv)?) + .prepend_path(self.list_bin_paths(&tv).await?)? + .envs(self.exec_env(&config, ts, &tv).await?) .execute()?; file::remove_all(tv.install_path())?; file::make_symlink(&cargo_home().join("bin"), &tv.install_path())?; - self.test_rust(ctx, &tv)?; + self.test_rust(ctx, &tv).await?; Ok(tv) } - fn uninstall_version_impl(&self, pr: &Box, tv: &ToolVersion) -> Result<()> { - let mut env = self.exec_env(&Config::get(), Config::get().get_toolset()?, tv)?; + async fn uninstall_version_impl( + &self, + pr: &Box, + tv: &ToolVersion, + ) -> Result<()> { + let config = Config::try_get().await?; + let ts = config.get_toolset().await?; + let mut env = self.exec_env(&config, ts, tv).await?; env.remove("RUSTUP_TOOLCHAIN"); CmdLineRunner::new(RUSTUP_BIN) .with_pr(pr) .arg("toolchain") .arg("uninstall") .arg(&tv.version) - .prepend_path(self.list_bin_paths(tv)?)? + .prepend_path(self.list_bin_paths(tv).await?)? .envs(env) .execute() } - fn list_bin_paths(&self, _tv: &ToolVersion) -> Result> { + async fn list_bin_paths(&self, _tv: &ToolVersion) -> Result> { Ok(vec![cargo_bindir()]) } - fn exec_env( + async fn exec_env( &self, _config: &Config, _ts: &Toolset, @@ -154,14 +170,17 @@ impl Backend for RustPlugin { .into()) } - fn outdated_info(&self, tv: &ToolVersion, bump: bool) -> Result> { + async fn outdated_info(&self, tv: &ToolVersion, bump: bool) -> Result> { + let config = Config::get().await; let v_re = regex!(r#"Update available : (.*) -> (.*)"#); if regex!(r"(\d+)\.(\d+)\.(\d+)").is_match(&tv.version) { - let oi = OutdatedInfo::resolve(tv.clone(), bump)?; + let oi = OutdatedInfo::resolve(&config, tv.clone(), bump).await?; Ok(oi) } else { - let mut cmd = cmd!(RUSTUP_BIN, "check").env("PATH", self.path_env_for_cmd(tv)?); - for (k, v) in self.exec_env(&Config::get(), Config::get().get_toolset()?, tv)? { + let config = Config::get().await; + let ts = config.get_toolset().await?; + let mut cmd = cmd!(RUSTUP_BIN, "check").env("PATH", self.path_env_for_cmd(tv).await?); + for (k, v) in self.exec_env(&config, ts, tv).await? { cmd = cmd.env(k, v); } let out = cmd.read()?; @@ -170,7 +189,7 @@ impl Backend for RustPlugin { if let Some(_cap) = v_re.captures(line) { // let requested = cap.get(1).unwrap().as_str().to_string(); // let latest = cap.get(2).unwrap().as_str().to_string(); - let oi = OutdatedInfo::new(tv.clone(), tv.version.clone())?; + let oi = OutdatedInfo::new(&config, tv.clone(), tv.version.clone())?; return Ok(Some(oi)); } } @@ -288,7 +307,7 @@ fn rustup_url() -> String { #[cfg(windows)] fn rustup_url() -> String { - let arch = &*SETTINGS.arch(); + let arch = SETTINGS.arch(); format!("https://win.rustup.rs/{arch}") } diff --git a/src/plugins/core/swift.rs b/src/plugins/core/swift.rs index 6c3231d02c..3110377551 100644 --- a/src/plugins/core/swift.rs +++ b/src/plugins/core/swift.rs @@ -7,19 +7,23 @@ use crate::install_context::InstallContext; use crate::toolset::ToolVersion; use crate::ui::progress_report::SingleReport; use crate::{file, github, gpg, plugins}; +use async_trait::async_trait; use eyre::Result; -use std::path::{Path, PathBuf}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; use tempfile::tempdir_in; #[derive(Debug)] pub struct SwiftPlugin { - ba: BackendArg, + ba: Arc, } impl SwiftPlugin { pub fn new() -> Self { Self { - ba: plugins::core::new_backend_arg("swift"), + ba: Arc::new(plugins::core::new_backend_arg("swift")), } } @@ -35,7 +39,7 @@ impl SwiftPlugin { .execute() } - fn download(&self, tv: &ToolVersion, pr: &Box) -> Result { + async fn download(&self, tv: &ToolVersion, pr: &Box) -> Result { let url = format!( "https://download.swift.org/swift-{version}-release/{platform_directory}/swift-{version}-RELEASE/swift-{version}-RELEASE-{platform}{architecture}.{extension}", version = tv.version, @@ -51,7 +55,7 @@ impl SwiftPlugin { let tarball_path = tv.download_path().join(filename); if !tarball_path.exists() { pr.set_message(format!("download {filename}")); - HTTP.download_file(&url, &tarball_path, Some(pr))?; + HTTP.download_file(&url, &tarball_path, Some(pr)).await?; } Ok(tarball_path) @@ -112,7 +116,7 @@ impl SwiftPlugin { Ok(()) } - fn verify_gpg( + async fn verify_gpg( &self, ctx: &InstallContext, tv: &ToolVersion, @@ -125,7 +129,8 @@ impl SwiftPlugin { } gpg::add_keys_swift(ctx)?; let sig_path = PathBuf::from(format!("{}.sig", tarball_path.to_string_lossy())); - HTTP.download_file(format!("{}.sig", url(tv)), &sig_path, Some(&ctx.pr))?; + HTTP.download_file(format!("{}.sig", url(tv)), &sig_path, Some(&ctx.pr)) + .await?; self.gpg(ctx) .arg("--quiet") .arg("--trust-model") @@ -146,13 +151,15 @@ impl SwiftPlugin { } } +#[async_trait] impl Backend for SwiftPlugin { - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } - fn _list_remote_versions(&self) -> Result> { - let versions = github::list_releases("swiftlang/swift")? + async fn _list_remote_versions(&self) -> Result> { + let versions = github::list_releases("swiftlang/swift") + .await? .into_iter() .map(|r| r.tag_name) .filter_map(|v| v.strip_prefix("swift-").map(|v| v.to_string())) @@ -170,10 +177,14 @@ impl Backend for SwiftPlugin { } } - fn install_version_(&self, ctx: &InstallContext, mut tv: ToolVersion) -> Result { - let tarball_path = self.download(&tv, &ctx.pr)?; + async fn install_version_( + &self, + ctx: &InstallContext, + mut tv: ToolVersion, + ) -> Result { + let tarball_path = self.download(&tv, &ctx.pr).await?; if cfg!(target_os = "linux") && SETTINGS.swift.gpg_verify != Some(false) { - self.verify_gpg(ctx, &tv, &tarball_path)?; + self.verify_gpg(ctx, &tv, &tarball_path).await?; } self.verify_checksum(ctx, &mut tv, &tarball_path)?; self.install(ctx, &tv, &tarball_path)?; diff --git a/src/plugins/core/zig.rs b/src/plugins/core/zig.rs index 0d008ac259..554a2c1980 100644 --- a/src/plugins/core/zig.rs +++ b/src/plugins/core/zig.rs @@ -1,4 +1,7 @@ -use std::path::{Path, PathBuf}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; use crate::backend::Backend; use crate::cli::args::BackendArg; @@ -8,10 +11,10 @@ use crate::config::SETTINGS; use crate::file::TarOptions; use crate::http::{HTTP, HTTP_FETCH}; use crate::install_context::InstallContext; -use crate::toolset::{ToolRequest, ToolVersion}; +use crate::toolset::ToolVersion; use crate::ui::progress_report::SingleReport; use crate::{file, github, minisign, plugins}; -use contracts::requires; +use async_trait::async_trait; use eyre::Result; use itertools::Itertools; use versions::Versioning; @@ -19,7 +22,7 @@ use xx::regex; #[derive(Debug)] pub struct ZigPlugin { - ba: BackendArg, + ba: Arc, } const ZIG_MINISIGN_KEY: &str = "RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U"; @@ -27,7 +30,7 @@ const ZIG_MINISIGN_KEY: &str = "RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wC impl ZigPlugin { pub fn new() -> Self { Self { - ba: plugins::core::new_backend_arg("zig"), + ba: Arc::new(plugins::core::new_backend_arg("zig")), } } @@ -47,7 +50,7 @@ impl ZigPlugin { .execute() } - fn download(&self, tv: &ToolVersion, pr: &Box) -> Result { + async fn download(&self, tv: &ToolVersion, pr: &Box) -> Result { let archive_ext = if cfg!(target_os = "windows") { "zip" } else { @@ -59,14 +62,14 @@ impl ZigPlugin { "https://ziglang.org/builds/zig-{}-{}-{}.{archive_ext}", os(), arch(), - self.get_version_from_json("master")? + self.get_version_from_json("master").await? ) } else if tv.version == "ref:mach-latest" { format!( "https://pkg.machengine.org/zig/zig-{}-{}-{}.{archive_ext}", os(), arch(), - self.get_version_from_json("mach-latest")? + self.get_version_from_json("mach-latest").await? ) } else if regex!(r"^[0-9]+\.[0-9]+\.[0-9]+-dev.[0-9]+\+[0-9a-f]+$").is_match(&tv.version) { format!( @@ -89,11 +92,11 @@ impl ZigPlugin { let tarball_path = tv.download_path().join(filename); pr.set_message(format!("download {filename}")); - HTTP.download_file(&url, &tarball_path, Some(pr))?; + HTTP.download_file(&url, &tarball_path, Some(pr)).await?; pr.set_message(format!("minisign {filename}")); let tarball_data = file::read(&tarball_path)?; - let sig = HTTP.get_text(format!("{url}.minisig"))?; + let sig = HTTP.get_text(format!("{url}.minisig")).await?; minisign::verify(ZIG_MINISIGN_KEY, &tarball_data, &sig)?; Ok(tarball_path) @@ -125,7 +128,7 @@ impl ZigPlugin { self.test_zig(ctx, tv) } - fn get_version_from_json(&self, key: &str) -> Result { + async fn get_version_from_json(&self, key: &str) -> Result { let json_url: &str = if key == "master" { "https://ziglang.org/download/index.json" } else if key == "mach-latest" { @@ -135,7 +138,7 @@ impl ZigPlugin { "" }; - let version_json: serde_json::Value = HTTP_FETCH.json(json_url)?; + let version_json: serde_json::Value = HTTP_FETCH.json(json_url).await?; let zig_version = version_json .pointer(&format!("/{key}/version")) .and_then(|v| v.as_str()) @@ -144,13 +147,15 @@ impl ZigPlugin { } } +#[async_trait] impl Backend for ZigPlugin { - fn ba(&self) -> &BackendArg { + fn ba(&self) -> &Arc { &self.ba } - fn _list_remote_versions(&self) -> Result> { - let versions: Vec = github::list_releases("ziglang/zig")? + async fn _list_remote_versions(&self) -> Result> { + let versions: Vec = github::list_releases("ziglang/zig") + .await? .into_iter() .map(|r| r.tag_name) .unique() @@ -159,7 +164,7 @@ impl Backend for ZigPlugin { Ok(versions) } - fn list_bin_paths(&self, tv: &ToolVersion) -> Result> { + async fn list_bin_paths(&self, tv: &ToolVersion) -> Result> { if cfg!(windows) { Ok(vec![tv.install_path()]) } else { @@ -171,9 +176,12 @@ impl Backend for ZigPlugin { Ok(vec![".zig-version".into()]) } - #[requires(matches!(tv.request, ToolRequest::Version { .. } | ToolRequest::Prefix { .. } | ToolRequest::Ref { .. }), "unsupported tool version request type")] - fn install_version_(&self, ctx: &InstallContext, mut tv: ToolVersion) -> Result { - let tarball_path = self.download(&tv, &ctx.pr)?; + async fn install_version_( + &self, + ctx: &InstallContext, + mut tv: ToolVersion, + ) -> Result { + let tarball_path = self.download(&tv, &ctx.pr).await?; self.verify_checksum(ctx, &mut tv, &tarball_path)?; self.install(ctx, &tv, &tarball_path)?; self.verify(ctx, &tv)?; diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs index fa26ea68e6..39a2a6de44 100644 --- a/src/plugins/mod.rs +++ b/src/plugins/mod.rs @@ -5,15 +5,19 @@ use crate::plugins::vfox_plugin::VfoxPlugin; use crate::toolset::install_state; use crate::ui::multi_progress_report::MultiProgressReport; use crate::ui::progress_report::SingleReport; +use async_trait::async_trait; use clap::Command; use eyre::{Result, eyre}; use heck::ToKebabCase; use regex::Regex; pub use script_manager::{Script, ScriptManager}; -use std::fmt::{Debug, Display}; use std::path::PathBuf; use std::sync::LazyLock as Lazy; use std::vec; +use std::{ + fmt::{Debug, Display}, + sync::Arc, +}; pub mod asdf_plugin; pub mod core; @@ -27,6 +31,127 @@ pub enum PluginType { Vfox, } +#[derive(Debug)] +pub enum PluginEnum { + Asdf(Arc), + Vfox(Arc), +} + +impl PluginEnum { + pub fn name(&self) -> &str { + match self { + PluginEnum::Asdf(plugin) => plugin.name(), + PluginEnum::Vfox(plugin) => plugin.name(), + } + } + + pub fn path(&self) -> PathBuf { + match self { + PluginEnum::Asdf(plugin) => plugin.path(), + PluginEnum::Vfox(plugin) => plugin.path(), + } + } + + pub fn get_plugin_type(&self) -> PluginType { + match self { + PluginEnum::Asdf(_) => PluginType::Asdf, + PluginEnum::Vfox(_) => PluginType::Vfox, + } + } + + pub fn get_remote_url(&self) -> eyre::Result> { + match self { + PluginEnum::Asdf(plugin) => plugin.get_remote_url(), + PluginEnum::Vfox(plugin) => plugin.get_remote_url(), + } + } + + pub fn set_remote_url(&self, url: String) { + match self { + PluginEnum::Asdf(plugin) => plugin.set_remote_url(url), + PluginEnum::Vfox(plugin) => plugin.set_remote_url(url), + } + } + + pub fn current_abbrev_ref(&self) -> eyre::Result> { + match self { + PluginEnum::Asdf(plugin) => plugin.current_abbrev_ref(), + PluginEnum::Vfox(plugin) => plugin.current_abbrev_ref(), + } + } + + pub fn current_sha_short(&self) -> eyre::Result> { + match self { + PluginEnum::Asdf(plugin) => plugin.current_sha_short(), + PluginEnum::Vfox(plugin) => plugin.current_sha_short(), + } + } + + pub fn external_commands(&self) -> eyre::Result> { + match self { + PluginEnum::Asdf(plugin) => plugin.external_commands(), + PluginEnum::Vfox(plugin) => plugin.external_commands(), + } + } + + pub fn execute_external_command(&self, command: &str, args: Vec) -> eyre::Result<()> { + match self { + PluginEnum::Asdf(plugin) => plugin.execute_external_command(command, args), + PluginEnum::Vfox(plugin) => plugin.execute_external_command(command, args), + } + } + + pub async fn update( + &self, + pr: &Box, + gitref: Option, + ) -> eyre::Result<()> { + match self { + PluginEnum::Asdf(plugin) => plugin.update(pr, gitref).await, + PluginEnum::Vfox(plugin) => plugin.update(pr, gitref).await, + } + } + + pub async fn uninstall(&self, pr: &Box) -> eyre::Result<()> { + match self { + PluginEnum::Asdf(plugin) => plugin.uninstall(pr).await, + PluginEnum::Vfox(plugin) => plugin.uninstall(pr).await, + } + } + + pub async fn install(&self, pr: &Box) -> eyre::Result<()> { + match self { + PluginEnum::Asdf(plugin) => plugin.install(pr).await, + PluginEnum::Vfox(plugin) => plugin.install(pr).await, + } + } + + pub fn is_installed(&self) -> bool { + match self { + PluginEnum::Asdf(plugin) => plugin.is_installed(), + PluginEnum::Vfox(plugin) => plugin.is_installed(), + } + } + + pub fn is_installed_err(&self) -> eyre::Result<()> { + match self { + PluginEnum::Asdf(plugin) => plugin.is_installed_err(), + PluginEnum::Vfox(plugin) => plugin.is_installed_err(), + } + } + + pub async fn ensure_installed( + &self, + mpr: &MultiProgressReport, + force: bool, + ) -> eyre::Result<()> { + match self { + PluginEnum::Asdf(plugin) => plugin.ensure_installed(mpr, force).await, + PluginEnum::Vfox(plugin) => plugin.ensure_installed(mpr, force).await, + } + } +} + impl PluginType { pub fn from_full(full: &str) -> eyre::Result { match full.split(':').next() { @@ -36,11 +161,11 @@ impl PluginType { } } - pub fn plugin(&self, short: String) -> APlugin { + pub fn plugin(&self, short: String) -> PluginEnum { let path = dirs::PLUGINS.join(short.to_kebab_case()); match self { - PluginType::Asdf => Box::new(AsdfPlugin::new(short, path)), - PluginType::Vfox => Box::new(VfoxPlugin::new(short, path)), + PluginType::Asdf => PluginEnum::Asdf(Arc::new(AsdfPlugin::new(short, path))), + PluginType::Vfox => PluginEnum::Vfox(Arc::new(VfoxPlugin::new(short, path))), } } } @@ -52,7 +177,7 @@ pub static VERSION_REGEX: Lazy = Lazy::new(|| { .unwrap() }); -pub fn get(short: &str) -> Result { +pub fn get(short: &str) -> Result { let (name, full) = short.split_once(':').unwrap_or((short, short)); let plugin_type = if let Some(plugin_type) = install_state::list_plugins()?.get(short) { *plugin_type @@ -62,15 +187,13 @@ pub fn get(short: &str) -> Result { Ok(plugin_type.plugin(name.to_string())) } -pub type APlugin = Box; - #[allow(unused_variables)] +#[async_trait] pub trait Plugin: Debug + Send { fn name(&self) -> &str; fn path(&self) -> PathBuf; - fn get_plugin_type(&self) -> PluginType; fn get_remote_url(&self) -> eyre::Result>; - fn set_remote_url(&mut self, url: String) {} + fn set_remote_url(&self, url: String) {} fn current_abbrev_ref(&self) -> eyre::Result>; fn current_sha_short(&self) -> eyre::Result>; fn is_installed(&self) -> bool { @@ -83,16 +206,20 @@ pub trait Plugin: Debug + Send { Ok(()) } - fn ensure_installed(&self, _mpr: &MultiProgressReport, _force: bool) -> eyre::Result<()> { + async fn ensure_installed(&self, _mpr: &MultiProgressReport, _force: bool) -> eyre::Result<()> { Ok(()) } - fn update(&self, _pr: &Box, _gitref: Option) -> eyre::Result<()> { + async fn update( + &self, + _pr: &Box, + _gitref: Option, + ) -> eyre::Result<()> { Ok(()) } - fn uninstall(&self, _pr: &Box) -> eyre::Result<()> { + async fn uninstall(&self, _pr: &Box) -> eyre::Result<()> { Ok(()) } - fn install(&self, _pr: &Box) -> eyre::Result<()> { + async fn install(&self, _pr: &Box) -> eyre::Result<()> { Ok(()) } fn external_commands(&self) -> eyre::Result> { @@ -107,27 +234,27 @@ pub trait Plugin: Debug + Send { } } -impl Ord for APlugin { +impl Ord for PluginEnum { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.name().cmp(other.name()) } } -impl PartialOrd for APlugin { +impl PartialOrd for PluginEnum { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl PartialEq for APlugin { +impl PartialEq for PluginEnum { fn eq(&self, other: &Self) -> bool { self.name() == other.name() } } -impl Eq for APlugin {} +impl Eq for PluginEnum {} -impl Display for APlugin { +impl Display for PluginEnum { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.name()) } diff --git a/src/plugins/vfox_plugin.rs b/src/plugins/vfox_plugin.rs index 0d571fa2b1..ab4166e299 100644 --- a/src/plugins/vfox_plugin.rs +++ b/src/plugins/vfox_plugin.rs @@ -1,11 +1,11 @@ use crate::file::{display_path, remove_all}; use crate::git::{CloneOptions, Git}; -use crate::plugins::{Plugin, PluginType}; +use crate::plugins::Plugin; use crate::result::Result; -use crate::tokio::RUNTIME; use crate::ui::multi_progress_report::MultiProgressReport; use crate::ui::progress_report::SingleReport; use crate::{dirs, registry}; +use async_trait::async_trait; use console::style; use contracts::requires; use eyre::{Context, eyre}; @@ -22,7 +22,7 @@ pub struct VfoxPlugin { pub full: Option, pub plugin_path: PathBuf, pub repo: Mutex, - pub repo_url: Option, + repo_url: Mutex>, } impl VfoxPlugin { @@ -32,7 +32,7 @@ impl VfoxPlugin { Self { name, full: None, - repo_url: None, + repo_url: Mutex::new(None), repo: Mutex::new(repo), plugin_path, } @@ -49,20 +49,20 @@ impl VfoxPlugin { vfox_to_url(self.full.as_ref().unwrap_or(&self.name)) } - pub fn mise_env(&self, opts: &toml::Value) -> Result>> { + pub async fn mise_env(&self, opts: &toml::Value) -> Result>> { let (vfox, _) = self.vfox(); let mut out = indexmap!(); - let results = RUNTIME.block_on(vfox.mise_env(&self.name, opts))?; + let results = vfox.mise_env(&self.name, opts).await?; for env in results { out.insert(env.key, env.value); } Ok(Some(out)) } - pub fn mise_path(&self, opts: &toml::Value) -> Result>> { + pub async fn mise_path(&self, opts: &toml::Value) -> Result>> { let (vfox, _) = self.vfox(); let mut out = vec![]; - let results = RUNTIME.block_on(vfox.mise_path(&self.name, opts))?; + let results = vfox.mise_path(&self.name, opts).await?; for env in results { out.push(env); } @@ -80,6 +80,7 @@ impl VfoxPlugin { } } +#[async_trait] impl Plugin for VfoxPlugin { fn name(&self) -> &str { &self.name @@ -89,17 +90,13 @@ impl Plugin for VfoxPlugin { self.plugin_path.clone() } - fn get_plugin_type(&self) -> PluginType { - PluginType::Vfox - } - fn get_remote_url(&self) -> eyre::Result> { let url = self.repo().get_remote_url(); - Ok(url.or(self.repo_url.clone())) + Ok(url.or(self.repo_url.lock().unwrap().clone())) } - fn set_remote_url(&mut self, url: String) { - self.repo_url = Some(url); + fn set_remote_url(&self, url: String) { + *self.repo_url.lock().unwrap() = Some(url); } fn current_abbrev_ref(&self) -> eyre::Result> { @@ -128,7 +125,7 @@ impl Plugin for VfoxPlugin { .wrap_err("run with --yes to install plugin automatically")) } - fn ensure_installed(&self, mpr: &MultiProgressReport, _force: bool) -> Result<()> { + async fn ensure_installed(&self, mpr: &MultiProgressReport, _force: bool) -> Result<()> { if !self.plugin_path.exists() { let url = self.get_repo_url()?; trace!("Cloning vfox plugin: {url}"); @@ -139,7 +136,7 @@ impl Plugin for VfoxPlugin { Ok(()) } - fn update(&self, pr: &Box, gitref: Option) -> Result<()> { + async fn update(&self, pr: &Box, gitref: Option) -> Result<()> { let plugin_path = self.plugin_path.to_path_buf(); if plugin_path.is_symlink() { warn!( @@ -167,7 +164,7 @@ impl Plugin for VfoxPlugin { Ok(()) } - fn uninstall(&self, pr: &Box) -> Result<()> { + async fn uninstall(&self, pr: &Box) -> Result<()> { if !self.is_installed() { return Ok(()); } @@ -191,13 +188,13 @@ impl Plugin for VfoxPlugin { Ok(()) } - fn install(&self, pr: &Box) -> eyre::Result<()> { + async fn install(&self, pr: &Box) -> eyre::Result<()> { let repository = self.get_repo_url()?; let (repo_url, repo_ref) = Git::split_url_and_ref(repository.as_str()); debug!("vfox_plugin[{}]:install {:?}", self.name, repository); if self.is_installed() { - self.uninstall(pr)?; + self.uninstall(pr).await?; } if regex!(r"^[/~]").is_match(&repo_url) { diff --git a/src/shims.rs b/src/shims.rs index 0efeedaa92..cd7001eb13 100644 --- a/src/shims.rs +++ b/src/shims.rs @@ -15,10 +15,10 @@ use color_eyre::eyre::{Result, bail, eyre}; use eyre::WrapErr; use indoc::formatdoc; use itertools::Itertools; -use rayon::prelude::*; +use tokio::task::JoinSet; // executes as if it was a shim if the command is not "mise", e.g.: "node" -pub fn handle_shim() -> Result<()> { +pub async fn handle_shim() -> Result<()> { // TODO: instead, check if bin is in shims dir let bin_name = *env::MISE_BIN_NAME; if bin_name.starts_with("mise") || cfg!(test) { @@ -27,7 +27,8 @@ pub fn handle_shim() -> Result<()> { logger::init(); let mut args = env::ARGS.read().unwrap().clone(); trace!("shim[{bin_name}] args: {}", args.join(" ")); - args[0] = which_shim(&env::MISE_BIN_NAME)? + args[0] = which_shim(&env::MISE_BIN_NAME) + .await? .to_string_lossy() .to_string(); env::set_var("__MISE_SHIM", "1"); @@ -39,14 +40,15 @@ pub fn handle_shim() -> Result<()> { raw: false, }; time!("shim exec"); - exec.run()?; + exec.run().await?; exit(0); } -fn which_shim(bin_name: &str) -> Result { - let mut ts = ToolsetBuilder::new().build(&Config::get())?; - if let Some((p, tv)) = ts.which(bin_name) { - if let Some(bin) = p.which(&tv, bin_name)? { +async fn which_shim(bin_name: &str) -> Result { + let config = Config::try_get().await?; + let mut ts = ToolsetBuilder::new().build(&config).await?; + if let Some((p, tv)) = ts.which(bin_name).await { + if let Some(bin) = p.which(&tv, bin_name).await? { trace!( "shim[{bin_name}] ToolVersion: {tv} bin: {bin}", bin = display_path(&bin) @@ -55,9 +57,13 @@ fn which_shim(bin_name: &str) -> Result { } } if SETTINGS.not_found_auto_install && console::user_attended() { - for tv in ts.install_missing_bin(bin_name)?.unwrap_or_default() { + for tv in ts + .install_missing_bin(&config, bin_name) + .await? + .unwrap_or_default() + { let p = tv.backend()?; - if let Some(bin) = p.which(&tv, bin_name)? { + if let Some(bin) = p.which(&tv, bin_name).await? { trace!( "shim[{bin_name}] NOT_FOUND ToolVersion: {tv} bin: {bin}", bin = display_path(&bin) @@ -79,11 +85,11 @@ fn which_shim(bin_name: &str) -> Result { return Ok(bin); } } - let tvs = ts.list_rtvs_with_bin(bin_name)?; - err_no_version_set(ts, bin_name, tvs) + let tvs = ts.list_rtvs_with_bin(bin_name).await?; + err_no_version_set(ts, bin_name, tvs).await } -pub fn reshim(ts: &Toolset, force: bool) -> Result<()> { +pub async fn reshim(ts: &Toolset, force: bool) -> Result<()> { let _lock = LockFile::new(&dirs::SHIMS) .with_callback(|l| { trace!("reshim callback {}", l.display()); @@ -97,7 +103,7 @@ pub fn reshim(ts: &Toolset, force: bool) -> Result<()> { } file::create_dir_all(*dirs::SHIMS)?; - let (shims_to_add, shims_to_remove) = get_shim_diffs(&mise_bin, ts)?; + let (shims_to_add, shims_to_remove) = get_shim_diffs(&mise_bin, ts).await?; for shim in shims_to_add { let symlink_path = dirs::SHIMS.join(&shim); @@ -107,21 +113,24 @@ pub fn reshim(ts: &Toolset, force: bool) -> Result<()> { let symlink_path = dirs::SHIMS.join(shim); file::remove_all(&symlink_path)?; } + let mut jset = JoinSet::new(); for plugin in backend::list() { - match dirs::PLUGINS.join(plugin.id()).join("shims").read_dir() { - Ok(files) => { + jset.spawn(async move { + if let Ok(files) = dirs::PLUGINS.join(plugin.id()).join("shims").read_dir() { for bin in files { let bin = bin?; let bin_name = bin.file_name().into_string().unwrap(); let symlink_path = dirs::SHIMS.join(bin_name); - make_shim(&bin.path(), &symlink_path)?; + make_shim(&bin.path(), &symlink_path).await?; } } - Err(_) => { - continue; - } - } + Ok(()) + }); } + jset.join_all() + .await + .into_iter() + .collect::>>()?; Ok(()) } @@ -198,13 +207,13 @@ fn add_shim(mise_bin: &Path, symlink_path: &Path, _shim: &str) -> Result<()> { // get_shim_diffs contrasts the actual shims on disk // with the desired shims specified by the Toolset // and returns a tuple of (missing shims, extra shims) -pub fn get_shim_diffs( +pub async fn get_shim_diffs( mise_bin: impl AsRef, toolset: &Toolset, ) -> Result<(BTreeSet, BTreeSet)> { let mise_bin = mise_bin.as_ref(); let (actual_shims, desired_shims) = - rayon::join(|| get_actual_shims(mise_bin), || get_desired_shims(toolset)); + tokio::join!(get_actual_shims(mise_bin), get_desired_shims(toolset)); let (actual_shims, desired_shims) = (actual_shims?, desired_shims?); let out: (BTreeSet, BTreeSet) = ( desired_shims.difference(&actual_shims).cloned().collect(), @@ -214,11 +223,11 @@ pub fn get_shim_diffs( Ok(out) } -fn get_actual_shims(mise_bin: impl AsRef) -> Result> { +async fn get_actual_shims(mise_bin: impl AsRef) -> Result> { let mise_bin = mise_bin.as_ref(); Ok(list_shims()? - .into_par_iter() + .into_iter() .filter(|bin| { let path = dirs::SHIMS.join(bin); @@ -230,7 +239,6 @@ fn get_actual_shims(mise_bin: impl AsRef) -> Result> { fn list_executables_in_dir(dir: &Path) -> Result> { Ok(dir .read_dir()? - .par_bridge() .map(|bin| { let bin = bin?; // files and symlinks which are executable @@ -251,7 +259,6 @@ fn list_executables_in_dir(dir: &Path) -> Result> { fn list_shims() -> Result> { Ok(dirs::SHIMS .read_dir()? - .par_bridge() .map(|bin| { let bin = bin?; // files and symlinks which are executable or extensionless files (Git Bash/Cygwin) @@ -269,48 +276,44 @@ fn list_shims() -> Result> { .collect()) } -fn get_desired_shims(toolset: &Toolset) -> Result> { - Ok(toolset - .list_installed_versions()? - .into_par_iter() - .flat_map(|(t, tv)| { - let bins = list_tool_bins(t.clone(), &tv).unwrap_or_else(|e| { - warn!("Error listing bin paths for {}: {:#}", tv, e); - Vec::new() - }); - if cfg!(windows) { - bins.into_iter() - .flat_map(|b| { - let p = PathBuf::from(&b); - match SETTINGS.windows_shim_mode.as_ref() { - "hardlink" | "symlink" => { - vec![p.with_extension("exe").to_string_lossy().to_string()] - } - "file" => { - vec![ - p.with_extension("").to_string_lossy().to_string(), - p.with_extension("cmd").to_string_lossy().to_string(), - ] - } - _ => panic!("Unknown shim mode"), - } - }) - .collect() - } else if cfg!(macos) { - // some bins might be uppercased but on mac APFS is case insensitive - bins.into_iter().map(|b| b.to_lowercase()).collect() - } else { - bins - } - }) - .collect()) +async fn get_desired_shims(toolset: &Toolset) -> Result> { + let mut shims = HashSet::new(); + for (t, tv) in toolset.list_installed_versions().await? { + let bins = list_tool_bins(t.clone(), &tv).await.unwrap_or_else(|e| { + warn!("Error listing bin paths for {}: {:#}", tv, e); + Vec::new() + }); + if cfg!(windows) { + shims.extend(bins.into_iter().flat_map(|b| { + let p = PathBuf::from(&b); + match SETTINGS.windows_shim_mode.as_ref() { + "hardlink" | "symlink" => { + vec![p.with_extension("exe").to_string_lossy().to_string()] + } + "file" => { + vec![ + p.with_extension("").to_string_lossy().to_string(), + p.with_extension("cmd").to_string_lossy().to_string(), + ] + } + _ => panic!("Unknown shim mode"), + } + })); + } else if cfg!(macos) { + // some bins might be uppercased but on mac APFS is case insensitive + shims.extend(bins.into_iter().map(|b| b.to_lowercase())); + } else { + shims.extend(bins); + } + } + Ok(shims) } // lists all the paths to bins in a tv that shims will be needed for -fn list_tool_bins(t: Arc, tv: &ToolVersion) -> Result> { - Ok(t.list_bin_paths(tv)? +async fn list_tool_bins(t: Arc, tv: &ToolVersion) -> Result> { + Ok(t.list_bin_paths(tv) + .await? .into_iter() - .par_bridge() .filter(|p| p.parent().is_some()) .filter(|path| path.exists()) .map(|dir| list_executables_in_dir(&dir)) @@ -320,11 +323,11 @@ fn list_tool_bins(t: Arc, tv: &ToolVersion) -> Result> .collect()) } -fn make_shim(target: &Path, shim: &Path) -> Result<()> { +async fn make_shim(target: &Path, shim: &Path) -> Result<()> { if shim.exists() { - file::remove_file(shim)?; + file::remove_file_async(shim).await?; } - file::write( + file::write_async( shim, formatdoc! {r#" #!/bin/sh @@ -335,8 +338,9 @@ fn make_shim(target: &Path, shim: &Path) -> Result<()> { data_dir = dirs::DATA.display(), fake_asdf_dir = fake_asdf::setup()?.display(), target = target.display()}, - )?; - file::make_executable(shim)?; + ) + .await?; + file::make_executable_async(shim).await?; trace!( "shim created from {} to {}", target.display(), @@ -345,7 +349,7 @@ fn make_shim(target: &Path, shim: &Path) -> Result<()> { Ok(()) } -fn err_no_version_set(ts: Toolset, bin_name: &str, tvs: Vec) -> Result { +async fn err_no_version_set(ts: Toolset, bin_name: &str, tvs: Vec) -> Result { if tvs.is_empty() { bail!( "{bin_name} is not a valid shim. This likely means you uninstalled a tool and the shim does not point to anything. Run `mise use ` to reinstall the tool." @@ -354,6 +358,7 @@ fn err_no_version_set(ts: Toolset, bin_name: &str, tvs: Vec) -> Res let missing_plugins = tvs.iter().map(|tv| tv.ba()).collect::>(); let mut missing_tools = ts .list_missing_versions() + .await .into_iter() .filter(|t| missing_plugins.contains(t.ba())) .collect_vec(); diff --git a/src/sops.rs b/src/sops.rs index 8703b87780..6c418a1f5a 100644 --- a/src/sops.rs +++ b/src/sops.rs @@ -7,48 +7,55 @@ use rops::cryptography::cipher::AES256GCM; use rops::cryptography::hasher::SHA512; use rops::file::RopsFile; use rops::file::state::EncryptedFile; -use std::sync::{Mutex, OnceLock}; +use tokio::sync::{Mutex, OnceCell}; -pub fn decrypt(input: &str, mut parse_template: PT, format: &str) -> result::Result +pub async fn decrypt( + config: &Config, + input: &str, + mut parse_template: PT, + format: &str, +) -> result::Result where PT: FnMut(String) -> result::Result, F: rops::file::format::FileFormat, { - static AGE_KEY: OnceLock> = OnceLock::new(); - static MUTEX: Mutex<()> = Mutex::new(()); - let age = AGE_KEY.get_or_init(|| { - let p = SETTINGS - .sops - .age_key_file - .clone() - .unwrap_or(dirs::CONFIG.join("age.txt")); - let p = replace_path(match parse_template(p.to_string_lossy().to_string()) { - Ok(p) => p, - Err(e) => { - warn!("failed to parse sops age key file: {}", e); - return None; - } - }); - if let Some(age_key) = &SETTINGS.sops.age_key { - if !age_key.is_empty() { - return Some(age_key.clone()); + static AGE_KEY: OnceCell> = OnceCell::const_new(); + static MUTEX: Mutex<()> = Mutex::const_new(()); + let age = AGE_KEY + .get_or_init(async || { + let p = SETTINGS + .sops + .age_key_file + .clone() + .unwrap_or(dirs::CONFIG.join("age.txt")); + let p = replace_path(match parse_template(p.to_string_lossy().to_string()) { + Ok(p) => p, + Err(e) => { + warn!("failed to parse sops age key file: {}", e); + return None; + } + }); + if let Some(age_key) = &SETTINGS.sops.age_key { + if !age_key.is_empty() { + return Some(age_key.clone()); + } } - } - if p.exists() { - if let Ok(raw) = file::read_to_string(p) { - let key = raw - .trim() - .lines() - .filter(|l| !l.starts_with('#')) - .collect::(); - if !key.trim().is_empty() { - return Some(key); + if p.exists() { + if let Ok(raw) = file::read_to_string(p) { + let key = raw + .trim() + .lines() + .filter(|l| !l.starts_with('#')) + .collect::(); + if !key.trim().is_empty() { + return Some(key); + } } } - } - None - }); - let _lock = MUTEX.lock().unwrap(); // prevent multiple threads from using the same age key + None + }) + .await; + let _lock = MUTEX.lock().await; // prevent multiple threads from using the same age key let age_env_key = if SETTINGS.sops.rops { "ROPS_AGE" } else { @@ -66,16 +73,17 @@ where .wrap_err("failed to decrypt sops file")? .to_string() } else { - let config = Config::get(); let mut ts = config .get_tool_request_set() + .await .cloned() .unwrap_or_default() .filter_by_tool(["sops".into()].into()) .into_toolset(); - ts.resolve()?; + Box::pin(ts.resolve()).await?; let sops = ts .which_bin("sops") + .await .map(|s| s.to_string_lossy().to_string()) .unwrap_or("sops".into()); // TODO: this obviously won't work on windows diff --git a/src/task/deps.rs b/src/task/deps.rs index d4a9723eef..ebf5a7f81c 100644 --- a/src/task/deps.rs +++ b/src/task/deps.rs @@ -1,17 +1,17 @@ use crate::cli::run::resolve_depends; use crate::task::Task; -use crossbeam_channel as channel; use itertools::Itertools; use petgraph::Direction; use petgraph::graph::DiGraph; use std::collections::{HashMap, HashSet}; +use tokio::sync::mpsc; #[derive(Debug, Clone)] pub struct Deps { pub graph: DiGraph, sent: HashSet<(String, Vec)>, // tasks+args that have already started so should not run again removed: HashSet<(String, Vec)>, // tasks+args that have already finished to track if we are in an infinitve loop - tx: channel::Sender>, + tx: mpsc::UnboundedSender>, } fn task_key(task: &Task) -> (String, Vec) { @@ -20,7 +20,7 @@ fn task_key(task: &Task) -> (String, Vec) { /// manages a dependency graph of tasks so `mise run` knows what to run next impl Deps { - pub fn new(tasks: Vec) -> eyre::Result { + pub async fn new(tasks: Vec) -> eyre::Result { let mut graph = DiGraph::new(); let mut indexes = HashMap::new(); let mut stack = vec![]; @@ -38,14 +38,14 @@ impl Deps { stack.push(t.clone()); add_idx(t, &mut graph); } - let all_tasks_to_run = resolve_depends(tasks)?; + let all_tasks_to_run = resolve_depends(tasks).await?; while let Some(a) = stack.pop() { if seen.contains(&a) { // prevent infinite loop continue; } let a_idx = add_idx(&a, &mut graph); - let (pre, post) = a.resolve_depends(&all_tasks_to_run)?; + let (pre, post) = a.resolve_depends(&all_tasks_to_run).await?; for b in pre { let b_idx = add_idx(&b, &mut graph); graph.update_edge(a_idx, b_idx, ()); @@ -58,7 +58,7 @@ impl Deps { } seen.insert(a); } - let (tx, _) = channel::unbounded(); + let (tx, _) = mpsc::unbounded_channel(); let sent = HashSet::new(); let removed = HashSet::new(); Ok(Self { @@ -100,8 +100,8 @@ impl Deps { } /// listened to by `mise run` which gets a stream of tasks to run - pub fn subscribe(&mut self) -> channel::Receiver> { - let (tx, rx) = channel::unbounded(); + pub fn subscribe(&mut self) -> mpsc::UnboundedReceiver> { + let (tx, rx) = mpsc::unbounded_channel(); self.tx = tx; self.emit_leaves(); rx diff --git a/src/task/mod.rs b/src/task/mod.rs index 15170983a8..718850f989 100644 --- a/src/task/mod.rs +++ b/src/task/mod.rs @@ -1,5 +1,5 @@ -use crate::config::Config; use crate::config::config_file::toml::{TomlParser, deserialize_arr}; +use crate::config::{self, Config}; use crate::task::task_script_parser::{TaskScriptParser, has_any_args_defined}; use crate::tera::get_tera; use crate::ui::tree::TreeItem; @@ -43,6 +43,8 @@ use task_sources::TaskOutputs; pub struct Task { #[serde(skip)] pub name: String, + #[serde(skip)] + pub display_name: String, #[serde(default)] pub description: String, #[serde(default, rename = "alias", deserialize_with = "deserialize_arr")] @@ -148,7 +150,7 @@ impl Task { }) } - pub fn from_path(path: &Path, prefix: &Path, config_root: &Path) -> Result { + pub async fn from_path(path: &Path, prefix: &Path, config_root: &Path) -> Result { let mut task = Task::new(path, prefix, config_root)?; let info = file::read_to_string(path)? .lines() @@ -201,24 +203,19 @@ impl Task { .collect() }) .unwrap_or_default(); - task.render(config_root)?; + task.render(config_root).await?; Ok(task) } /// prints the task name without an extension - pub fn display_name(&self) -> String { + pub fn display_name(&self, all_tasks: &BTreeMap) -> String { let display_name = self .name .rsplitn(2, '.') .last() .unwrap_or_default() .to_string(); - let config = Config::get(); - if config - .tasks() - .map(|t| t.contains_key(&display_name)) - .unwrap_or_default() - { + if all_tasks.contains_key(&display_name) { // this means another task has the name without an extension so use the full name self.name.clone() } else { @@ -235,11 +232,11 @@ impl Task { || self.aliases.contains(&pat.to_string()) } - pub fn task_dir() -> PathBuf { - let config = Config::get(); + pub async fn task_dir() -> PathBuf { + let config = Config::get().await; let cwd = dirs::CWD.clone().unwrap_or_default(); let project_root = config.project_root.clone().unwrap_or(cwd); - for dir in config.task_includes_for_dir(&project_root) { + for dir in config::task_includes_for_dir(&project_root, &config.config_files) { if dir.is_dir() && project_root.join(&dir).exists() { return project_root.join(dir); } @@ -253,7 +250,7 @@ impl Task { } pub fn prefix(&self) -> String { - format!("[{}]", self.display_name()) + format!("[{}]", self.display_name) } pub fn run(&self) -> &Vec { @@ -264,19 +261,17 @@ impl Task { } } - pub fn all_depends(&self) -> Result> { - let config = Config::get(); - let tasks = config.tasks_with_aliases()?; + pub fn all_depends(&self, tasks: &BTreeMap) -> Result> { let mut depends: Vec = self .depends .iter() .chain(self.depends_post.iter()) - .map(|td| match_tasks(&tasks, td)) + .map(|td| match_tasks(tasks, td)) .flatten_ok() .filter_ok(|t| t.name != self.name) .collect::>>()?; for dep in depends.clone() { - let mut extra = dep.all_depends()?; + let mut extra = dep.all_depends(tasks)?; extra.retain(|t| t.name != self.name); // prevent depending on ourself depends.extend(extra); } @@ -284,10 +279,10 @@ impl Task { Ok(depends) } - pub fn resolve_depends(&self, tasks_to_run: &[Task]) -> Result<(Vec, Vec)> { - let config = Config::get(); + pub async fn resolve_depends(&self, tasks_to_run: &[Task]) -> Result<(Vec, Vec)> { + let config = Config::get().await; let tasks_to_run: HashSet<&Task> = tasks_to_run.iter().collect(); - let tasks = config.tasks_with_aliases()?; + let tasks = config.tasks_with_aliases().await?; let depends = self .depends .iter() @@ -315,7 +310,7 @@ impl Task { Ok((depends, depends_post)) } - pub fn parse_usage_spec( + pub async fn parse_usage_spec( &self, cwd: Option, env: &EnvMap, @@ -331,16 +326,17 @@ impl Task { .unwrap_or_default(); (spec, vec![]) } else { - let (scripts, spec) = - TaskScriptParser::new(cwd).parse_run_scripts(self, self.run(), env)?; + let (scripts, spec) = TaskScriptParser::new(cwd) + .parse_run_scripts(self, self.run(), env) + .await?; (spec, scripts) }; - spec.name = self.display_name(); - spec.bin = self.display_name(); + spec.name = self.display_name.clone(); + spec.bin = self.display_name.clone(); if spec.cmd.help.is_none() { spec.cmd.help = Some(self.description.clone()); } - spec.cmd.name = self.display_name(); + spec.cmd.name = self.display_name.clone(); spec.cmd.aliases = self.aliases.clone(); if spec.cmd.before_help.is_none() && spec.cmd.before_help_long.is_none() @@ -353,21 +349,17 @@ impl Task { Ok((spec, scripts)) } - pub fn render_run_scripts_with_args( + pub async fn render_run_scripts_with_args( &self, cwd: Option, args: &[String], env: &EnvMap, ) -> Result)>> { - let (spec, scripts) = self.parse_usage_spec(cwd.clone(), env)?; + let (spec, scripts) = self.parse_usage_spec(cwd.clone(), env).await?; if has_any_args_defined(&spec) { - let scripts = TaskScriptParser::new(cwd).parse_run_scripts_with_args( - self, - self.run(), - env, - args, - &spec, - )?; + let scripts = TaskScriptParser::new(cwd) + .parse_run_scripts_with_args(self, self.run(), env, args, &spec) + .await?; Ok(scripts.into_iter().map(|s| (s, vec![])).collect()) } else { Ok(scripts @@ -384,9 +376,9 @@ impl Task { } } - pub fn render_markdown(&self, ts: &Toolset, dir: &Path) -> Result { - let env = self.render_env(ts)?; - let (spec, _) = self.parse_usage_spec(Some(dir.to_path_buf()), &env)?; + pub async fn render_markdown(&self, ts: &Toolset, dir: &Path) -> Result { + let env = self.render_env(ts).await?; + let (spec, _) = self.parse_usage_spec(Some(dir.to_path_buf()), &env).await?; let ctx = usage::docs::markdown::MarkdownRenderer::new(spec) .with_replace_pre_with_code_fences(true) .with_header_level(2); @@ -404,12 +396,7 @@ impl Task { Color::Red, ] }); - let idx = self - .display_name() - .chars() - .map(|c| c as usize) - .sum::() - % COLORS.len(); + let idx = self.display_name.chars().map(|c| c as usize).sum::() % COLORS.len(); let prefix = style::ereset() + &style::estyle(self.prefix()).fg(COLORS[idx]).to_string(); if *env::MISE_TASK_LEVEL > 0 { format!( @@ -421,8 +408,8 @@ impl Task { } } - pub fn dir(&self) -> Result> { - let config = Config::get(); + pub async fn dir(&self) -> Result> { + let config = Config::get().await; if let Some(dir) = self.dir.clone().or_else(|| { self.cf(&config) .as_ref() @@ -430,7 +417,7 @@ impl Task { }) { let config_root = self.config_root.clone().unwrap_or_default(); let mut tera = get_tera(Some(&config_root)); - let tera_ctx = self.tera_ctx()?; + let tera_ctx = self.tera_ctx().await?; let dir = tera.render_str(&dir, &tera_ctx)?; let dir = file::replace_path(&dir); if dir.is_absolute() { @@ -445,15 +432,15 @@ impl Task { } } - pub fn tera_ctx(&self) -> Result { - let config = Config::get(); - let ts = config.get_toolset()?; - let mut tera_ctx = ts.tera_ctx()?.clone(); + pub async fn tera_ctx(&self) -> Result { + let config = Config::get().await; + let ts = config.get_toolset().await?; + let mut tera_ctx = ts.tera_ctx().await?.clone(); tera_ctx.insert("config_root", &self.config_root); Ok(tera_ctx) } - pub fn cf<'a>(&self, config: &'a Config) -> Option<&'a Box> { + pub fn cf<'a>(&self, config: &'a Config) -> Option<&'a Arc> { config.config_files.get(&self.config_source) } @@ -472,9 +459,9 @@ impl Task { }) } - pub fn render(&mut self, config_root: &Path) -> Result<()> { + pub async fn render(&mut self, config_root: &Path) -> Result<()> { let mut tera = get_tera(Some(config_root)); - let tera_ctx = self.tera_ctx()?; + let tera_ctx = self.tera_ctx().await?; for a in &mut self.aliases { *a = tera.render_str(a, &tera_ctx)?; } @@ -508,11 +495,11 @@ impl Task { self.name.replace(':', path::MAIN_SEPARATOR_STR).into() } - pub fn render_env(&self, ts: &Toolset) -> Result { - let config = Config::get(); + pub async fn render_env(&self, ts: &Toolset) -> Result { + let config = Config::try_get().await?; let mut tera = get_tera(self.config_root.as_deref()); - let mut tera_ctx = ts.tera_ctx()?.clone(); - let mut env = ts.full_env(&config)?; + let mut tera_ctx = ts.tera_ctx().await?.clone(); + let mut env = ts.full_env(&config).await?; if let Some(root) = &config.project_root { tera_ctx.insert("config_root", &root); } @@ -590,6 +577,7 @@ impl Default for Task { fn default() -> Self { Task { name: "".to_string(), + display_name: "".to_string(), description: "".to_string(), aliases: vec![], config_source: PathBuf::new(), @@ -670,7 +658,7 @@ impl TreeItem for (&Graph, NodeIndex) { fn write_self(&self) -> std::io::Result<()> { if let Some(w) = self.0.node_weight(self.1) { - miseprint!("{}", w.display_name())?; + miseprint!("{}", w.display_name)?; } Ok(()) } @@ -725,8 +713,8 @@ mod tests { use super::name_from_path; - #[test] - fn test_from_path() { + #[tokio::test] + async fn test_from_path() { let test_cases = [(".mise/tasks/filetask", "filetask", vec!["ft"])]; for (path, name, aliases) in test_cases { @@ -735,6 +723,7 @@ mod tests { Path::new(".mise/tasks"), Path::new(dirs::CWD.as_ref().unwrap()), ) + .await .unwrap(); assert_eq!(t.name, name); assert_eq!(t.aliases, aliases); diff --git a/src/task/task_file_providers/local_task.rs b/src/task/task_file_providers/local_task.rs index 59dcba6dc0..09c66a2a45 100644 --- a/src/task/task_file_providers/local_task.rs +++ b/src/task/task_file_providers/local_task.rs @@ -1,10 +1,15 @@ use std::path::{Path, PathBuf}; +use async_trait::async_trait; + +use crate::Result; + use super::TaskFileProvider; #[derive(Debug)] pub struct LocalTask; +#[async_trait] impl TaskFileProvider for LocalTask { fn is_match(&self, file: &str) -> bool { let path = Path::new(file); @@ -12,7 +17,7 @@ impl TaskFileProvider for LocalTask { path.is_relative() || path.is_absolute() } - fn get_local_path(&self, file: &str) -> Result> { + async fn get_local_path(&self, file: &str) -> Result { Ok(PathBuf::from(file)) } } @@ -32,19 +37,19 @@ mod tests { assert!(provider.is_match("../test.txt")); } - #[test] - fn test_get_local_path() { + #[tokio::test] + async fn test_get_local_path() { let provider = LocalTask; assert_eq!( - provider.get_local_path("/test.txt").unwrap(), + provider.get_local_path("/test.txt").await.unwrap(), PathBuf::from("/test.txt") ); assert_eq!( - provider.get_local_path("./test.txt").unwrap(), + provider.get_local_path("./test.txt").await.unwrap(), PathBuf::from("./test.txt") ); assert_eq!( - provider.get_local_path("../test.txt").unwrap(), + provider.get_local_path("../test.txt").await.unwrap(), PathBuf::from("../test.txt") ); } diff --git a/src/task/task_file_providers/mod.rs b/src/task/task_file_providers/mod.rs index 86ed6e070e..e33c529cea 100644 --- a/src/task/task_file_providers/mod.rs +++ b/src/task/task_file_providers/mod.rs @@ -3,14 +3,16 @@ use std::{fmt::Debug, path::PathBuf}; mod local_task; mod remote_task_git; mod remote_task_http; - +use crate::Result; +use async_trait::async_trait; use local_task::LocalTask; use remote_task_git::RemoteTaskGitBuilder; use remote_task_http::RemoteTaskHttpBuilder; +#[async_trait] pub trait TaskFileProvider: Debug { fn is_match(&self, file: &str) -> bool; - fn get_local_path(&self, file: &str) -> Result>; + async fn get_local_path(&self, file: &str) -> Result; } pub struct TaskFileProvidersBuilder { diff --git a/src/task/task_file_providers/remote_task_git.rs b/src/task/task_file_providers/remote_task_git.rs index 122f8b2529..00c8d8e043 100644 --- a/src/task/task_file_providers/remote_task_git.rs +++ b/src/task/task_file_providers/remote_task_git.rs @@ -1,5 +1,8 @@ +use crate::Result; use std::path::PathBuf; +use async_trait::async_trait; +use eyre::eyre; use regex::Regex; use crate::{ @@ -80,11 +83,11 @@ impl RemoteTaskGit { self.detect_https(file).unwrap() } - fn detect_ssh(&self, file: &str) -> Result> { + fn detect_ssh(&self, file: &str) -> Result { let re = Regex::new(r"^git::(?Pssh://((?P[^@]+)@)(?P[^/]+)/(?P.+)\.git)//(?P[^?]+)(\?ref=(?P[^?]+))?$").unwrap(); if !re.is_match(file) { - return Err("Invalid SSH URL".into()); + return Err(eyre!("Invalid SSH URL")); } let captures = re.captures(file).unwrap(); @@ -98,11 +101,11 @@ impl RemoteTaskGit { Ok(GitRepoStructure::new(url_without_path, path, branch)) } - fn detect_https(&self, file: &str) -> Result> { + fn detect_https(&self, file: &str) -> Result { let re = Regex::new(r"^git::(?Phttps://(?P[^/]+)/(?P.+)\.git)//(?P[^?]+)(\?ref=(?P[^?]+))?$").unwrap(); if !re.is_match(file) { - return Err("Invalid HTTPS URL".into()); + return Err(eyre!("Invalid HTTPS URL")); } let captures = re.captures(file).unwrap(); @@ -117,6 +120,7 @@ impl RemoteTaskGit { } } +#[async_trait] impl TaskFileProvider for RemoteTaskGit { fn is_match(&self, file: &str) -> bool { if self.detect_ssh(file).is_ok() { @@ -130,7 +134,7 @@ impl TaskFileProvider for RemoteTaskGit { false } - fn get_local_path(&self, file: &str) -> Result> { + async fn get_local_path(&self, file: &str) -> Result { let repo_structure = self.get_repo_structure(file); let cache_key = self.get_cache_key(&repo_structure); let destination = self.storage_path.join(&cache_key); diff --git a/src/task/task_file_providers/remote_task_http.rs b/src/task/task_file_providers/remote_task_http.rs index 5c2394127d..3c38887214 100644 --- a/src/task/task_file_providers/remote_task_http.rs +++ b/src/task/task_file_providers/remote_task_http.rs @@ -1,6 +1,8 @@ use std::path::PathBuf; -use crate::{dirs, env, file, hash, http::HTTP}; +use async_trait::async_trait; + +use crate::{Result, dirs, env, file, hash, http::HTTP}; use super::TaskFileProvider; @@ -45,18 +47,15 @@ impl RemoteTaskHttp { hash::hash_sha256_to_str(file) } - fn download_file( - &self, - file: &str, - destination: &PathBuf, - ) -> Result<(), Box> { + async fn download_file(&self, file: &str, destination: &PathBuf) -> Result<()> { trace!("Downloading file: {}", file); - HTTP.download_file(file, destination, None)?; + HTTP.download_file(file, destination, None).await?; file::make_executable(destination)?; Ok(()) } } +#[async_trait] impl TaskFileProvider for RemoteTaskHttp { fn is_match(&self, file: &str) -> bool { let url = url::Url::parse(file); @@ -71,7 +70,7 @@ impl TaskFileProvider for RemoteTaskHttp { }) } - fn get_local_path(&self, file: &str) -> Result> { + async fn get_local_path(&self, file: &str) -> Result { let cache_key = self.get_cache_key(file); let destination = self.storage_path.join(&cache_key); @@ -93,7 +92,7 @@ impl TaskFileProvider for RemoteTaskHttp { } } - self.download_file(file, &destination)?; + self.download_file(file, &destination).await?; Ok(destination) } } @@ -103,8 +102,8 @@ mod tests { use super::*; - #[test] - fn test_is_match() { + #[tokio::test] + async fn test_is_match() { let provider = RemoteTaskHttpBuilder::new().build(); // Positive cases @@ -120,14 +119,14 @@ mod tests { assert!(!provider.is_match("https://myhost.com/")); } - #[test] - fn test_http_remote_task_get_local_path_without_cache() { + #[tokio::test] + async fn test_http_remote_task_get_local_path_without_cache() { let paths = vec![ "/myfile.py", "/subpath/myfile.sh", "/myfile.sh?query=1&sdfsdf=2", ]; - let mut server = mockito::Server::new(); + let mut server = mockito::Server::new_async().await; for request_path in paths { let mocked_server: mockito::Mock = server @@ -135,14 +134,15 @@ mod tests { .with_status(200) .with_body("Random content") .expect(2) - .create(); + .create_async() + .await; let provider = RemoteTaskHttpBuilder::new().build(); let request_url = format!("{}{}", server.url(), request_path); let cache_key = provider.get_cache_key(&request_url); for _ in 0..2 { - let local_path = provider.get_local_path(&request_url).unwrap(); + let local_path = provider.get_local_path(&request_url).await.unwrap(); assert!(local_path.exists()); assert!(local_path.is_file()); assert!(local_path.ends_with(&cache_key)); @@ -152,14 +152,14 @@ mod tests { } } - #[test] - fn test_http_remote_task_get_local_path_with_cache() { + #[tokio::test] + async fn test_http_remote_task_get_local_path_with_cache() { let paths = vec![ "/myfile.py", "/subpath/myfile.sh", "/myfile.sh?query=1&sdfsdf=2", ]; - let mut server = mockito::Server::new(); + let mut server = mockito::Server::new_async().await; for request_path in paths { let mocked_server = server @@ -167,14 +167,15 @@ mod tests { .with_status(200) .with_body("Random content") .expect(1) - .create(); + .create_async() + .await; let provider = RemoteTaskHttpBuilder::new().with_cache(true).build(); let request_url = format!("{}{}", server.url(), request_path); let cache_key = provider.get_cache_key(&request_url); for _ in 0..2 { - let path = provider.get_local_path(&request_url).unwrap(); + let path = provider.get_local_path(&request_url).await.unwrap(); assert!(path.exists()); assert!(path.is_file()); assert!(path.ends_with(&cache_key)); diff --git a/src/task/task_script_parser.rs b/src/task/task_script_parser.rs index 1c66f4654d..b8a3bc1e98 100644 --- a/src/task/task_script_parser.rs +++ b/src/task/task_script_parser.rs @@ -24,7 +24,7 @@ impl TaskScriptParser { get_tera(self.dir.as_deref()) } - pub fn parse_run_scripts( + pub async fn parse_run_scripts( &self, task: &Task, scripts: &[String], @@ -273,7 +273,7 @@ impl TaskScriptParser { } } }); - let mut tera_ctx = task.tera_ctx()?; + let mut tera_ctx = task.tera_ctx().await?; tera_ctx.insert("env", &env); let scripts = scripts .iter() @@ -306,7 +306,7 @@ impl TaskScriptParser { Ok((scripts, spec)) } - pub fn parse_run_scripts_with_args( + pub async fn parse_run_scripts_with_args( &self, task: &Task, scripts: &[String], @@ -397,7 +397,7 @@ impl TaskScriptParser { }; tera.register_function("option", flag_func.clone()); tera.register_function("flag", flag_func); - let mut tera_ctx = task.tera_ctx()?; + let mut tera_ctx = task.tera_ctx().await?; tera_ctx.insert("env", &env); out.push( tera.render_str(script, &tera_ctx) @@ -426,13 +426,14 @@ fn shell_from_shebang(script: &str) -> Option> { mod tests { use super::*; - #[test] - fn test_task_parse_arg() { + #[tokio::test] + async fn test_task_parse_arg() { let task = Task::default(); let parser = TaskScriptParser::new(None); let scripts = vec!["echo {{ arg(i=0, name='foo') }}".to_string()]; let (parsed_scripts, spec) = parser .parse_run_scripts(&task, &scripts, &Default::default()) + .await .unwrap(); assert_eq!(parsed_scripts, vec!["echo "]); let arg0 = spec.cmd.args.first().unwrap(); @@ -446,12 +447,13 @@ mod tests { &["abc".to_string()], &spec, ) + .await .unwrap(); assert_eq!(parsed_scripts, vec!["echo abc"]); } - #[test] - fn test_task_parse_multi_use_arg() { + #[tokio::test] + async fn test_task_parse_multi_use_arg() { let task = Task::default(); let parser = TaskScriptParser::new(None); let scripts = vec![ @@ -460,6 +462,7 @@ mod tests { ]; let (parsed_scripts, spec) = parser .parse_run_scripts(&task, &scripts, &Default::default()) + .await .unwrap(); assert_eq!(parsed_scripts, vec!["echo ; echo ; echo "]); let arg0 = spec.cmd.args.first().unwrap(); @@ -476,17 +479,19 @@ mod tests { &["abc".to_string(), "def".to_string()], &spec, ) + .await .unwrap(); assert_eq!(parsed_scripts, vec!["echo abc; echo def; echo abc"]); } - #[test] - fn test_task_parse_arg_var() { + #[tokio::test] + async fn test_task_parse_arg_var() { let task = Task::default(); let parser = TaskScriptParser::new(None); let scripts = vec!["echo {{ arg(var=true) }}".to_string()]; let (parsed_scripts, spec) = parser .parse_run_scripts(&task, &scripts, &Default::default()) + .await .unwrap(); assert_eq!(parsed_scripts, vec!["echo "]); let arg0 = spec.cmd.args.first().unwrap(); @@ -500,17 +505,19 @@ mod tests { &["abc".to_string(), "def".to_string()], &spec, ) + .await .unwrap(); assert_eq!(parsed_scripts, vec!["echo abc def"]); } - #[test] - fn test_task_parse_flag() { + #[tokio::test] + async fn test_task_parse_flag() { let task = Task::default(); let parser = TaskScriptParser::new(None); let scripts = vec!["echo {{ flag(name='foo') }}".to_string()]; let (parsed_scripts, spec) = parser .parse_run_scripts(&task, &scripts, &Default::default()) + .await .unwrap(); assert_eq!(parsed_scripts, vec!["echo "]); let flag = spec.cmd.flags.iter().find(|f| &f.name == "foo").unwrap(); @@ -524,27 +531,31 @@ mod tests { &["--foo".to_string()], &spec, ) + .await .unwrap(); assert_eq!(parsed_scripts, vec!["echo true"]); let scripts = vec!["echo {{ flag(name='foo') }}".to_string()]; let (parsed_scripts, spec) = parser .parse_run_scripts(&task, &scripts, &Default::default()) + .await .unwrap(); assert_eq!(parsed_scripts, vec!["echo "]); let parsed_scripts = parser .parse_run_scripts_with_args(&task, &scripts, &Default::default(), &[], &spec) + .await .unwrap(); assert_eq!(parsed_scripts, vec!["echo false"]); } - #[test] - fn test_task_parse_option() { + #[tokio::test] + async fn test_task_parse_option() { let task = Task::default(); let parser = TaskScriptParser::new(None); let scripts = vec!["echo {{ option(name='foo') }}".to_string()]; let (parsed_scripts, spec) = parser .parse_run_scripts(&task, &scripts, &Default::default()) + .await .unwrap(); assert_eq!(parsed_scripts, vec!["echo "]); let option = spec.cmd.flags.iter().find(|f| &f.name == "foo").unwrap(); @@ -558,24 +569,29 @@ mod tests { &["--foo".to_string(), "abc".to_string()], &spec, ) + .await .unwrap(); assert_eq!(parsed_scripts, vec!["echo abc"]); } - #[test] - fn test_task_nested_template() { + #[tokio::test] + async fn test_task_nested_template() { let task = Task::default(); let parser = TaskScriptParser::new(None); let scripts = vec!["echo {% if flag(name=env.FLAG_NAME) == 'true' %}TRUE{% endif %}".to_string()]; let env = EnvMap::from_iter(vec![("FLAG_NAME".to_string(), "foo".to_string())]); - let (parsed_scripts, spec) = parser.parse_run_scripts(&task, &scripts, &env).unwrap(); + let (parsed_scripts, spec) = parser + .parse_run_scripts(&task, &scripts, &env) + .await + .unwrap(); assert_eq!(parsed_scripts, vec!["echo "]); let flag = spec.cmd.flags.first().unwrap(); assert_eq!(&flag.name, "foo"); let parsed_scripts = parser .parse_run_scripts_with_args(&task, &scripts, &env, &["--foo".to_string()], &spec) + .await .unwrap(); assert_eq!(parsed_scripts, vec!["echo TRUE"]); } diff --git a/src/timeout.rs b/src/timeout.rs index a0f2d3c276..c1e7606f73 100644 --- a/src/timeout.rs +++ b/src/timeout.rs @@ -19,3 +19,16 @@ where rx.recv_timeout(timeout).context("timed out") })? } + +pub async fn run_with_timeout_async(f: F, timeout: Duration) -> Result +where + Fut: Future> + Send, + T: Send, + F: FnOnce() -> Fut, +{ + match tokio::time::timeout(timeout, f()).await { + Ok(Ok(output)) => Ok(output), + Ok(Err(e)) => Err(e), + Err(_) => Err(eyre::eyre!("timed out")), + } +} diff --git a/src/tokio.rs b/src/tokio.rs deleted file mode 100644 index 4e0d136c46..0000000000 --- a/src/tokio.rs +++ /dev/null @@ -1,10 +0,0 @@ -use std::sync::LazyLock as Lazy; - -pub static RUNTIME: Lazy = Lazy::new(|| { - tokio::runtime::Builder::new_multi_thread() - .worker_threads(4) - .enable_io() - .enable_time() - .build() - .unwrap() -}); diff --git a/src/toolset/builder.rs b/src/toolset/builder.rs index ac05def34a..c7c8839309 100644 --- a/src/toolset/builder.rs +++ b/src/toolset/builder.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use eyre::Result; use itertools::Itertools; @@ -35,19 +37,27 @@ impl ToolsetBuilder { self } - pub fn build(self, config: &Config) -> Result { + pub async fn build(self, config: &Config) -> Result { let mut toolset = Toolset { ..Default::default() }; - self.load_config_files(config, &mut toolset)?; - self.load_runtime_env(&mut toolset, env::vars().collect())?; - self.load_runtime_args(&mut toolset)?; - if let Err(err) = toolset.resolve() { - if Error::is_argument_err(&err) { - return Err(err); + measure!("toolset_builder::build::load_config_files", { + self.load_config_files(config, &mut toolset)?; + }); + measure!("toolset_builder::build::load_runtime_env", { + self.load_runtime_env(&mut toolset, env::vars().collect())?; + }); + measure!("toolset_builder::build::load_runtime_args", { + self.load_runtime_args(&mut toolset)?; + }); + measure!("toolset_builder::build::resolve", { + if let Err(err) = toolset.resolve().await { + if Error::is_argument_err(&err) { + return Err(err); + } + warn!("failed to resolve toolset: {err}"); } - warn!("failed to resolve toolset: {err}"); - } + }); time!("toolset::builder::build"); Ok(toolset) @@ -74,11 +84,11 @@ impl ToolsetBuilder { // ignore MISE_INSTALL_VERSION continue; } - let fa: BackendArg = plugin_name.as_str().into(); + let ba: Arc = Arc::new(plugin_name.as_str().into()); let source = ToolSource::Environment(k, v.clone()); let mut env_ts = Toolset::new(source.clone()); for v in v.split_whitespace() { - let tvr = ToolRequest::new(fa.clone(), v, source.clone())?; + let tvr = ToolRequest::new(ba.clone(), v, source.clone())?; env_ts.add_version(tvr); } ts.merge(env_ts); diff --git a/src/toolset/install_state.rs b/src/toolset/install_state.rs index ef006a0843..b7e614b532 100644 --- a/src/toolset/install_state.rs +++ b/src/toolset/install_state.rs @@ -7,7 +7,6 @@ use crate::{dirs, file, runtime_symlinks}; use eyre::{Ok, Result}; use heck::ToKebabCase; use itertools::Itertools; -use rayon::prelude::*; use std::collections::BTreeMap; use std::ops::Deref; use std::path::{Path, PathBuf}; @@ -28,21 +27,11 @@ pub struct InstallStateTool { static INSTALL_STATE_PLUGINS: Mutex>> = Mutex::new(None); static INSTALL_STATE_TOOLS: Mutex>> = Mutex::new(None); -pub(crate) fn init() -> Result<()> { - let (plugins, tools) = rayon::join( - || { - measure!("init_plugins", { drop(init_plugins()?) }); - Ok(()) - }, - || { - measure!("init_tools", { - drop(init_tools()?); - }); - Ok(()) - }, - ); - plugins?; - tools?; +pub(crate) async fn init() -> Result<()> { + tokio::try_join!( + async { measure!("init_plugins", { init_plugins() }) }, + async { measure!("init_tools", { init_tools() }) }, + )?; Ok(()) } @@ -79,7 +68,7 @@ fn init_tools() -> MutexResult { return Ok(tools); } let mut tools = file::dir_subdirs(&dirs::INSTALLS)? - .into_par_iter() + .into_iter() .map(|dir| { let backend_meta = read_backend_meta(&dir).unwrap_or_default(); let short = backend_meta.first().unwrap_or(&dir).to_string(); diff --git a/src/toolset/mod.rs b/src/toolset/mod.rs index f644716a26..2262e0ff94 100644 --- a/src/toolset/mod.rs +++ b/src/toolset/mod.rs @@ -1,8 +1,7 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt::{Display, Formatter}; use std::path::PathBuf; -use std::sync::{Arc, Mutex}; -use std::{panic, thread}; +use std::sync::Arc; use crate::backend::Backend; use crate::cli::args::BackendArg; @@ -17,17 +16,17 @@ use crate::install_context::InstallContext; use crate::path_env::PathEnv; use crate::registry::tool_enabled; use crate::ui::multi_progress_report::MultiProgressReport; -use crate::uv::get_uv_venv; +use crate::uv; use crate::{backend, config, env, hooks}; pub use builder::ToolsetBuilder; use console::truncate_str; -use eyre::{Result, WrapErr, eyre}; +use eyre::{Result, WrapErr}; use indexmap::{IndexMap, IndexSet}; use itertools::Itertools; -use once_cell::sync::OnceCell; use outdated_info::OutdatedInfo; pub use outdated_info::is_outdated_version; -use rayon::prelude::*; +use tokio::sync::OnceCell; +use tokio::{sync::Semaphore, task::JoinSet}; pub use tool_request::ToolRequest; pub use tool_request_set::{ToolRequestSet, ToolRequestSetBuilder}; pub use tool_source::ToolSource; @@ -85,7 +84,7 @@ pub fn parse_tool_options(s: &str) -> ToolVersionOptions { tvo } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct InstallOptions { pub force: bool, pub jobs: Option, @@ -116,7 +115,7 @@ impl Default for InstallOptions { /// merge in other toolsets from various sources #[derive(Debug, Default, Clone)] pub struct Toolset { - pub versions: IndexMap, + pub versions: IndexMap, ToolVersionList>, pub source: Option, tera_ctx: OnceCell, } @@ -150,29 +149,39 @@ impl Toolset { self.versions = versions; self.source = other.source; } - pub fn resolve(&mut self) -> eyre::Result<()> { + pub async fn resolve(&mut self) -> eyre::Result<()> { + let config = Config::get().await; self.list_missing_plugins(); - let errors = self - .versions - .iter_mut() - .collect::>() - .par_iter_mut() - .map(|(_, v)| v.resolve(&Default::default())) - .filter(|r| r.is_err()) - .map(|r| r.unwrap_err()) + let mut jset: JoinSet> = JoinSet::new(); + for (i, (ba, mut tvl)) in self.versions.clone().into_iter().enumerate() { + let config = config.clone(); + jset.spawn(async move { + tvl.resolve(&config, &Default::default()).await?; + Ok((i, ba, tvl)) + }); + } + let tvls = jset + .join_all() + .await + .into_iter() + .collect::>>()? + .into_iter() + .sorted_by_key(|(i, _, _)| *i) + .map(|(_, ba, tvl)| (ba, tvl)) .collect::>(); - match errors.is_empty() { - true => Ok(()), - false => { - let err = eyre!("error resolving versions"); - Err(errors.into_iter().fold(err, |e, x| e.wrap_err(x))) - } + for (ba, tvl) in tvls { + self.versions.insert(ba, tvl); } + Ok(()) } - pub fn install_missing_versions(&mut self, opts: &InstallOptions) -> Result> { - let mpr = MultiProgressReport::get(); + pub async fn install_missing_versions( + &mut self, + config: &Arc, + opts: &InstallOptions, + ) -> Result> { let versions = self .list_missing_versions() + .await .into_iter() .filter(|tv| { !opts.missing_args_only @@ -187,9 +196,9 @@ impl Toolset { }) .map(|tv| tv.request) .collect_vec(); - let versions = self.install_all_versions(versions, &mpr, opts)?; + let versions = self.install_all_versions(config, versions, opts).await?; if !versions.is_empty() { - config::rebuild_shims_and_runtime_symlinks(&versions)?; + config::rebuild_shims_and_runtime_symlinks(&versions).await?; } Ok(versions) } @@ -232,16 +241,16 @@ impl Toolset { } } - pub fn install_all_versions( + pub async fn install_all_versions( &mut self, + config: &Arc, mut versions: Vec, - mpr: &MultiProgressReport, opts: &InstallOptions, ) -> Result> { if versions.is_empty() { return Ok(vec![]); } - hooks::run_one_hook(self, Hooks::Preinstall, None); + hooks::run_one_hook(self, Hooks::Preinstall, None).await; self.init_request_options(&mut versions); show_python_install_hint(&versions); let mut installed = vec![]; @@ -251,13 +260,13 @@ impl Toolset { debug!("installing {} leaf tools first", leaf_deps.len()); } versions.retain(|tr| !leaf_deps.contains(tr)); - installed.extend(self.install_some_versions(leaf_deps, mpr, opts)?); + installed.extend(self.install_some_versions(config, leaf_deps, opts).await?); leaf_deps = get_leaf_dependencies(&versions)?; } trace!("install: resolving"); install_state::reset(); - if let Err(err) = self.resolve() { + if let Err(err) = self.resolve().await { debug!("error resolving versions after install: {err:#}"); } if log::log_enabled!(log::Level::Debug) { @@ -265,13 +274,16 @@ impl Toolset { let backend = tv.backend()?; let bin_paths = backend .list_bin_paths(tv) + .await .map_err(|e| { warn!("Error listing bin paths for {tv}: {e:#}"); }) .unwrap_or_default(); debug!("[{tv}] list_bin_paths: {bin_paths:?}"); + let config = Config::get().await; let env = backend - .exec_env(&Config::get(), self, tv) + .exec_env(&config, self, tv) + .await .map_err(|e| { warn!("Error running exec-env: {e:#}"); }) @@ -281,14 +293,14 @@ impl Toolset { } } } - hooks::run_one_hook(self, Hooks::Postinstall, None); + hooks::run_one_hook(self, Hooks::Postinstall, None).await; Ok(installed) } - fn install_some_versions( + async fn install_some_versions( &mut self, + config: &Arc, versions: Vec, - mpr: &MultiProgressReport, opts: &InstallOptions, ) -> Result> { debug!("install_some_versions: {}", versions.iter().join(" ")); @@ -302,7 +314,8 @@ impl Toolset { for (backend, _) in &queue { if let Some(plugin) = backend.plugin() { if !plugin.is_installed() { - plugin.ensure_installed(mpr, false).or_else(|err| { + let mpr = MultiProgressReport::get(); + plugin.ensure_installed(&mpr, false).await.or_else(|err| { if let Some(&Error::PluginNotInstalled(_)) = err.downcast_ref::() { Ok(()) } else { @@ -312,87 +325,97 @@ impl Toolset { } } } - let queue = Arc::new(Mutex::new(queue)); let raw = opts.raw || SETTINGS.raw; let jobs = match raw { true => 1, false => opts.jobs.unwrap_or(SETTINGS.jobs), }; - thread::scope(|s| { - #[allow(clippy::map_collect_result_unit)] - (0..jobs) - .map(|_| { - let queue = queue.clone(); - let ts = &*self; - s.spawn(move || { - let next_job = || queue.lock().unwrap().pop(); - let mut installed = vec![]; - while let Some((t, versions)) = next_job() { - for tr in versions { - let tv = tr.resolve(&opts.resolve_options)?; - let ctx = InstallContext { - ts, - pr: mpr.add(&tv.style()), - force: opts.force, - }; - let old_tv = tv.clone(); - let tv = t - .install_version(ctx, tv) - .wrap_err_with(|| format!("failed to install {old_tv}"))?; - installed.push(tv); - } - } - Ok(installed) - }) - }) - .collect::>() + let semaphore = Arc::new(Semaphore::new(jobs)); + let ts = Arc::new(self.clone()); + let mut tset: JoinSet, eyre::Report>> = JoinSet::new(); + let opts = Arc::new(opts.clone()); + for (ba, trs) in queue { + let ts = ts.clone(); + let semaphore = semaphore.clone(); + let opts = opts.clone(); + let ba = ba.clone(); + let config = config.clone(); + tset.spawn(async move { + let _permit = semaphore.acquire().await?; + let mpr = MultiProgressReport::get(); + let mut installed = vec![]; + for tr in trs { + let tv = tr.resolve(&config, &opts.resolve_options).await?; + let ctx = InstallContext { + ts: ts.clone(), + pr: mpr.add(&tv.style()), + force: opts.force, + }; + let old_tv = tv.clone(); + let tv = ba + .install_version(ctx, tv) + .await + .wrap_err_with(|| format!("failed to install {old_tv}"))?; + installed.push(tv); + } + Ok(installed) + }); + } + let mut installed = vec![]; + while let Some(res) = tset.join_next().await { + installed.extend(res??); + } + installed.reverse(); + Ok(installed) + } + + pub async fn list_missing_versions(&self) -> Vec { + let config = Config::get().await; + measure!("toolset::list_missing_versions", { + self.list_current_versions() .into_iter() - .map(|t| match t.join() { - Ok(x) => x, - Err(e) => panic::resume_unwind(e), + .filter(|(p, tv)| { + tv.request.is_os_supported() && !p.is_version_installed(&config, tv, true) }) - .collect::>>>() - .map(|x| x.into_iter().flatten().rev().collect()) + .map(|(_, tv)| tv) + .collect() }) } - - pub fn list_missing_versions(&self) -> Vec { - self.list_current_versions() - .into_par_iter() - .filter(|(p, tv)| tv.request.is_os_supported() && !p.is_version_installed(tv, true)) - .map(|(_, tv)| tv) - .collect() - } - pub fn list_installed_versions(&self) -> Result, ToolVersion)>> { - let current_versions: HashMap<(String, String), (Arc, ToolVersion)> = self + pub async fn list_installed_versions(&self) -> Result> { + let config = Config::get().await; + let current_versions: HashMap<(String, String), TVTuple> = self .list_current_versions() - .into_par_iter() + .into_iter() .map(|(p, tv)| ((p.id().into(), tv.version.clone()), (p.clone(), tv))) .collect(); - let versions = backend::list() - .into_par_iter() - .map(|p| { - let versions = p.list_installed_versions()?; - versions - .into_iter() - .map( - |v| match current_versions.get(&(p.id().into(), v.clone())) { - Some((p, tv)) => Ok((p.clone(), tv.clone())), - None => { - let tv = ToolRequest::new(p.ba().clone(), &v, ToolSource::Unknown)? - .resolve(&Default::default()) - .unwrap(); - Ok((p.clone(), tv)) - } - }, - ) - .collect::>>() - }) + let current_versions = Arc::new(current_versions); + let mut jset: JoinSet> = JoinSet::new(); + for (i, b) in backend::list().into_iter().enumerate() { + let current_versions = current_versions.clone(); + let config = config.clone(); + jset.spawn(async move { + let mut versions = vec![]; + for v in b.list_installed_versions()? { + if let Some((p, tv)) = current_versions.get(&(b.id().into(), v.clone())) { + versions.push((p.clone(), tv.clone())); + } + let tv = ToolRequest::new(b.ba().clone(), &v, ToolSource::Unknown)? + .resolve(&config, &Default::default()) + .await?; + versions.push((b.clone(), tv)); + } + Ok((i, versions)) + }); + } + let versions = jset + .join_all() + .await + .into_iter() .collect::>>()? .into_iter() - .flatten() + .sorted_by_key(|(i, _)| *i) + .flat_map(|(_, versions)| versions) .collect(); - Ok(versions) } pub fn list_current_requests(&self) -> Vec<&ToolRequest> { @@ -432,27 +455,32 @@ impl Toolset { }) .collect() } - pub fn list_all_versions(&self) -> Result, ToolVersion)>> { + pub async fn list_all_versions(&self) -> Result, ToolVersion)>> { let versions = self .list_current_versions() .into_iter() - .chain(self.list_installed_versions()?) + .chain(self.list_installed_versions().await?) .unique_by(|(ba, tv)| (ba.clone(), tv.tv_pathname().to_string())) .collect(); Ok(versions) } - pub fn list_current_installed_versions(&self) -> Vec<(Arc, ToolVersion)> { + pub fn list_current_installed_versions( + &self, + config: &Config, + ) -> Vec<(Arc, ToolVersion)> { self.list_current_versions() - .into_par_iter() - .filter(|(p, v)| p.is_version_installed(v, true)) + .into_iter() + .filter(|(p, v)| p.is_version_installed(config, v, true)) .collect() } - pub fn list_outdated_versions(&self, bump: bool) -> Vec { - self.list_current_versions() - .into_par_iter() - .filter_map(|(t, tv)| { - match t.outdated_info(&tv, bump) { - Ok(Some(oi)) => return Some(oi), + pub async fn list_outdated_versions(&self, bump: bool) -> Vec { + let config = Config::get().await; + let mut jset = JoinSet::new(); + for (i, (t, tv)) in self.list_current_versions().into_iter().enumerate() { + let config = config.clone(); + jset.spawn(async move { + match t.outdated_info(&tv, bump).await { + Ok(Some(oi)) => return Some((i, oi)), Ok(None) => {} Err(e) => { warn!("Error getting outdated info for {tv}: {e:#}"); @@ -464,50 +492,79 @@ impl Toolset { // do not consider symlinked versions to be outdated return None; } - OutdatedInfo::resolve(tv.clone(), bump).unwrap_or_else(|e| { - warn!("Error creating OutdatedInfo for {tv}: {e:#}"); - None - }) - }) + OutdatedInfo::resolve(&config, tv.clone(), bump) + .await + .unwrap_or_else(|e| { + warn!("Error creating OutdatedInfo for {tv}: {e:#}"); + None + }) + .map(|oi| (i, oi)) + }); + } + jset.join_all() + .await + .into_iter() + .flatten() + .sorted_by_key(|(i, _)| *i) + .map(|(_, oi)| oi) .collect() } /// returns env_with_path but also with the existing env vars from the system - pub fn full_env(&self, config: &Config) -> Result { + pub async fn full_env(&self, config: &Config) -> Result { let mut env = env::PRISTINE_ENV.clone().into_iter().collect::(); - env.extend(self.env_with_path(config)?.clone()); + env.extend(self.env_with_path(config).await?.clone()); Ok(env) } /// the full mise environment including all tool paths - pub fn env_with_path(&self, config: &Config) -> Result { - let (mut env, env_results) = self.final_env(config)?; + pub async fn env_with_path(&self, config: &Config) -> Result { + let (mut env, env_results) = self.final_env(config).await?; let mut path_env = PathEnv::from_iter(env::PATH.clone()); - for p in self.list_final_paths(config, env_results)? { + for p in self.list_final_paths(config, env_results).await? { path_env.add(p.clone()); } env.insert(PATH_KEY.to_string(), path_env.to_string()); Ok(env) } - pub fn env_from_tools(&self, config: &Config) -> Vec<(String, String, String)> { - self.list_current_installed_versions() - .into_par_iter() - .filter(|(_, tv)| !matches!(tv.request, ToolRequest::System { .. })) - .flat_map(|(p, tv)| match p.exec_env(config, self, &tv) { - Ok(env) => env - .into_iter() - .map(|(k, v)| (k, v, p.id().into())) - .collect(), - Err(e) => { - warn!("Error running exec-env: {:#}", e); - Vec::new() + pub async fn env_from_tools(&self) -> Vec<(String, String, String)> { + let mut jset = JoinSet::new(); + let config = Config::get().await; + for (i, (b, tv)) in self + .list_current_installed_versions(&config) + .into_iter() + .enumerate() + { + if matches!(tv.request, ToolRequest::System { .. }) { + continue; + } + let this = Arc::new(self.clone()); + jset.spawn(async move { + let config = Config::get().await; + match b.exec_env(&config, &this, &tv).await { + Ok(env) => env + .into_iter() + .map(|(k, v)| (i, k, v, b.id().to_string())) + .collect(), + Err(e) => { + warn!("Error running exec-env: {:#}", e); + Vec::new() + } } - }) - .filter(|(_, k, _)| k.to_uppercase() != "PATH") - .collect::>() + }); + } + jset.join_all() + .await + .into_iter() + .flatten() + .sorted_by_key(|(i, _, _, _)| *i) + .map(|(_, k, v, id)| (k, v, id)) + .filter(|(k, _, _)| k.to_uppercase() != "PATH") + .collect() } - fn env(&self, config: &Config) -> Result { + async fn env(&self, config: &Config) -> Result { time!("env start"); let entries = self - .env_from_tools(config) + .env_from_tools() + .await .into_iter() .map(|(k, v, _)| (k, v)) .collect::>(); @@ -527,8 +584,8 @@ impl Toolset { if !add_paths.is_empty() { env.insert(PATH_KEY.to_string(), add_paths); } - env.extend(config.env()?.clone()); - if let Some(venv) = get_uv_venv() { + env.extend(config.env().await?.clone()); + if let Some(venv) = uv::uv_venv().await { for (k, v) in venv.env { env.insert(k, v); } @@ -536,21 +593,21 @@ impl Toolset { time!("env end"); Ok(env) } - pub fn final_env(&self, config: &Config) -> Result<(EnvMap, EnvResults)> { - let mut env = self.env(config)?; + pub async fn final_env(&self, config: &Config) -> Result<(EnvMap, EnvResults)> { + let mut env = self.env(config).await?; let mut tera_env = env::PRISTINE_ENV.clone().into_iter().collect::(); tera_env.extend(env.clone()); let mut path_env = PathEnv::from_iter(env::PATH.clone()); - for p in self.list_paths() { + for p in self.list_paths().await { path_env.add(p); } - for p in config.path_dirs()?.clone() { + for p in config.path_dirs().await?.clone() { path_env.add(p); } tera_env.insert(PATH_KEY.to_string(), path_env.to_string()); let mut ctx = config.tera_ctx.clone(); ctx.insert("env", &tera_env); - let env_results = self.load_post_env(config, ctx, &tera_env)?; + let env_results = self.load_post_env(config, ctx, &tera_env).await?; env.extend( env_results .env @@ -559,36 +616,53 @@ impl Toolset { ); Ok((env, env_results)) } - pub fn list_paths(&self) -> Vec { - self.list_current_installed_versions() - .into_par_iter() - .filter(|(_, tv)| !matches!(tv.request, ToolRequest::System { .. })) - .flat_map(|(p, tv)| { - p.list_bin_paths(&tv).unwrap_or_else(|e| { - warn!("Error listing bin paths for {tv}: {e:#}"); - Vec::new() - }) - }) - .filter(|p| p.parent().is_some()) + pub async fn list_paths(&self) -> Vec { + let config = Config::get().await; + let mut jset = JoinSet::new(); + for (i, (p, tv)) in self + .list_current_installed_versions(&config) + .into_iter() + .enumerate() + { + jset.spawn(async move { + p.list_bin_paths(&tv) + .await + .unwrap_or_else(|e| { + warn!("Error listing bin paths for {tv}: {e:#}"); + Vec::new() + }) + .into_iter() + .map(|p| (i, p)) + .collect::>() + }); + } + + jset.join_all() + .await + .into_iter() + .flatten() + .sorted_by_key(|(i, _)| *i) + .map(|(_, path)| path) + .filter(|p| p.parent().is_some()) // TODO: why? .collect() } /// same as list_paths but includes config.list_paths, venv paths, and MISE_ADD_PATHs from self.env() - pub fn list_final_paths( + pub async fn list_final_paths( &self, config: &Config, env_results: EnvResults, ) -> Result> { let mut paths = IndexSet::new(); - for p in config.path_dirs()?.clone() { + for p in config.path_dirs().await?.clone() { paths.insert(p); } - if let Some(venv) = get_uv_venv() { + if let Some(venv) = uv::uv_venv().await { paths.insert(venv.venv_path); } - if let Some(path) = self.env(config)?.get(&*PATH_KEY) { + if let Some(path) = self.env(config).await?.get(&*PATH_KEY) { paths.insert(PathBuf::from(path)); } - for p in self.list_paths() { + for p in self.list_paths().await { paths.insert(p); } let mut path_env = PathEnv::from_iter(env::PATH.clone()); @@ -599,48 +673,51 @@ impl Toolset { let paths = env_results.env_paths.into_iter().chain(paths).collect(); Ok(paths) } - pub fn tera_ctx(&self) -> Result<&tera::Context> { - self.tera_ctx.get_or_try_init(|| { - let config = Config::get(); - let env = self.full_env(&config)?; - let mut ctx = config.tera_ctx.clone(); - ctx.insert("env", &env); - Ok(ctx) - }) - } - pub fn which(&self, bin_name: &str) -> Option<(Arc, ToolVersion)> { - self.list_current_installed_versions() - .into_par_iter() - .find_first(|(p, tv)| match p.which(tv, bin_name) { - Ok(x) => x.is_some(), + pub async fn tera_ctx(&self) -> Result<&tera::Context> { + self.tera_ctx + .get_or_try_init(async || { + let config = Config::try_get().await?; + let env = self.full_env(&config).await?; + let mut ctx = config.tera_ctx.clone(); + ctx.insert("env", &env); + Ok(ctx) + }) + .await + } + pub async fn which(&self, bin_name: &str) -> Option<(Arc, ToolVersion)> { + let config = Config::get().await; + for (p, tv) in self.list_current_installed_versions(&config) { + match Box::pin(p.which(&tv, bin_name)).await { + Ok(Some(_bin)) => return Some((p, tv)), + Ok(None) => {} Err(e) => { debug!("Error running which: {:#}", e); - false } - }) + } + } + None } - pub fn which_bin(&self, bin_name: &str) -> Option { - self.which(bin_name) - .and_then(|(p, tv)| p.which(&tv, bin_name).ok()) - .flatten() + pub async fn which_bin(&self, bin_name: &str) -> Option { + let (p, tv) = Box::pin(self.which(bin_name)).await?; + Box::pin(p.which(&tv, bin_name)).await.ok().flatten() } - pub fn install_missing_bin(&mut self, bin_name: &str) -> Result>> { - let plugins = self - .list_installed_versions()? - .into_iter() - .filter(|(p, tv)| { - if let Ok(x) = p.which(tv, bin_name) { - x.is_some() - } else { - false - } - }) - .collect_vec(); - for (plugin, _) in plugins { + pub async fn install_missing_bin( + &mut self, + config: &Arc, + bin_name: &str, + ) -> Result>> { + let mut plugins = IndexSet::new(); + for (p, tv) in self.list_current_installed_versions(config) { + if let Ok(Some(_bin)) = p.which(&tv, bin_name).await { + plugins.insert(p); + } + } + for plugin in plugins { let versions = self .list_missing_versions() + .await .into_iter() - .filter(|tv| tv.ba() == plugin.ba()) + .filter(|tv| tv.ba() == &**plugin.ba()) .filter(|tv| match &SETTINGS.auto_install_disable_tools { Some(disable_tools) => !disable_tools.contains(&tv.ba().short), None => true, @@ -648,11 +725,11 @@ impl Toolset { .map(|tv| tv.request) .collect_vec(); if !versions.is_empty() { - let mpr = MultiProgressReport::get(); - let versions = - self.install_all_versions(versions.clone(), &mpr, &InstallOptions::default())?; + let versions = self + .install_all_versions(config, versions.clone(), &InstallOptions::default()) + .await?; if !versions.is_empty() { - config::rebuild_shims_and_runtime_symlinks(&versions)?; + config::rebuild_shims_and_runtime_symlinks(&versions).await?; } return Ok(Some(versions)); } @@ -660,26 +737,26 @@ impl Toolset { Ok(None) } - pub fn list_rtvs_with_bin(&self, bin_name: &str) -> Result> { - Ok(self - .list_installed_versions()? - .into_par_iter() - .filter(|(p, tv)| match p.which(tv, bin_name) { - Ok(x) => x.is_some(), + pub async fn list_rtvs_with_bin(&self, bin_name: &str) -> Result> { + let mut rtvs = vec![]; + for (p, tv) in self.list_installed_versions().await? { + match p.which(&tv, bin_name).await { + Ok(Some(_bin)) => rtvs.push(tv), + Ok(None) => {} Err(e) => { warn!("Error running which: {:#}", e); - false } - }) - .map(|(_, tv)| tv) - .collect()) + } + } + Ok(rtvs) } // shows a warning if any versions are missing // only displays for tools which have at least one version already installed - pub fn notify_if_versions_missing(&self) { + pub async fn notify_if_versions_missing(&self) { let missing = self .list_missing_versions() + .await .into_iter() .filter(|tv| match SETTINGS.status.missing_tools() { SettingsStatusMissingTools::Never => false, @@ -712,7 +789,7 @@ impl Toolset { ) } - fn load_post_env( + async fn load_post_env( &self, config: &Config, ctx: tera::Context, @@ -732,6 +809,7 @@ impl Toolset { .collect(); // trace!("load_env: entries: {:#?}", entries); let env_results = EnvResults::resolve( + config, ctx, env, entries, @@ -739,7 +817,8 @@ impl Toolset { tools: true, ..Default::default() }, - )?; + ) + .await?; if log::log_enabled!(log::Level::Trace) { trace!("{env_results:#?}"); } else if !env_results.is_empty() { @@ -815,6 +894,8 @@ fn get_leaf_dependencies(requests: &[ToolRequest]) -> eyre::Result, ToolVersion); + #[cfg(test)] mod tests { use pretty_assertions::assert_eq; diff --git a/src/toolset/outdated_info.rs b/src/toolset/outdated_info.rs index afaca1ea82..9f3bb888d3 100644 --- a/src/toolset/outdated_info.rs +++ b/src/toolset/outdated_info.rs @@ -1,6 +1,6 @@ -use crate::Result; use crate::toolset; use crate::toolset::{ToolRequest, ToolSource, ToolVersion}; +use crate::{Result, config::Config}; use serde_derive::Serialize; use std::fmt::{Display, Formatter}; use tabled::Tabled; @@ -25,9 +25,9 @@ pub struct OutdatedInfo { } impl OutdatedInfo { - pub fn new(tv: ToolVersion, latest: String) -> Result { + pub fn new(config: &Config, tv: ToolVersion, latest: String) -> Result { let t = tv.backend()?; - let current = if t.is_version_installed(&tv, true) { + let current = if t.is_version_installed(config, &tv, true) { Some(tv.version.clone()) } else { None @@ -45,16 +45,20 @@ impl OutdatedInfo { Ok(oi) } - pub fn resolve(tv: ToolVersion, bump: bool) -> eyre::Result> { + pub async fn resolve( + config: &Config, + tv: ToolVersion, + bump: bool, + ) -> eyre::Result> { let t = tv.backend()?; // prefix is something like "temurin-" or "corretto-" let prefix = xx::regex!(r"^[a-zA-Z-]+-") .find(&tv.request.version()) .map(|m| m.as_str().to_string()); let latest_result = if bump { - t.latest_version(prefix.clone()) + t.latest_version(prefix.clone()).await } else { - tv.latest_version().map(Option::from) + tv.latest_version(config).await.map(Option::from) }; let latest = match latest_result { Ok(Some(latest)) => latest, @@ -67,7 +71,8 @@ impl OutdatedInfo { return Ok(None); } }; - let mut oi = Self::new(tv, latest)?; + let config = Config::try_get().await?; + let mut oi = Self::new(&config, tv, latest)?; if oi .current .as_ref() diff --git a/src/toolset/tool_request.rs b/src/toolset/tool_request.rs index cedafeb7db..33473fe856 100644 --- a/src/toolset/tool_request.rs +++ b/src/toolset/tool_request.rs @@ -1,61 +1,64 @@ -use std::fmt::{Display, Formatter}; use std::path::PathBuf; +use std::{ + fmt::{Display, Formatter}, + sync::Arc, +}; use eyre::{Result, bail}; use versions::{Chunk, Version}; use xx::file; -use crate::backend::ABackend; use crate::cli::args::BackendArg; use crate::lockfile::LockfileTool; use crate::runtime_symlinks::is_runtime_symlink; use crate::toolset::tool_version::ResolveOptions; use crate::toolset::{ToolSource, ToolVersion, ToolVersionOptions}; use crate::{backend, lockfile}; +use crate::{backend::ABackend, config::Config}; #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum ToolRequest { Version { - backend: BackendArg, + backend: Arc, version: String, options: ToolVersionOptions, source: ToolSource, }, Prefix { - backend: BackendArg, + backend: Arc, prefix: String, options: ToolVersionOptions, source: ToolSource, }, Ref { - backend: BackendArg, + backend: Arc, ref_: String, ref_type: String, options: ToolVersionOptions, source: ToolSource, }, Sub { - backend: BackendArg, + backend: Arc, sub: String, orig_version: String, options: ToolVersionOptions, source: ToolSource, }, Path { - backend: BackendArg, + backend: Arc, path: PathBuf, options: ToolVersionOptions, source: ToolSource, }, System { - backend: BackendArg, + backend: Arc, source: ToolSource, options: ToolVersionOptions, }, } impl ToolRequest { - pub fn new(backend: BackendArg, s: &str, source: ToolSource) -> eyre::Result { + pub fn new(backend: Arc, s: &str, source: ToolSource) -> eyre::Result { let s = match s.split_once('-') { Some((ref_type @ ("ref" | "tag" | "branch" | "rev"), r)) => format!("{ref_type}:{r}"), _ => s.to_string(), @@ -107,7 +110,7 @@ impl ToolRequest { }) } pub fn new_opts( - backend: BackendArg, + backend: Arc, s: &str, options: ToolVersionOptions, source: ToolSource, @@ -132,7 +135,7 @@ impl ToolRequest { } self.clone() } - pub fn ba(&self) -> &BackendArg { + pub fn ba(&self) -> &Arc { match self { Self::Version { backend, .. } | Self::Prefix { backend, .. } @@ -202,10 +205,11 @@ impl ToolRequest { } } - pub fn is_installed(&self) -> bool { + pub async fn is_installed(&self) -> bool { + let config = Config::get().await; if let Some(backend) = backend::get(self.ba()) { - match self.resolve(&Default::default()) { - Ok(tv) => backend.is_version_installed(&tv, false), + match self.resolve(&config, &Default::default()).await { + Ok(tv) => backend.is_version_installed(&config, &tv, false), Err(e) => { debug!("ToolRequest.is_installed: {e:#}"); false @@ -216,7 +220,7 @@ impl ToolRequest { } } - pub fn install_path(&self) -> Option { + pub fn install_path(&self, config: &Config) -> Option { match self { Self::Version { backend, version, .. @@ -233,7 +237,7 @@ impl ToolRequest { orig_version, .. } => self - .local_resolve(orig_version) + .local_resolve(config, orig_version) .inspect_err(|e| warn!("ToolRequest.local_resolve: {e:#}")) .unwrap_or_default() .map(|v| backend.installs_path.join(version_sub(&v, sub.as_str()))), @@ -254,17 +258,17 @@ impl ToolRequest { } } - pub fn lockfile_resolve(&self) -> Result> { + pub fn lockfile_resolve(&self, config: &Config) -> Result> { match self.source() { ToolSource::MiseToml(path) => { - lockfile::get_locked_version(Some(path), &self.ba().short, &self.version()) + lockfile::get_locked_version(config, Some(path), &self.ba().short, &self.version()) } - _ => lockfile::get_locked_version(None, &self.ba().short, &self.version()), + _ => lockfile::get_locked_version(config, None, &self.ba().short, &self.version()), } } - pub fn local_resolve(&self, v: &str) -> eyre::Result> { - if let Some(lt) = self.lockfile_resolve()? { + pub fn local_resolve(&self, config: &Config, v: &str) -> eyre::Result> { + if let Some(lt) = self.lockfile_resolve(config)? { return Ok(Some(lt.version)); } if let Some(backend) = backend::get(self.ba()) { @@ -279,8 +283,8 @@ impl ToolRequest { Ok(None) } - pub fn resolve(&self, opts: &ResolveOptions) -> Result { - ToolVersion::resolve(self.clone(), opts) + pub async fn resolve(&self, config: &Config, opts: &ResolveOptions) -> Result { + ToolVersion::resolve(config, self.clone(), opts).await } pub fn is_os_supported(&self) -> bool { diff --git a/src/toolset/tool_request_set.rs b/src/toolset/tool_request_set.rs index 257209cbae..a85877954a 100644 --- a/src/toolset/tool_request_set.rs +++ b/src/toolset/tool_request_set.rs @@ -1,5 +1,8 @@ -use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::fmt::{Debug, Display}; +use std::{ + collections::{BTreeMap, BTreeSet, HashSet}, + sync::Arc, +}; use crate::backend::backend_type::BackendType; use crate::cli::args::{BackendArg, ToolArg}; @@ -12,8 +15,8 @@ use itertools::Itertools; #[derive(Debug, Default, Clone)] pub struct ToolRequestSet { - pub tools: IndexMap>, - pub sources: BTreeMap, + pub tools: IndexMap, Vec>, + pub sources: BTreeMap, ToolSource>, } impl ToolRequestSet { @@ -40,15 +43,17 @@ impl ToolRequestSet { // .collect() // } - pub fn missing_tools(&self) -> Vec<&ToolRequest> { - self.tools - .values() - .flatten() - .filter(|tr| tr.is_os_supported() && !tr.is_installed()) - .collect() + pub async fn missing_tools(&self) -> Vec<&ToolRequest> { + let mut tools = vec![]; + for tr in self.tools.values().flatten() { + if tr.is_os_supported() && !tr.is_installed().await { + tools.push(tr); + } + } + tools } - pub fn list_tools(&self) -> Vec<&BackendArg> { + pub fn list_tools(&self) -> Vec<&Arc> { self.tools.keys().collect() } @@ -61,16 +66,18 @@ impl ToolRequestSet { list.push(tr); } - pub fn iter(&self) -> impl Iterator, &ToolSource)> { + pub fn iter(&self) -> impl Iterator, &Vec, &ToolSource)> { self.tools .iter() .map(|(backend, tvr)| (backend, tvr, self.sources.get(backend).unwrap())) } - pub fn into_iter(self) -> impl Iterator, ToolSource)> { - self.tools.into_iter().map(move |(fa, tvr)| { - let source = self.sources.get(&fa).unwrap().clone(); - (fa, tvr, source) + pub fn into_iter( + self, + ) -> impl Iterator, Vec, ToolSource)> { + self.tools.into_iter().map(move |(ba, tvr)| { + let source = self.sources.get(&ba).unwrap().clone(); + (ba, tvr, source) }) } @@ -83,7 +90,7 @@ impl ToolRequestSet { } self.iter() .filter(|(ba, ..)| tools.contains(&ba.short)) - .map(|(fa, trl, ts)| (fa.clone(), trl.clone(), ts.clone())) + .map(|(ba, trl, ts)| (ba.clone(), trl.clone(), ts.clone())) .collect::() } @@ -104,13 +111,13 @@ impl Display for ToolRequestSet { } } -impl FromIterator<(BackendArg, Vec, ToolSource)> for ToolRequestSet { +impl FromIterator<(Arc, Vec, ToolSource)> for ToolRequestSet { fn from_iter(iter: T) -> Self where - T: IntoIterator, ToolSource)>, + T: IntoIterator, Vec, ToolSource)>, { let mut trs = ToolRequestSet::new(); - for (_fa, tvr, source) in iter { + for (_ba, tvr, source) in iter { for tr in tvr { trs.add_version(tr.clone(), &source); } @@ -152,9 +159,9 @@ impl ToolRequestSetBuilder { // } // - pub fn build(&self) -> eyre::Result { + pub async fn build(&self, config: &Config) -> eyre::Result { let mut trs = ToolRequestSet::default(); - trs = self.load_config_files(trs)?; + trs = self.load_config_files(config, trs).await?; trs = self.load_runtime_env(trs)?; trs = self.load_runtime_args(trs)?; @@ -177,8 +184,11 @@ impl ToolRequestSetBuilder { || !tool_enabled(&self.enable_tools, &self.disable_tools, ba) } - fn load_config_files(&self, mut trs: ToolRequestSet) -> eyre::Result { - let config = Config::get(); + async fn load_config_files( + &self, + config: &Config, + mut trs: ToolRequestSet, + ) -> eyre::Result { for cf in config.config_files.values().rev() { trs = merge(trs, cf.to_tool_request_set()?); } @@ -196,11 +206,11 @@ impl ToolRequestSetBuilder { // ignore MISE_INSTALL_VERSION continue; } - let fa: BackendArg = plugin_name.as_str().into(); + let ba: Arc = Arc::new(plugin_name.as_str().into()); let source = ToolSource::Environment(k, v.clone()); let mut env_ts = ToolRequestSet::new(); for v in v.split_whitespace() { - let tvr = ToolRequest::new(fa.clone(), v, source.clone())?; + let tvr = ToolRequest::new(ba.clone(), v, source.clone())?; env_ts.add_version(tvr, &source); } trs = merge(trs, env_ts); diff --git a/src/toolset/tool_version.rs b/src/toolset/tool_version.rs index a29580a783..02ec0571a9 100644 --- a/src/toolset/tool_version.rs +++ b/src/toolset/tool_version.rs @@ -36,10 +36,14 @@ impl ToolVersion { } } - pub fn resolve(request: ToolRequest, opts: &ResolveOptions) -> Result { + pub async fn resolve( + config: &Config, + request: ToolRequest, + opts: &ResolveOptions, + ) -> Result { trace!("resolving {} {}", &request, opts); if opts.use_locked_version { - if let Some(lt) = request.lockfile_resolve()? { + if let Some(lt) = request.lockfile_resolve(config)? { let mut tv = Self::new(request.clone(), lt.version); tv.checksums = lt.checksums; return Ok(tv); @@ -53,16 +57,21 @@ impl ToolVersion { } } let tv = match request.clone() { - ToolRequest::Version { version: v, .. } => Self::resolve_version(request, &v, opts)?, - ToolRequest::Prefix { prefix, .. } => Self::resolve_prefix(request, &prefix, opts)?, + ToolRequest::Version { version: v, .. } => { + Self::resolve_version(config, request, &v, opts).await? + } + ToolRequest::Prefix { prefix, .. } => { + Self::resolve_prefix(request, &prefix, opts).await? + } ToolRequest::Sub { sub, orig_version, .. - } => Self::resolve_sub(request, &sub, &orig_version, opts)?, + } => Self::resolve_sub(config, request, &sub, &orig_version, opts).await?, _ => { let version = request.version(); Self::new(request, version) } }; + trace!("resolved: {tv}"); Ok(tv) } @@ -110,12 +119,12 @@ impl ToolVersion { pub fn download_path(&self) -> PathBuf { self.request.ba().downloads_path.join(self.tv_pathname()) } - pub fn latest_version(&self) -> Result { + pub async fn latest_version(&self, config: &Config) -> Result { let opts = ResolveOptions { latest_versions: true, use_locked_version: false, }; - let tv = self.request.resolve(&opts)?; + let tv = self.request.resolve(config, &opts).await?; // map cargo backend specific prefixes to ref let version = match tv.request.version().split_once(':') { Some((_ref_type @ ("tag" | "branch" | "rev"), r)) => { @@ -149,14 +158,14 @@ impl ToolVersion { } .replace([':', '/'], "-") } - fn resolve_version( + async fn resolve_version( + config: &Config, request: ToolRequest, v: &str, opts: &ResolveOptions, ) -> Result { - let config = Config::get(); let backend = request.backend()?; - let v = config.resolve_alias(&backend, v)?; + let v = config.resolve_alias(&backend, v).await?; match v.split_once(':') { Some((ref_type @ ("ref" | "tag" | "branch" | "rev"), r)) => { return Ok(Self::resolve_ref( @@ -170,11 +179,11 @@ impl ToolVersion { return Self::resolve_path(PathBuf::from(p), &request); } Some(("prefix", p)) => { - return Self::resolve_prefix(request, p, opts); + return Self::resolve_prefix(request, p, opts).await; } Some((part, v)) if part.starts_with("sub-") => { let sub = part.split_once('-').unwrap().1; - return Self::resolve_sub(request, sub, v, opts); + return Self::resolve_sub(config, request, sub, v, opts).await; } _ => (), } @@ -193,7 +202,7 @@ impl ToolVersion { return build(v); } } - if let Some(v) = backend.latest_version(None)? { + if let Some(v) = backend.latest_version(None).await? { return build(v); } } @@ -206,15 +215,16 @@ impl ToolVersion { return build(v.clone()); } } - let matches = backend.list_versions_matching(&v)?; + let matches = backend.list_versions_matching(&v).await?; if matches.contains(&v) { return build(v); } - Self::resolve_prefix(request, &v, opts) + Self::resolve_prefix(request, &v, opts).await } /// resolve a version like `sub-1:12.0.0` which becomes `11.0.0`, `sub-0.1:12.1.0` becomes `12.0.0` - fn resolve_sub( + async fn resolve_sub( + config: &Config, request: ToolRequest, sub: &str, v: &str, @@ -222,21 +232,25 @@ impl ToolVersion { ) -> Result { let backend = request.backend()?; let v = match v { - "latest" => backend.latest_version(None)?.unwrap(), - _ => Config::get().resolve_alias(&backend, v)?, + "latest" => backend.latest_version(None).await?.unwrap(), + _ => config.resolve_alias(&backend, v).await?, }; let v = tool_request::version_sub(&v, sub); - Self::resolve_version(request, &v, opts) + Box::pin(Self::resolve_version(config, request, &v, opts)).await } - fn resolve_prefix(request: ToolRequest, prefix: &str, opts: &ResolveOptions) -> Result { + async fn resolve_prefix( + request: ToolRequest, + prefix: &str, + opts: &ResolveOptions, + ) -> Result { let backend = request.backend()?; if !opts.latest_versions { if let Some(v) = backend.list_installed_versions_matching(prefix)?.last() { return Ok(Self::new(request, v.to_string())); } } - let matches = backend.list_versions_matching(prefix)?; + let matches = backend.list_versions_matching(prefix).await?; let v = match matches.last() { Some(v) => v, None => prefix, @@ -297,7 +311,7 @@ impl PartialOrd for ToolVersion { impl Ord for ToolVersion { fn cmp(&self, other: &Self) -> Ordering { - match self.request.ba().cmp(other.ba()) { + match self.request.ba().as_ref().cmp(other.ba()) { Ordering::Equal => self.version.cmp(&other.version), o => o, } diff --git a/src/toolset/tool_version_list.rs b/src/toolset/tool_version_list.rs index 54857578ce..8b4f947653 100644 --- a/src/toolset/tool_version_list.rs +++ b/src/toolset/tool_version_list.rs @@ -1,20 +1,22 @@ -use crate::cli::args::BackendArg; +use std::sync::Arc; + use crate::errors::Error; use crate::toolset::tool_request::ToolRequest; use crate::toolset::tool_version::ResolveOptions; use crate::toolset::{ToolSource, ToolVersion}; +use crate::{cli::args::BackendArg, config::Config}; /// represents several versions of a tool for a particular plugin #[derive(Debug, Clone)] pub struct ToolVersionList { - pub backend: BackendArg, + pub backend: Arc, pub versions: Vec, pub requests: Vec, pub source: ToolSource, } impl ToolVersionList { - pub fn new(backend: BackendArg, source: ToolSource) -> Self { + pub fn new(backend: Arc, source: ToolSource) -> Self { Self { backend, versions: Vec::new(), @@ -22,10 +24,10 @@ impl ToolVersionList { source, } } - pub fn resolve(&mut self, opts: &ResolveOptions) -> eyre::Result<()> { + pub async fn resolve(&mut self, config: &Config, opts: &ResolveOptions) -> eyre::Result<()> { self.versions.clear(); for tvr in &mut self.requests { - match tvr.resolve(opts) { + match tvr.resolve(config, opts).await { Ok(v) => self.versions.push(v), Err(err) => { return Err(Error::FailedToResolveVersion { @@ -49,34 +51,45 @@ mod tests { use super::*; - #[test] + #[tokio::test] #[cfg(unix)] - fn test_tool_version_list() { - let fa: BackendArg = "tiny".into(); - let mut tvl = ToolVersionList::new(fa.clone(), ToolSource::Argument); + async fn test_tool_version_list() { + let ba: Arc = Arc::new("tiny".into()); + let config = Config::get().await; + let mut tvl = ToolVersionList::new(ba.clone(), ToolSource::Argument); tvl.requests - .push(ToolRequest::new(fa, "latest", ToolSource::Argument).unwrap()); - tvl.resolve(&ResolveOptions { - latest_versions: true, - use_locked_version: false, - }) + .push(ToolRequest::new(ba, "latest", ToolSource::Argument).unwrap()); + tvl.resolve( + &config, + &ResolveOptions { + latest_versions: true, + use_locked_version: false, + }, + ) + .await .unwrap(); assert_eq!(tvl.versions.len(), 1); } - #[test] - fn test_tool_version_list_failure() { + #[tokio::test] + async fn test_tool_version_list_failure() { backend::reset(); env::set_var("MISE_FAILURE", "1"); file::remove_all(dirs::CACHE.join("dummy")).unwrap(); - let fa: BackendArg = "dummy".into(); - let mut tvl = ToolVersionList::new(fa.clone(), ToolSource::Argument); + let config = Config::get().await; + let ba: Arc = Arc::new("dummy".into()); + let mut tvl = ToolVersionList::new(ba.clone(), ToolSource::Argument); tvl.requests - .push(ToolRequest::new(fa, "latest", ToolSource::Argument).unwrap()); - let _ = tvl.resolve(&ResolveOptions { - latest_versions: true, - use_locked_version: false, - }); + .push(ToolRequest::new(ba, "latest", ToolSource::Argument).unwrap()); + let _ = tvl + .resolve( + &config, + &ResolveOptions { + latest_versions: true, + use_locked_version: false, + }, + ) + .await; assert_eq!(tvl.versions.len(), 0); env::remove_var("MISE_FAILURE"); } diff --git a/src/uv.rs b/src/uv.rs index 573d8be945..1b4347d64b 100644 --- a/src/uv.rs +++ b/src/uv.rs @@ -6,7 +6,8 @@ use crate::{dirs, file}; use eyre::Result; use std::collections::HashMap; use std::path::PathBuf; -use std::sync::{LazyLock as Lazy, Mutex}; +use std::sync::LazyLock as Lazy; +use tokio::sync::OnceCell; #[derive(Clone, Debug)] pub struct Venv { @@ -16,34 +17,41 @@ pub struct Venv { // use a mutex to prevent deadlocks that occurs due to reentrantly initialization // when resolving the venv path or env vars -static UV_VENV: Mutex>> = Mutex::new(Lazy::new(|| { +static UV_VENV: Lazy>> = Lazy::new(Default::default); + +pub async fn uv_venv() -> Option { + if let Some(venv) = UV_VENV.get() { + return venv.clone(); + } if !SETTINGS.python.uv_venv_auto { + UV_VENV.set(None).unwrap(); return None; } - if let (Some(venv_path), Some(uv_path)) = (venv_path(), uv_path()) { - match get_or_create_venv(venv_path, uv_path) { - Ok(venv) => return Some(venv), + if let (Some(venv_path), Some(uv_path)) = (venv_path(), uv_path().await) { + match get_or_create_venv(venv_path, uv_path).await { + Ok(venv) => { + UV_VENV.set(Some(venv.clone())).unwrap(); + return Some(venv); + } Err(e) => { warn!("uv venv failed: {e}"); } } } + UV_VENV.set(None).unwrap(); None -})); - -pub fn get_uv_venv() -> Option { - let uv_venv = UV_VENV.try_lock().ok()?; - uv_venv.as_ref().cloned() } -fn get_or_create_venv(venv_path: PathBuf, uv_path: PathBuf) -> Result { +async fn get_or_create_venv(venv_path: PathBuf, uv_path: PathBuf) -> Result { SETTINGS.ensure_experimental("uv venv auto")?; let mut venv = Venv { env: Default::default(), venv_path: venv_path.join("bin"), }; if let Some(python_tv) = Config::get() - .get_toolset()? + .await + .get_toolset() + .await? .versions .get(&BackendArg::from("python")) .and_then(|tvl| tvl.versions.first()) @@ -77,10 +85,11 @@ fn uv_root() -> Option { fn venv_path() -> Option { Some(uv_root()?.join(".venv")) } -fn uv_path() -> Option { - Config::get() - .get_toolset() - .ok()? - .which_bin("uv") - .or_else(|| which::which("uv").ok()) +async fn uv_path() -> Option { + let config = Config::try_get().await.ok()?; + let ts = config.get_toolset().await.ok()?; + if let Some(uv_path) = ts.which_bin("uv").await { + return Some(uv_path); + } + which::which("uv").ok() } diff --git a/src/versions_host.rs b/src/versions_host.rs index 0cc4a7d8bb..e7ac27ecf1 100644 --- a/src/versions_host.rs +++ b/src/versions_host.rs @@ -17,7 +17,7 @@ static PLUGINS_USE_VERSION_HOST: Lazy> = Lazy::new(|| { .collect() }); -pub fn list_versions(ba: &BackendArg) -> eyre::Result>> { +pub async fn list_versions(ba: &BackendArg) -> eyre::Result>> { if !SETTINGS.use_versions_host || ba.short.contains(':') || !PLUGINS_USE_VERSION_HOST.contains(ba.short.as_str()) @@ -42,8 +42,16 @@ pub fn list_versions(ba: &BackendArg) -> eyre::Result>> { } } let response = match SETTINGS.paranoid { - true => HTTP_FETCH.get_text(format!("https://mise-versions.jdx.dev/{}", &ba.short)), - false => HTTP_FETCH.get_text(format!("http://mise-versions.jdx.dev/{}", &ba.short)), + true => { + HTTP_FETCH + .get_text(format!("https://mise-versions.jdx.dev/{}", &ba.short)) + .await + } + false => { + HTTP_FETCH + .get_text(format!("http://mise-versions.jdx.dev/{}", &ba.short)) + .await + } }; let versions = // using http is not a security concern and enabling tls makes mise significantly slower diff --git a/src/watch_files.rs b/src/watch_files.rs index d4dad29083..1677282bf6 100644 --- a/src/watch_files.rs +++ b/src/watch_files.rs @@ -5,7 +5,6 @@ use crate::toolset::Toolset; use eyre::Result; use globset::{GlobBuilder, GlobSetBuilder}; use itertools::Itertools; -use rayon::prelude::*; use std::collections::BTreeSet; use std::iter::once; use std::path::{Path, PathBuf}; @@ -27,19 +26,22 @@ pub fn add_modified_file(file: PathBuf) { set.insert(file); } -pub fn execute_runs(ts: &Toolset) { - let mut mu = MODIFIED_FILES.lock().unwrap(); - let files = mu.take().unwrap_or_default(); +pub async fn execute_runs(ts: &Toolset) { + let files = { + let mut mu = MODIFIED_FILES.lock().unwrap(); + mu.take().unwrap_or_default() + }; if files.is_empty() { return; } - for (root, wf) in Config::get().watch_file_hooks().unwrap_or_default() { + let config = Config::get().await; + for (root, wf) in config.watch_file_hooks().unwrap_or_default() { match has_matching_files(&root, &wf, &files) { Ok(files) if files.is_empty() => { continue; } Ok(files) => { - if let Err(e) = execute(ts, &root, &wf.run, files) { + if let Err(e) = execute(ts, &root, &wf.run, files).await { warn!("error executing watch_file hook: {e}"); } } @@ -50,7 +52,7 @@ pub fn execute_runs(ts: &Toolset) { } } -fn execute(ts: &Toolset, root: &Path, run: &str, files: Vec<&PathBuf>) -> Result<()> { +async fn execute(ts: &Toolset, root: &Path, run: &str, files: Vec<&PathBuf>) -> Result<()> { SETTINGS.ensure_experimental("watch_file_hooks")?; let modified_files_var = files .iter() @@ -64,8 +66,8 @@ fn execute(ts: &Toolset, root: &Path, run: &str, files: Vec<&PathBuf>) -> Result .map(|s| s.as_str()) .chain(once(run)) .collect_vec(); - let config = Config::get(); - let mut env = ts.full_env(&config)?; + let config = Config::get().await; + let mut env = ts.full_env(&config).await?; env.insert("MISE_WATCH_FILES_MODIFIED".to_string(), modified_files_var); if let Some(cwd) = &*dirs::CWD { env.insert( @@ -125,7 +127,7 @@ pub fn glob(root: &Path, patterns: &[String]) -> Result> { ..Default::default() }; Ok(patterns - .par_iter() + .iter() .map(|pattern| root.join(pattern).to_string_lossy().to_string()) .filter_map(|pattern| glob::glob_with(&pattern, opts).ok()) .collect::>() diff --git a/xtasks/test/perf b/xtasks/test/perf index 958ca4c429..f64811af8a 100755 --- a/xtasks/test/perf +++ b/xtasks/test/perf @@ -21,14 +21,14 @@ if [ -v MISE_ALT ]; then which "$MISE_ALT" fi -recent_benchmarks["install-uncached"]=678 +recent_benchmarks["install-uncached"]=0 recent_benchmarks["install-cached"]=186 -recent_benchmarks["ls-uncached"]=372 -recent_benchmarks["ls-cached"]=57 -recent_benchmarks["bin-paths-uncached"]=490 -recent_benchmarks["bin-paths-cached"]=61 -recent_benchmarks["task-ls-uncached"]=10000 -recent_benchmarks["task-ls-cached"]=10000 +recent_benchmarks["ls-uncached"]=1003 +recent_benchmarks["ls-cached"]=0 +recent_benchmarks["bin-paths-uncached"]=1077 +recent_benchmarks["bin-paths-cached"]=0 +recent_benchmarks["task-ls-uncached"]=3255 +recent_benchmarks["task-ls-cached"]=313 for name in "${!recent_benchmarks[@]}"; do maximums["$name"]=$(("${recent_benchmarks["$name"]}" * 12 / 10)) @@ -46,7 +46,8 @@ benchmark_error() { if [ "$name" = "*-uncached" ]; then mise cache clear fi - CARGO_PROFILE_RELEASE_DEBUG=true timeout -v 120 \ + rm -rf cargo-flamegraph.trace + CARGO_PROFILE_RELEASE_DEBUG=true timeout -v 180 \ mise x cargo:flamegraph -- \ cargo flamegraph --verbose -o "flamegraphs/$1.svg" --title "$1" --notes "$error" -- $cmd \ >/dev/null || true @@ -95,13 +96,13 @@ check_maximum() { local uncached_duration="$2" local cached_duration="$3" local cmd="$4" - if [[ ${maximums["$name-uncached"]} -lt $uncached_duration ]]; then + if [[ ${maximums["$name-uncached"]} != 0 ]] && [[ ${maximums["$name-uncached"]} -lt $uncached_duration ]]; then benchmark_error "$name-uncached" "maximum for $name-uncached is ${maximums["$name-uncached"]}, got $uncached_duration" "$name" "$cmd" - elif [[ ${maximums["$name-cached"]} -lt $cached_duration ]]; then + elif [[ ${maximums["$name-cached"]} != 0 ]] && [[ ${maximums["$name-cached"]} -lt $cached_duration ]]; then benchmark_error "$name-cached" "maximum for $name-cached is ${maximums["$name-cached"]}, got $cached_duration" "$name" "$cmd" - elif [[ ${minimums["$name-uncached"]} -gt $uncached_duration ]]; then + elif [[ ${minimums["$name-uncached"]} != 0 ]] && [[ ${minimums["$name-uncached"]} -gt $uncached_duration ]]; then benchmark_error "$name-uncached" "(yay!) minimum for $name-uncached is ${minimums["$name-uncached"]}, got $uncached_duration" "$name" "$cmd" - elif [[ ${minimums["$name-cached"]} -gt $cached_duration ]]; then + elif [[ ${minimums["$name-cached"]} != 0 ]] && [[ ${minimums["$name-cached"]} -gt $cached_duration ]]; then benchmark_error "$name-cached" "(yay!) minimum for $name-cached is ${minimums["$name-cached"]}, got $cached_duration" "$name" "$cmd" fi }