diff --git a/docs/configuration.md b/docs/configuration.md index e93a97f565..c9de82e999 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -414,22 +414,27 @@ other developers to use a specific tool like mise or asdf. They support aliases, which means you can have an `.nvmrc` file with `lts/hydrogen` and it will work in mise and nvm. Here are some of the supported idiomatic version files: -| Plugin | Idiomatic Files | -| ---------- | ------------------------------------- | -| atmos | `.atmos-version` | -| crystal | `.crystal-version` | -| elixir | `.exenv-version` | -| go | `.go-version` | -| java | `.java-version`, `.sdkmanrc` | -| node | `.nvmrc`, `.node-version` | -| opentofu | `.opentofu-version` | -| packer | `.packer-version` | -| python | `.python-version`, `.python-versions` | -| ruby | `.ruby-version`, `Gemfile` | -| terraform | `.terraform-version`, `main.tf` | -| terragrunt | `.terragrunt-version` | -| terramate | `.terramate-version` | -| yarn | `.yvmrc` | +| Plugin | Idiomatic Files | +| ---------- | ----------------------------------------- | +| atmos | `.atmos-version` | +| bun | `.bun-version`, `package.json` | +| crystal | `.crystal-version` | +| deno | `.deno-version`, `package.json` | +| dotnet | `global.json` | +| elixir | `.exenv-version` | +| go | `.go-version` | +| java | `.java-version`, `.sdkmanrc` | +| node | `.nvmrc`, `.node-version`, `package.json` | +| npm | `package.json` | +| opentofu | `.opentofu-version` | +| packer | `.packer-version` | +| pnpm | `package.json` | +| python | `.python-version`, `.python-versions` | +| ruby | `.ruby-version`, `Gemfile` | +| terraform | `.terraform-version`, `main.tf` | +| terragrunt | `.terragrunt-version` | +| terramate | `.terramate-version` | +| yarn | `.yvmrc`, `package.json` | In mise, these are disabled by default, see for rationale. diff --git a/src/backend/asdf.rs b/src/backend/asdf.rs index aae077a13b..6f22d907e5 100644 --- a/src/backend/asdf.rs +++ b/src/backend/asdf.rs @@ -308,7 +308,7 @@ impl Backend for AsdfBackend { Ok(aliases) } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { if let Some(data) = &self.toml.list_idiomatic_filenames.data { return Ok(self.plugin.parse_idiomatic_filenames(data)); } @@ -326,9 +326,9 @@ impl Backend for AsdfBackend { .cloned() } - async fn parse_idiomatic_file(&self, idiomatic_file: &Path) -> Result { + async fn _parse_idiomatic_file(&self, idiomatic_file: &Path) -> Result> { if let Some(cached) = self.fetch_cached_idiomatic_file(idiomatic_file)? { - return Ok(cached); + return Ok(cached.split_whitespace().map(|s| s.to_string()).collect()); } trace!( "parsing idiomatic file: {}", @@ -343,7 +343,13 @@ impl Backend for AsdfBackend { let idiomatic_version = normalize_idiomatic_contents(&idiomatic_version); self.write_idiomatic_cache(idiomatic_file, &idiomatic_version)?; - Ok(idiomatic_version) + if idiomatic_version.is_empty() { + return Ok(vec![]); + } + Ok(idiomatic_version + .split_whitespace() + .map(|s| s.to_string()) + .collect()) } fn plugin(&self) -> Option<&PluginEnum> { diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 85e619da0e..378ae8d9fc 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -915,22 +915,57 @@ pub trait Backend: Debug + Send + Sync { fn get_aliases(&self) -> eyre::Result> { Ok(BTreeMap::new()) } + + /// Returns a list of idiomatic filenames for this tool. + /// + /// This method is additive: + /// 1. It calls `_idiomatic_filenames` to get backend-specific filenames. + /// 2. It checks the Registry for any additional filenames defined there. async fn idiomatic_filenames(&self) -> Result> { - Ok(REGISTRY - .get(self.id()) - .map(|rt| rt.idiomatic_files.iter().map(|s| s.to_string()).collect()) - .unwrap_or_default()) + let mut filenames = self._idiomatic_filenames().await?; + if let Some(rt) = REGISTRY.get(self.id()) { + filenames.extend(rt.idiomatic_files.iter().map(|s| s.to_string())); + } + filenames = filenames.into_iter().unique().collect(); + Ok(filenames) + } + + /// Backend-specific implementation for `idiomatic_filenames`. + /// Override this to provide native idiomatic filenames for the backend. + async fn _idiomatic_filenames(&self) -> Result> { + Ok(vec![]) } - async fn parse_idiomatic_file(&self, path: &Path) -> eyre::Result { + + /// Parses an idiomatic version file to extract the version. + /// + /// This handles special files like `package.json` which are parsed natively to avoid + /// every backend needing to implement `package.json` support. For other files, it + /// delegates to `_parse_idiomatic_file`. + async fn parse_idiomatic_file(&self, path: &Path) -> eyre::Result> { if path.file_name().is_some_and(|f| f == "package.json") { - let pkg = crate::package_json::PackageJson::parse(path)?; - return pkg - .package_manager_version(self.id()) - .ok_or_else(|| eyre::eyre!("no {} version found in package.json", self.id())); + return crate::config::config_file::idiomatic_version::package_json::parse( + path, + self.id(), + ); } + self._parse_idiomatic_file(path).await + } + + /// Backend-specific implementation for `parse_idiomatic_file`. + /// Default implementation reads the file and treats each whitespace-separated token as a version. + /// Override to provide format-specific parsing; return `Err` on real failures so the plugin is skipped. + async fn _parse_idiomatic_file(&self, path: &Path) -> eyre::Result> { let contents = file::read_to_string(path)?; - Ok(normalize_idiomatic_contents(&contents)) + let normalized = normalize_idiomatic_contents(&contents); + if normalized.is_empty() { + return Ok(vec![]); + } + Ok(normalized + .split_whitespace() + .map(|s| s.to_string()) + .collect()) } + fn plugin(&self) -> Option<&PluginEnum> { None } diff --git a/src/backend/vfox.rs b/src/backend/vfox.rs index fce2f0a6ef..de3c091721 100644 --- a/src/backend/vfox.rs +++ b/src/backend/vfox.rs @@ -171,16 +171,20 @@ impl Backend for VfoxBackend { Some(&self.plugin_enum) } - async fn parse_idiomatic_file(&self, path: &Path) -> eyre::Result { + async fn _idiomatic_filenames(&self) -> eyre::Result> { + let (vfox, _log_rx) = self.plugin.vfox(); + + let metadata = vfox.metadata(&self.pathname).await?; + Ok(metadata.legacy_filenames) + } + + async fn _parse_idiomatic_file(&self, path: &Path) -> eyre::Result> { let (vfox, _log_rx) = self.plugin.vfox(); let response = vfox.parse_legacy_file(&self.pathname, path).await?; - response.version.ok_or_else(|| { - eyre::eyre!( - "Version for {} not found in '{}'", - self.pathname, - path.display() - ) - }) + if let Some(version) = response.version { + return Ok(version.split_whitespace().map(|s| s.to_string()).collect()); + } + Ok(vec![]) } async fn get_tarball_url( diff --git a/src/config/config_file/idiomatic_version.rs b/src/config/config_file/idiomatic_version.rs deleted file mode 100644 index f3627dc11c..0000000000 --- a/src/config/config_file/idiomatic_version.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use eyre::Result; - -use crate::backend::{self, Backend, BackendList}; -use crate::cli::args::BackendArg; -use crate::config::config_file::ConfigFile; -use crate::toolset::{ToolRequest, ToolRequestSet, ToolSource}; - -use super::ConfigFileType; - -#[derive(Debug, Clone)] -pub struct IdiomaticVersionFile { - path: PathBuf, - tools: ToolRequestSet, -} - -impl IdiomaticVersionFile { - pub fn init(path: PathBuf) -> Self { - Self { - path, - tools: ToolRequestSet::new(), - } - } - - pub async fn parse(path: PathBuf, plugins: BackendList) -> Result { - let source = ToolSource::IdiomaticVersionFile(path.clone()); - let mut tools = ToolRequestSet::new(); - - for plugin in plugins { - let version = match plugin.parse_idiomatic_file(&path).await { - Ok(v) => v, - Err(e) => { - trace!("skipping {} for {}: {e}", plugin.id(), path.display()); - continue; - } - }; - for version in version.split_whitespace() { - let tr = ToolRequest::new(plugin.ba().clone(), version, source.clone())?; - tools.add_version(tr, &source); - } - } - - Ok(Self { tools, path }) - } - - pub async fn from_file(path: &Path) -> Result { - trace!("parsing idiomatic version: {}", path.display()); - let file_name = &path.file_name().unwrap().to_string_lossy().to_string(); - let mut tools: Vec> = vec![]; - for b in backend::list().into_iter() { - if b.idiomatic_filenames() - .await - .is_ok_and(|f| f.contains(file_name)) - { - tools.push(b); - } - } - Self::parse(path.to_path_buf(), tools).await - } -} - -impl ConfigFile for IdiomaticVersionFile { - fn config_type(&self) -> ConfigFileType { - ConfigFileType::IdiomaticVersion - } - - fn get_path(&self) -> &Path { - self.path.as_path() - } - - #[cfg_attr(coverage_nightly, coverage(off))] - fn remove_tool(&self, _fa: &BackendArg) -> Result<()> { - unimplemented!() - } - - #[cfg_attr(coverage_nightly, coverage(off))] - fn replace_versions( - &self, - _plugin_name: &BackendArg, - _versions: Vec, - ) -> Result<()> { - unimplemented!() - } - - #[cfg_attr(coverage_nightly, coverage(off))] - fn save(&self) -> Result<()> { - unimplemented!() - } - - #[cfg_attr(coverage_nightly, coverage(off))] - fn dump(&self) -> Result { - unimplemented!() - } - - fn source(&self) -> ToolSource { - ToolSource::IdiomaticVersionFile(self.path.clone()) - } - - fn to_tool_request_set(&self) -> Result { - Ok(self.tools.clone()) - } -} diff --git a/src/config/config_file/idiomatic_version/mod.rs b/src/config/config_file/idiomatic_version/mod.rs new file mode 100644 index 0000000000..1dcce1e8bb --- /dev/null +++ b/src/config/config_file/idiomatic_version/mod.rs @@ -0,0 +1,198 @@ +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use eyre::Result; + +use crate::backend::{self, Backend, BackendList}; +use crate::cli::args::BackendArg; +use crate::config::config_file::ConfigFile; +use crate::toolset::{ToolRequest, ToolRequestSet, ToolSource}; + +use super::ConfigFileType; + +pub mod package_json; + +#[derive(Debug, Clone)] +pub struct IdiomaticVersionFile { + path: PathBuf, + tools: ToolRequestSet, +} + +impl IdiomaticVersionFile { + pub fn init(path: PathBuf) -> Self { + Self { + path, + tools: ToolRequestSet::new(), + } + } + + pub async fn parse(path: PathBuf, plugins: BackendList) -> Result { + let source = ToolSource::IdiomaticVersionFile(path.clone()); + let mut tools = ToolRequestSet::new(); + + for plugin in plugins { + match plugin.parse_idiomatic_file(&path).await { + Ok(versions) => { + for v in versions { + let tr = ToolRequest::new(plugin.ba().clone(), &v, source.clone())?; + tools.add_version(tr, &source); + } + } + Err(e) => { + trace!("skipping {} for {}: {}", path.display(), plugin.id(), e); + continue; + } + } + } + + Ok(Self { tools, path }) + } + + pub async fn from_file(path: &Path) -> Result { + trace!("parsing idiomatic version: {}", path.display()); + let file_name = path.file_name().unwrap().to_string_lossy().to_string(); + let mut tools: Vec> = vec![]; + for b in backend::list().into_iter() { + if b.idiomatic_filenames() + .await + .is_ok_and(|f| f.contains(&file_name)) + { + tools.push(b); + } + } + Self::parse(path.to_path_buf(), tools).await + } +} + +impl ConfigFile for IdiomaticVersionFile { + fn config_type(&self) -> ConfigFileType { + ConfigFileType::IdiomaticVersion + } + + fn get_path(&self) -> &Path { + self.path.as_path() + } + + #[cfg_attr(coverage_nightly, coverage(off))] + fn remove_tool(&self, _fa: &BackendArg) -> Result<()> { + unimplemented!() + } + + #[cfg_attr(coverage_nightly, coverage(off))] + fn replace_versions( + &self, + _plugin_name: &BackendArg, + _versions: Vec, + ) -> Result<()> { + unimplemented!() + } + + #[cfg_attr(coverage_nightly, coverage(off))] + fn save(&self) -> Result<()> { + unimplemented!() + } + + #[cfg_attr(coverage_nightly, coverage(off))] + fn dump(&self) -> Result { + unimplemented!() + } + + fn source(&self) -> ToolSource { + ToolSource::IdiomaticVersionFile(self.path.clone()) + } + + fn to_tool_request_set(&self) -> Result { + Ok(self.tools.clone()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::backend::{Backend, VersionInfo}; + use crate::cli::args::{BackendArg, BackendResolution}; + use crate::config::Config; + use crate::install_context::InstallContext; + use crate::toolset::ToolVersion; + use async_trait::async_trait; + use std::sync::Arc; + + #[derive(Debug)] + struct MockBackend { + ba: Arc, + fail: bool, + version: Option, + } + + impl MockBackend { + fn new(short: &str, fail: bool, version: Option) -> Self { + let ba = BackendArg::new_raw( + short.to_string(), + None, + short.to_string(), + None, + BackendResolution::new(false), + ); + Self { + ba: Arc::new(ba), + fail, + version, + } + } + } + + #[async_trait] + impl Backend for MockBackend { + fn ba(&self) -> &Arc { + &self.ba + } + + async fn _list_remote_versions(&self, _config: &Arc) -> Result> { + Ok(vec![]) + } + + async fn install_version_( + &self, + _ctx: &InstallContext, + _tv: ToolVersion, + ) -> Result { + unimplemented!() + } + + async fn parse_idiomatic_file(&self, _path: &Path) -> Result> { + if self.fail { + eyre::bail!("mock error"); + } + if let Some(v) = &self.version { + Ok(vec![v.clone()]) + } else { + Ok(vec![]) + } + } + } + + #[tokio::test] + async fn test_idiomatic_parse_error_propagation() { + let _config = Config::get().await.unwrap(); + let path = PathBuf::from(".tool-versions"); + let backend1 = Arc::new(MockBackend::new("node", true, None)); + let backend2 = Arc::new(MockBackend::new( + "python", + false, + Some("3.10.0".to_string()), + )); + let plugins: BackendList = vec![backend1, backend2]; + + let result = IdiomaticVersionFile::parse(path, plugins).await; + + assert!(result.is_ok(), "Should not propagate error from backend1"); + + let file = result.unwrap(); + let trs = file.to_tool_request_set().unwrap(); + let tools: Vec<_> = trs.into_iter().collect(); + assert_eq!(tools.len(), 1); + let (ba, versions, _) = &tools[0]; + assert_eq!(ba.short, "python"); + assert_eq!(versions[0].version(), "3.10.0"); + } +} diff --git a/src/package_json.rs b/src/config/config_file/idiomatic_version/package_json.rs similarity index 72% rename from src/package_json.rs rename to src/config/config_file/idiomatic_version/package_json.rs index 61785f67d2..a5bd7ef310 100644 --- a/src/package_json.rs +++ b/src/config/config_file/idiomatic_version/package_json.rs @@ -1,14 +1,12 @@ -use std::path::Path; - +use crate::file; use eyre::Result; use serde::Deserialize; use serde::de::Deserializer; - -use crate::file; +use std::path::Path; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PackageJson { +struct PackageJsonData { dev_engines: Option, package_manager: Option, } @@ -50,15 +48,15 @@ where } } -impl PackageJson { - pub fn parse(path: &Path) -> Result { +impl PackageJsonData { + fn parse(path: &Path) -> Result { let contents = file::read_to_string(path)?; - let pkg: PackageJson = serde_json::from_str(&contents)?; + let pkg: PackageJsonData = serde_json::from_str(&contents)?; Ok(pkg) } - /// Extract a runtime version for the given tool name from devEngines.runtime - pub fn runtime_version(&self, tool_name: &str) -> Option { + /// Extract a runtime version for the given tool name. + fn runtime_version(&self, tool_name: &str) -> Option { self.dev_engines .as_ref() .and_then(|de| de.runtime.as_ref()) @@ -70,7 +68,7 @@ impl PackageJson { /// Extract a package manager version for the given tool name. /// Checks devEngines.packageManager first, then falls back to the packageManager field. - pub fn package_manager_version(&self, tool_name: &str) -> Option { + fn package_manager_version(&self, tool_name: &str) -> Option { // Try devEngines.packageManager first self.dev_engines .as_ref() @@ -107,7 +105,7 @@ impl PackageJson { /// This doesn't handle all edge cases correctly. For example, `^20.0.1` should not /// match `20.0.0`, but our simplified approach strips it to `20` which would match. /// Full semver range support may be added in the future. -pub fn simplify_semver(input: &str) -> String { +fn simplify_semver(input: &str) -> String { let input = input.trim(); if input == "*" || input == "x" { return "latest".to_string(); @@ -161,9 +159,29 @@ pub fn simplify_semver(input: &str) -> String { } } +pub fn parse(path: &Path, tool_name: &str) -> Result> { + let pkg = PackageJsonData::parse(path)?; + // We ignore unknown tools in package.json + let v = match tool_name { + "node" | "deno" => pkg.runtime_version(tool_name), + "bun" => pkg + .runtime_version(tool_name) + .or_else(|| pkg.package_manager_version(tool_name)), + "npm" | "yarn" | "pnpm" => pkg.package_manager_version(tool_name), + _ => None, + }; + if let Some(v) = v { + Ok(vec![v]) + } else { + Ok(vec![]) + } +} + #[cfg(test)] mod tests { use super::*; + use std::fs; + use tempfile::tempdir; #[test] fn test_simplify_semver() { @@ -181,6 +199,47 @@ mod tests { assert_eq!(simplify_semver("=18.0.0"), "18"); } + #[test] + fn test_parse_package_json() { + let dir = tempdir().unwrap(); + let path = dir.path().join("package.json"); + fs::write( + &path, + r#"{ + "devEngines": { + "packageManager": { + "name": "yarn", + "version": "1.22.19" + }, + "runtime": { + "name": "node", + "version": "20.0.0" + } + } + }"#, + ) + .unwrap(); + + assert_eq!(parse(&path, "yarn").unwrap(), vec!["1.22.19".to_string()]); + assert_eq!(parse(&path, "node").unwrap(), vec!["20.0.0".to_string()]); + } + + #[test] + fn test_bun_logic() { + let dir = tempdir().unwrap(); + let path = dir.path().join("package.json"); + fs::write( + &path, + r#"{ + "packageManager": "bun@1.0.0" + }"#, + ) + .unwrap(); + + assert_eq!(parse(&path, "bun").unwrap(), vec!["1.0.0".to_string()]); + assert_eq!(parse(&path, "node").unwrap(), Vec::::new()); + } + #[test] fn test_simplify_semver_upper_bound() { assert_eq!(simplify_semver("<18.0.0"), ""); @@ -197,7 +256,7 @@ mod tests { #[test] fn test_runtime_version() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "devEngines": { "runtime": { @@ -214,7 +273,7 @@ mod tests { #[test] fn test_runtime_version_bun() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "devEngines": { "runtime": { @@ -231,7 +290,7 @@ mod tests { #[test] fn test_runtime_version_array_form() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "devEngines": { "runtime": [ @@ -247,7 +306,7 @@ mod tests { #[test] fn test_runtime_version_missing_name() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "devEngines": { "runtime": { @@ -262,7 +321,7 @@ mod tests { #[test] fn test_package_manager_version_dev_engines() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "devEngines": { "packageManager": { @@ -279,7 +338,7 @@ mod tests { #[test] fn test_package_manager_version_field() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "packageManager": "pnpm@9.1.0+sha256.abcdef" }"#, @@ -294,7 +353,7 @@ mod tests { #[test] fn test_package_manager_version_no_hash() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "packageManager": "yarn@4.1.0" }"#, @@ -308,7 +367,7 @@ mod tests { #[test] fn test_dev_engines_overrides_package_manager_field() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "devEngines": { "packageManager": { @@ -325,14 +384,14 @@ mod tests { #[test] fn test_missing_fields() { - let pkg: PackageJson = serde_json::from_str(r#"{}"#).unwrap(); + let pkg: PackageJsonData = serde_json::from_str(r#"{}"#).unwrap(); assert_eq!(pkg.runtime_version("node"), None); assert_eq!(pkg.package_manager_version("pnpm"), None); } #[test] fn test_empty_dev_engines() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "devEngines": {} }"#, @@ -344,7 +403,7 @@ mod tests { #[test] fn test_bun_as_package_manager() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "packageManager": "bun@1.2.0" }"#, @@ -359,7 +418,7 @@ mod tests { #[test] fn test_deno_dev_engines() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "devEngines": { "runtime": { @@ -372,4 +431,40 @@ mod tests { .unwrap(); assert_eq!(pkg.runtime_version("deno"), Some("1.40.0".to_string())); } + + #[test] + fn test_engines_field_ignored() { + let pkg: PackageJsonData = serde_json::from_str( + r#"{ + "engines": { + "node": ">=18.0.0", + "pnpm": "9.0.0" + } + }"#, + ) + .unwrap(); + // Should ignore engines field + assert_eq!(pkg.runtime_version("node"), None); + assert_eq!(pkg.package_manager_version("pnpm"), None); + } + + #[test] + fn test_engines_field_does_not_interfere() { + let pkg: PackageJsonData = serde_json::from_str( + r#"{ + "devEngines": { + "runtime": { + "name": "node", + "version": "20.0.0" + } + }, + "engines": { + "node": "18.0.0" + } + }"#, + ) + .unwrap(); + // Should ignore engines and pick devEngines + assert_eq!(pkg.runtime_version("node"), Some("20.0.0".to_string())); + } } diff --git a/src/config/config_file/mod.rs b/src/config/config_file/mod.rs index 72db6bc941..bdd8c9303f 100644 --- a/src/config/config_file/mod.rs +++ b/src/config/config_file/mod.rs @@ -517,14 +517,9 @@ fn trust_file_hash(path: &Path) -> eyre::Result { async fn filename_is_idiomatic(file_name: String) -> bool { for b in backend::list() { match b.idiomatic_filenames().await { - Ok(filenames) => { - if filenames.contains(&file_name) { - return true; - } - } - Err(e) => { - debug!("idiomatic_filenames failed for {}: {:?}", b, e); - } + Ok(filenames) if filenames.contains(&file_name) => return true, + Err(e) => debug!("idiomatic_filenames failed for {}: {:?}", b, e), + _ => {} } } false diff --git a/src/config/mod.rs b/src/config/mod.rs index 7f8f4e199f..2e797e1679 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -125,7 +125,7 @@ impl Config { pub async fn load() -> Result> { backend::load_tools().await?; let idiomatic_files = measure!("config::load idiomatic_files", { - load_idiomatic_files().await + load_idiomatic_filenames().await }); let config_filenames = idiomatic_files .keys() @@ -898,7 +898,7 @@ fn find_monorepo_config(config_files: &ConfigMap) -> Option<&Arc .find(|cf| cf.experimental_monorepo_root() == Some(true)) } -async fn load_idiomatic_files() -> BTreeMap> { +async fn load_idiomatic_filenames() -> BTreeMap> { let enable_tools = Settings::get().idiomatic_version_file_enable_tools.clone(); if enable_tools.is_empty() { return BTreeMap::new(); @@ -1434,7 +1434,7 @@ async fn load_all_config_files( /// Load config files from a list of paths (for monorepo task config contexts) pub async fn load_config_files_from_paths(config_paths: &[PathBuf]) -> Result { backend::load_tools().await?; - let idiomatic_filenames = BTreeMap::new(); // TODO: support idiomatic files in config hierarchy loading + let idiomatic_filenames = load_idiomatic_filenames().await; let mut config_map = ConfigMap::default(); for f in config_paths.iter().unique() { diff --git a/src/main.rs b/src/main.rs index 811c7a95b3..736bbb3ec7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -64,7 +64,6 @@ pub(crate) mod maplit; mod migrate; mod minisign; mod netrc; -mod package_json; pub(crate) mod parallel; mod path; mod path_env; diff --git a/src/plugins/core/bun.rs b/src/plugins/core/bun.rs index 0fca54df3e..edbbab1820 100644 --- a/src/plugins/core/bun.rs +++ b/src/plugins/core/bun.rs @@ -140,22 +140,10 @@ impl Backend for BunPlugin { Ok(versions) } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { Ok(vec![".bun-version".into(), "package.json".into()]) } - async fn parse_idiomatic_file(&self, path: &Path) -> Result { - if path.file_name().is_some_and(|f| f == "package.json") { - let pkg = crate::package_json::PackageJson::parse(path)?; - return pkg - .runtime_version("bun") - .or_else(|| pkg.package_manager_version("bun")) - .ok_or_else(|| eyre::eyre!("no bun version found in package.json")); - } - let contents = file::read_to_string(path)?; - Ok(contents.trim().to_string()) - } - async fn install_version_( &self, ctx: &InstallContext, diff --git a/src/plugins/core/deno.rs b/src/plugins/core/deno.rs index 6b6e610ada..d908c0840c 100644 --- a/src/plugins/core/deno.rs +++ b/src/plugins/core/deno.rs @@ -119,21 +119,10 @@ impl Backend for DenoPlugin { Ok(versions) } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { Ok(vec![".deno-version".into(), "package.json".into()]) } - async fn parse_idiomatic_file(&self, path: &Path) -> Result { - if path.file_name().is_some_and(|f| f == "package.json") { - let pkg = crate::package_json::PackageJson::parse(path)?; - return pkg - .runtime_version("deno") - .ok_or_else(|| eyre::eyre!("no deno version found in package.json")); - } - let body = file::read_to_string(path)?; - Ok(body.trim().to_string()) - } - async fn install_version_( &self, ctx: &InstallContext, diff --git a/src/plugins/core/dotnet.rs b/src/plugins/core/dotnet.rs index bfade63849..e5e8b183ab 100644 --- a/src/plugins/core/dotnet.rs +++ b/src/plugins/core/dotnet.rs @@ -95,17 +95,20 @@ impl Backend for DotnetPlugin { .collect()) } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { Ok(vec!["global.json".into()]) } - async fn parse_idiomatic_file(&self, path: &Path) -> Result { + async fn _parse_idiomatic_file(&self, path: &Path) -> Result> { let content = file::read_to_string(path)?; let global_json: GlobalJson = serde_json::from_str(&content)?; let sdk = global_json .sdk .ok_or_else(|| eyre::eyre!("no sdk.version found in {}", path.display()))?; - Ok(sdk.version) + if sdk.version.is_empty() { + return Ok(vec![]); + } + Ok(vec![sdk.version]) } async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { diff --git a/src/plugins/core/elixir.rs b/src/plugins/core/elixir.rs index f9e1a1ac4c..a84766ba8a 100644 --- a/src/plugins/core/elixir.rs +++ b/src/plugins/core/elixir.rs @@ -126,7 +126,7 @@ impl Backend for ElixirPlugin { Ok(versions) } - async fn idiomatic_filenames(&self) -> eyre::Result> { + async fn _idiomatic_filenames(&self) -> eyre::Result> { Ok(vec![".exenv-version".into()]) } diff --git a/src/plugins/core/go.rs b/src/plugins/core/go.rs index bad1f004cd..06242302d7 100644 --- a/src/plugins/core/go.rs +++ b/src/plugins/core/go.rs @@ -271,7 +271,7 @@ impl Backend for GoPlugin { }; Ok(versions) } - async fn idiomatic_filenames(&self) -> eyre::Result> { + async fn _idiomatic_filenames(&self) -> eyre::Result> { Ok(vec![".go-version".into()]) } diff --git a/src/plugins/core/java.rs b/src/plugins/core/java.rs index d8faec16cb..04151dfba6 100644 --- a/src/plugins/core/java.rs +++ b/src/plugins/core/java.rs @@ -387,11 +387,11 @@ impl Backend for JavaPlugin { Ok(aliases) } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { Ok(vec![".java-version".into(), ".sdkmanrc".into()]) } - async fn parse_idiomatic_file(&self, path: &Path) -> Result { + async fn _parse_idiomatic_file(&self, path: &Path) -> Result> { let contents = file::read_to_string(path)?; if path.file_name() == Some(".sdkmanrc".as_ref()) { let version = contents @@ -402,7 +402,7 @@ impl Backend for JavaPlugin { .unwrap_or_default() .1; if !version.contains('-') { - return Ok(version.to_string()); + return Ok(vec![version.to_string()]); } let (version, vendor) = version.rsplit_once('-').unwrap_or_default(); let vendor = match vendor { @@ -422,9 +422,12 @@ impl Backend for JavaPlugin { if vendor == "zulu" { version = version.split_once('.').unwrap_or_default().0; } - Ok(format!("{vendor}-{version}")) + Ok(vec![format!("{vendor}-{version}")]) } else { - Ok(normalize_idiomatic_contents(&contents)) + Ok(normalize_idiomatic_contents(&contents) + .lines() + .map(|s| s.to_string()) + .collect()) } } diff --git a/src/plugins/core/node.rs b/src/plugins/core/node.rs index 84c3c42174..2e3b4ed313 100644 --- a/src/plugins/core/node.rs +++ b/src/plugins/core/node.rs @@ -562,7 +562,7 @@ impl Backend for NodePlugin { Ok(aliases) } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { Ok(vec![ ".node-version".into(), ".nvmrc".into(), @@ -570,19 +570,19 @@ impl Backend for NodePlugin { ]) } - async fn parse_idiomatic_file(&self, path: &Path) -> Result { - if path.file_name().is_some_and(|f| f == "package.json") { - let pkg = crate::package_json::PackageJson::parse(path)?; - return pkg - .runtime_version("node") - .ok_or_else(|| eyre::eyre!("no node version found in package.json")); - } - let body = normalize_idiomatic_contents(&file::read_to_string(path)?); - // trim "v" prefix - let body = body.trim().strip_prefix('v').unwrap_or(&body); - // replace lts/* with lts - let body = body.replace("lts/*", "lts"); - Ok(body.to_string()) + async fn _parse_idiomatic_file(&self, path: &Path) -> Result> { + let contents = file::read_to_string(path)?; + let body = normalize_idiomatic_contents(&contents); + + let versions = body + .lines() + .map(|line| { + let mut version = line.trim().strip_prefix('v').unwrap_or(line).to_string(); + version = version.replace("lts/*", "lts"); + version + }) + .collect(); + Ok(versions) } async fn install_version_( diff --git a/src/plugins/core/python.rs b/src/plugins/core/python.rs index b8e54ffdf2..20033be8e1 100644 --- a/src/plugins/core/python.rs +++ b/src/plugins/core/python.rs @@ -572,7 +572,7 @@ impl Backend for PythonPlugin { } } - async fn idiomatic_filenames(&self) -> eyre::Result> { + async fn _idiomatic_filenames(&self) -> eyre::Result> { Ok(vec![ ".python-version".to_string(), ".python-versions".to_string(), diff --git a/src/plugins/core/ruby.rs b/src/plugins/core/ruby.rs index c123269c1a..31057924fc 100644 --- a/src/plugins/core/ruby.rs +++ b/src/plugins/core/ruby.rs @@ -774,11 +774,11 @@ impl Backend for RubyPlugin { .await } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { Ok(vec![".ruby-version".into(), "Gemfile".into()]) } - async fn parse_idiomatic_file(&self, path: &Path) -> Result { + async fn _parse_idiomatic_file(&self, path: &Path) -> Result> { let v = match path.file_name() { Some(name) if name == "Gemfile" => parse_gemfile(&file::read_to_string(path)?), _ => { @@ -790,7 +790,10 @@ impl Backend for RubyPlugin { .to_string() } }; - Ok(v) + if v.is_empty() { + return Ok(vec![]); + } + Ok(vec![v]) } async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { diff --git a/src/plugins/core/ruby_windows.rs b/src/plugins/core/ruby_windows.rs index d74a56bec2..967d528d71 100644 --- a/src/plugins/core/ruby_windows.rs +++ b/src/plugins/core/ruby_windows.rs @@ -178,11 +178,11 @@ impl Backend for RubyPlugin { Ok(versions) } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { Ok(vec![".ruby-version".into(), "Gemfile".into()]) } - async fn parse_idiomatic_file(&self, path: &Path) -> Result { + async fn _parse_idiomatic_file(&self, path: &Path) -> Result> { let v = match path.file_name() { Some(name) if name == "Gemfile" => parse_gemfile(&file::read_to_string(path)?), _ => { @@ -194,7 +194,10 @@ impl Backend for RubyPlugin { .to_string() } }; - Ok(v) + if v.is_empty() { + return Ok(vec![]); + } + Ok(vec![v]) } async fn install_version_( diff --git a/src/plugins/core/rust.rs b/src/plugins/core/rust.rs index 4d55e50c42..77d92f4c81 100644 --- a/src/plugins/core/rust.rs +++ b/src/plugins/core/rust.rs @@ -116,13 +116,16 @@ impl Backend for RustPlugin { Ok(versions) } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { Ok(vec!["rust-toolchain.toml".into()]) } - async fn parse_idiomatic_file(&self, path: &Path) -> Result { + async fn _parse_idiomatic_file(&self, path: &Path) -> Result> { let rt = parse_idiomatic_file(path)?; - Ok(rt.channel) + if rt.channel.is_empty() { + return Ok(vec![]); + } + Ok(vec![rt.channel]) } async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { diff --git a/src/plugins/core/swift.rs b/src/plugins/core/swift.rs index 8fcf7478ef..717b1ccce2 100644 --- a/src/plugins/core/swift.rs +++ b/src/plugins/core/swift.rs @@ -193,7 +193,7 @@ impl Backend for SwiftPlugin { Ok(versions) } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { if Settings::get().experimental { Ok(vec![".swift-version".into()]) } else { diff --git a/src/plugins/core/zig.rs b/src/plugins/core/zig.rs index c6bcafce93..c0c9bf0c49 100644 --- a/src/plugins/core/zig.rs +++ b/src/plugins/core/zig.rs @@ -300,7 +300,7 @@ impl Backend for ZigPlugin { } } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { Ok(vec![".zig-version".into()]) }