diff --git a/docs/dev-tools/backends/dotnet.md b/docs/dev-tools/backends/dotnet.md index 6302d4ebcc..4d47587e6f 100644 --- a/docs/dev-tools/backends/dotnet.md +++ b/docs/dev-tools/backends/dotnet.md @@ -68,3 +68,14 @@ import Settings from '/components/settings.vue'; The following [tool-options](/dev-tools/#tool-options) are available for the `dotnet` backend—these go in `[tools]` in `mise.toml`. + +### `prerelease` + +By default, NuGet pre-release versions are excluded from `mise ls-remote` and from `latest` resolution. Set `prerelease = true` to include them: + +```toml +[tools] +"dotnet:GitVersion.Tool" = { version = "latest", prerelease = true } +``` + +The legacy `dotnet.package_flags = ["prerelease"]` setting is deprecated. Prefer the per-tool `prerelease = true` option, or the global `prereleases` setting when every tool should include pre-release versions. Because `dotnet.package_flags` is global, remove it before relying on `prerelease = false` per-tool opt-outs. diff --git a/e2e/backend/test_dotnet b/e2e/backend/test_dotnet index 27ece3a9b3..f32dfd4940 100644 --- a/e2e/backend/test_dotnet +++ b/e2e/backend/test_dotnet @@ -15,9 +15,11 @@ assert_not_contains "mise ls-remote dotnet:GitVersion.Tool" "-beta" test dotnet:GitVersion.Tool@5.12.0 "dotnet-gitversion /version" "5.12.0+Branch.support-5.x.Sha.3f75764963eb3d7956dcd5a40488c074dd9faf9e" test dotnet:Husky@0.7.2 "husky --version" "v0.7.2" -# This command is needed if you want to reexcute the ls-remote command +# This command is needed if you want to reexecute the ls-remote command mise cache clear -export MISE_DOTNET_PACKAGE_FLAGS="prerelease" +assert_contains "mise ls-remote 'dotnet:GitVersion.Tool[prerelease=true]'" "-beta" -assert_contains "mise ls-remote dotnet:GitVersion.Tool" "-beta" +mise cache clear + +assert_contains "MISE_DOTNET_PACKAGE_FLAGS=prerelease mise ls-remote dotnet:GitVersion.Tool" "-beta" diff --git a/schema/mise.json b/schema/mise.json index 2467e792f6..aefce881e7 100644 --- a/schema/mise.json +++ b/schema/mise.json @@ -710,8 +710,9 @@ }, "package_flags": { "default": [], - "description": "Extends dotnet search and install abilities.", + "description": "[deprecated] Extends dotnet search and install abilities.", "type": "array", + "deprecated": true, "items": { "type": "string" } diff --git a/settings.toml b/settings.toml index a4c121fd1e..6fd1edd459 100644 --- a/settings.toml +++ b/settings.toml @@ -400,13 +400,19 @@ type = "Bool" [dotnet.package_flags] default = [] -description = "Extends dotnet search and install abilities." +deprecated = "Use the `prerelease = true` tool option or the global `prereleases` setting instead." +deprecated_remove_at = "2027.11.0" +deprecated_warn_at = "2026.11.0" +description = "[deprecated] Extends dotnet search and install abilities." docs = """ -This is a list of flags to extend the search and install abilities of dotnet tools. +Deprecated. Use the `prerelease = true` tool option on a `dotnet:` tool, or the global +`prereleases` setting, to include pre-release versions. -Here are the available flags: +This legacy setting is a list of flags to extend the search and install abilities of dotnet tools. +Because it is global, remove it before relying on `prerelease = false` per-tool opt-outs. +The only supported flag is: -- 'prerelease' : include prerelease versions in search and install +- 'prerelease' : include pre-release versions in search and install """ env = "MISE_DOTNET_PACKAGE_FLAGS" parse_env = "list_by_comma" @@ -1636,7 +1642,7 @@ description = "Include pre-release versions in `ls-remote`, `latest` resolution, docs = """ By default, releases flagged `prerelease: true` on GitHub are excluded from `mise ls-remote`, `latest` resolution, and fuzzy version matching. Per-tool opt-in is available via the -`prerelease = true` tool option (currently honored by the `github:` and `aqua:` backends). +`prerelease = true` tool option (currently honored by the `github:`, `aqua:`, and `dotnet:` backends). Set `prereleases = true` (or `MISE_PRERELEASES=1`) to opt in globally for every tool — equivalent to setting `prerelease = true` on each one. Useful for build pipelines that diff --git a/src/backend/aqua.rs b/src/backend/aqua.rs index 73fcb43966..1dae254a0d 100644 --- a/src/backend/aqua.rs +++ b/src/backend/aqua.rs @@ -289,7 +289,11 @@ impl Backend for AquaBackend { Ok(versions) } - async fn latest_stable_version(&self, _config: &Arc) -> Result> { + async fn latest_stable_version(&self, config: &Arc) -> Result> { + let opts = config.get_tool_opts_with_overrides(&self.ba).await?; + if self.include_prereleases(&opts) { + return Ok(None); + } self.latest_marked_release_version().await } diff --git a/src/backend/conda.rs b/src/backend/conda.rs index 2751572c45..505b97d384 100644 --- a/src/backend/conda.rs +++ b/src/backend/conda.rs @@ -1,8 +1,6 @@ use crate::backend::backend_type::BackendType; use crate::backend::platform_target::PlatformTarget; -use crate::backend::{ - VersionInfo, filter_cached_prereleases, include_prereleases, mark_prerelease, -}; +use crate::backend::{VersionInfo, filter_cached_prereleases, mark_prerelease}; use crate::cli::args::BackendArg; use crate::config::Config; use crate::config::Settings; @@ -676,7 +674,7 @@ impl Backend for CondaBackend { _refresh: bool, ) -> Result> { let opts = config.get_tool_opts_with_overrides(&self.ba).await?; - let want_prereleases = include_prereleases(&opts); + let want_prereleases = self.include_prereleases(&opts); let versions = self ._list_remote_versions(config) .await? diff --git a/src/backend/dotnet.rs b/src/backend/dotnet.rs index 6ed1858d0b..74ca34c94f 100644 --- a/src/backend/dotnet.rs +++ b/src/backend/dotnet.rs @@ -1,9 +1,7 @@ use std::sync::Arc; +use crate::backend::VersionInfo; use crate::backend::backend_type::BackendType; -use crate::backend::{ - VersionInfo, filter_cached_prereleases, include_prereleases, mark_prerelease, -}; use crate::cli::args::BackendArg; use crate::cmd::CmdLineRunner; use crate::config::Settings; @@ -12,7 +10,6 @@ use crate::toolset::ToolVersionOptions; use crate::{backend::Backend, config::Config}; use async_trait::async_trait; use eyre::eyre; -use jiff::Timestamp; /// Dotnet backend requires experimental mode to be enabled pub const EXPERIMENTAL: bool = true; @@ -40,24 +37,15 @@ impl Backend for DotnetBackend { true } - fn remote_version_listing_tool_option_keys(&self) -> &'static [&'static str] { - // TODO: Once dotnet remote listing always fetches the prerelease - // superset and filters at read time, remove this override entirely. - // Today `prerelease` changes the NuGet query, but there are no dotnet - // backend registry tools using the versions host. - &[] - } - - async fn _list_remote_versions(&self, config: &Arc) -> eyre::Result> { + async fn _list_remote_versions(&self, _config: &Arc) -> eyre::Result> { let feed_url = self.get_search_url().await?; - let opts = self.tool_opts(config).await?; let feed: NugetFeedSearch = HTTP_FETCH .json(format!( "{}?q={}&packageType=dotnettool&take=1&prerelease={}", feed_url, &self.tool_name(), - self.dotnet_prereleases_enabled(&opts) + true )) .await?; @@ -82,53 +70,6 @@ impl Backend for DotnetBackend { .collect()) } - /// Bypass the shared remote-versions cache because the dotnet package flags - /// affect which versions NuGet returns. The override is on `_with_refresh` - /// so install-time latest resolution uses the same dotnet-specific - /// prerelease filtering as `ls-remote`. - async fn list_remote_versions_with_info_with_refresh( - &self, - config: &Arc, - _refresh: bool, - ) -> eyre::Result> { - let opts = self.tool_opts(config).await?; - let want_prereleases = self.dotnet_prereleases_enabled(&opts); - let versions = self - ._list_remote_versions(config) - .await? - .into_iter() - .map(mark_prerelease) - .collect(); - Ok(filter_cached_prereleases(versions, want_prereleases)) - } - - async fn list_versions_matching_with_opts( - &self, - config: &Arc, - query: &str, - before_date: Option, - refresh: bool, - ) -> eyre::Result> { - let versions = match before_date { - Some(before) => { - let versions_with_info = self - .list_remote_versions_with_info_with_refresh(config, refresh) - .await?; - VersionInfo::filter_by_date(versions_with_info, before) - .into_iter() - .map(|v| v.version) - .collect() - } - None => { - self.list_remote_versions_with_refresh(config, refresh) - .await? - } - }; - let opts = self.tool_opts(config).await?; - let filter = !self.dotnet_prereleases_enabled(&opts); - Ok(self.fuzzy_match_filter(versions, query, filter)) - } - async fn install_version_( &self, ctx: &crate::install_context::InstallContext, @@ -164,6 +105,15 @@ impl Backend for DotnetBackend { Ok(tv) } + + fn include_prereleases(&self, opts: &ToolVersionOptions) -> bool { + if Settings::get().prereleases { + return true; + } + + opts.opts.get("prerelease").is_some_and(tool_option_bool) + || dotnet_legacy_prerelease_package_flag_enabled() + } } impl DotnetBackend { @@ -191,18 +141,30 @@ impl DotnetBackend { Ok(feed.id.clone()) } +} - async fn tool_opts(&self, config: &Arc) -> eyre::Result { - config.get_tool_opts_with_overrides(&self.ba).await +fn dotnet_legacy_prerelease_package_flag_enabled() -> bool { + let enabled = Settings::get() + .dotnet + .package_flags + .iter() + .any(|flag| flag == "prerelease"); + if enabled { + deprecated_at!( + "2026.11.0", + "2027.11.0", + "setting.dotnet.package_flags.prerelease", + "`dotnet.package_flags = [\"prerelease\"]` is deprecated. Use the `prerelease = true` tool option instead." + ); } + enabled +} - fn dotnet_prereleases_enabled(&self, opts: &ToolVersionOptions) -> bool { - Settings::get().prereleases - || Settings::get() - .dotnet - .package_flags - .contains(&"prerelease".to_string()) - || include_prereleases(opts) +fn tool_option_bool(value: &toml::Value) -> bool { + match value { + toml::Value::Boolean(b) => *b, + toml::Value::String(s) => s.parse::().unwrap_or(false), + _ => false, } } diff --git a/src/backend/github.rs b/src/backend/github.rs index 76f7e4fa2f..44b6b4d925 100644 --- a/src/backend/github.rs +++ b/src/backend/github.rs @@ -15,7 +15,7 @@ use crate::install_context::InstallContext; use crate::lockfile::{PlatformInfo, ProvenanceType}; use crate::toolset::ToolVersionOptions; use crate::toolset::{ToolRequest, ToolVersion}; -use crate::{backend::Backend, backend::include_prereleases, forgejo, github, gitlab}; +use crate::{backend::Backend, forgejo, github, gitlab}; use async_trait::async_trait; use eyre::Result; use regex::Regex; @@ -274,7 +274,7 @@ impl Backend for UnifiedGitBackend { // defaulting to the newest non-prerelease). Returning `None` lets the // trait's `latest_version` fall through to `latest_version_for_query`, // which resolves against the full list — now including pre-releases. - if include_prereleases(&opts) { + if self.include_prereleases(&opts) { return Ok(None); } diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 2cb0242e1e..78c41e1079 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -640,29 +640,30 @@ mod tests { fn test_include_prereleases_accepts_bool_and_string_values() { use crate::toolset::ToolVersionOptions; + let backend = TestBackend::default(); let mut opts = ToolVersionOptions::default(); - assert!(!include_prereleases(&opts)); + assert!(!backend.include_prereleases(&opts)); // Inline backend args normalize scalars to strings — cover that shape. opts.opts .insert("prerelease".to_string(), toml::Value::String("true".into())); - assert!(include_prereleases(&opts)); + assert!(backend.include_prereleases(&opts)); opts.opts.insert( "prerelease".to_string(), toml::Value::String("false".into()), ); - assert!(!include_prereleases(&opts)); + assert!(!backend.include_prereleases(&opts)); // Defense-in-depth: also accept a native TOML boolean, in case a future // config path stores the value without string normalization. opts.opts .insert("prerelease".to_string(), toml::Value::Boolean(true)); - assert!(include_prereleases(&opts)); + assert!(backend.include_prereleases(&opts)); opts.opts .insert("prerelease".to_string(), toml::Value::Boolean(false)); - assert!(!include_prereleases(&opts)); + assert!(!backend.include_prereleases(&opts)); } #[test] @@ -671,9 +672,10 @@ mod tests { use crate::toolset::ToolVersionOptions; use confique::Layer; + let backend = TestBackend::default(); let opts = ToolVersionOptions::default(); // Sanity: with no per-tool opt and no setting, prereleases stay filtered. - assert!(!include_prereleases(&opts)); + assert!(!backend.include_prereleases(&opts)); // Flipping the global setting takes effect without any per-tool config — // this is the path `MISE_PRERELEASES=1` and `mise ls-remote --prerelease` @@ -681,10 +683,45 @@ mod tests { let mut partial = SettingsPartial::empty(); partial.prereleases = Some(true); Settings::reset(Some(partial)); - let res = include_prereleases(&opts); + let res = backend.include_prereleases(&opts); Settings::reset(None); assert!(res); } + + #[derive(Debug)] + struct TestBackend { + ba: Arc, + } + + impl Default for TestBackend { + fn default() -> Self { + Self { + ba: Arc::new("test".into()), + } + } + } + + #[async_trait] + impl Backend for TestBackend { + fn ba(&self) -> &Arc { + &self.ba + } + + async fn _list_remote_versions( + &self, + _config: &Arc, + ) -> eyre::Result> { + Ok(vec![]) + } + + async fn install_version_( + &self, + _ctx: &InstallContext, + tv: ToolVersion, + ) -> Result { + Ok(tv) + } + } } #[async_trait] @@ -768,6 +805,21 @@ pub trait Backend: Debug + Send + Sync { false } + /// Whether pre-release versions should be included for this backend and + /// current tool options. Backends can override this only for compatibility + /// with deprecated backend-specific prerelease settings. + fn include_prereleases(&self, opts: &crate::toolset::ToolVersionOptions) -> bool { + if Settings::get().prereleases { + return true; + } + + if let Some(value) = opts.opts.get("prerelease") { + return tool_option_bool(value); + } + + false + } + /// Tool option keys whose non-registry overrides change the backend's /// remote version list. When any of these keys come from a backend alias, /// config, or inline backend arg, the versions host must be skipped because @@ -949,7 +1001,7 @@ pub trait Backend: Debug + Send + Sync { // that honor `prerelease`. When the current opts don't opt in, drop // entries with `prerelease = true` before returning so flipping the // tool option takes effect without invalidating the cache. - let want_prereleases = include_prereleases(opts); + let want_prereleases = self.include_prereleases(opts); if Settings::get().offline() { trace!( @@ -1185,7 +1237,7 @@ pub trait Backend: Debug + Send + Sync { let versions = self.list_installed_versions(); // No async config lookup available here; fall back to inline/registry // opts, which is the best we have for a sync path. - let filter = !include_prereleases(&self.ba().opts()); + let filter = !self.include_prereleases(&self.ba().opts()); self.fuzzy_match_filter(versions, query, filter) } async fn list_versions_matching( @@ -1195,7 +1247,7 @@ pub trait Backend: Debug + Send + Sync { ) -> eyre::Result> { let versions = self.list_remote_versions(config).await?; let opts = config.get_tool_opts_with_overrides(self.ba()).await?; - let filter = !include_prereleases(&opts); + let filter = !self.include_prereleases(&opts); Ok(self.fuzzy_match_filter(versions, query, filter)) } @@ -1230,7 +1282,7 @@ pub trait Backend: Debug + Send + Sync { } }; let opts = config.get_tool_opts_with_overrides(self.ba()).await?; - let filter = !include_prereleases(&opts); + let filter = !self.include_prereleases(&opts); Ok(self.fuzzy_match_filter(versions, query, filter)) } @@ -2550,24 +2602,12 @@ pub(crate) fn mark_prerelease(mut version: VersionInfo) -> VersionInfo { version } -/// Whether pre-release versions should be included for the current tool. -/// -/// Returns true if either the global `prereleases` setting (`MISE_PRERELEASES=1`, -/// or `--prerelease` on `ls-remote`) is on, or the per-tool `prerelease = true` -/// option is set. Accepts both TOML booleans (`prerelease = true`) and the -/// string form (`prerelease = "true"`), since inline backend args normalize -/// scalars to strings before they reach here. Used by `github:` and `aqua:` -/// backends to opt in to pre-release versions in `ls-remote`, `latest` -/// resolution, and fuzzy matching. -pub(crate) fn include_prereleases(opts: &crate::toolset::ToolVersionOptions) -> bool { - if Settings::get().prereleases { - return true; - } - opts.opts.get("prerelease").is_some_and(|v| match v { +fn tool_option_bool(value: &toml::Value) -> bool { + match value { toml::Value::Boolean(b) => *b, toml::Value::String(s) => s.parse::().unwrap_or(false), _ => false, - }) + } } /// Fuzzy-match `versions` against `query` with PEP 440 prerelease detection