diff --git a/src/cli/install.rs b/src/cli/install.rs index a0ed64550d..5949679dfa 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -233,6 +233,7 @@ impl Install { use_locked_version: true, latest_versions: true, before_date: self.get_before_date()?, + offline: false, }, dry_run: self.is_dry_run(), locked: Settings::get().locked, diff --git a/src/cli/prune.rs b/src/cli/prune.rs index b334352395..a5a41a0835 100644 --- a/src/cli/prune.rs +++ b/src/cli/prune.rs @@ -109,7 +109,7 @@ pub async fn prunable_tools( } // Remove versions that are still needed by tracked configs - let needed_versions = get_versions_needed_by_tracked_configs(config, true).await?; + let needed_versions = get_versions_needed_by_tracked_configs(config, true, true).await?; for key in needed_versions { to_delete.remove(&key); } diff --git a/src/cli/upgrade.rs b/src/cli/upgrade.rs index 1ee34e55be..d0d942672c 100644 --- a/src/cli/upgrade.rs +++ b/src/cli/upgrade.rs @@ -117,6 +117,7 @@ impl Upgrade { use_locked_version: false, latest_versions: true, before_date, + offline: false, }; // Filter tools to check before doing expensive version lookups let filter_tools = if !self.interactive && !self.tool.is_empty() { @@ -260,6 +261,7 @@ impl Upgrade { use_locked_version: false, latest_versions: true, before_date, + offline: false, }, ..Default::default() }; @@ -338,7 +340,7 @@ impl Upgrade { // Get versions needed by tracked configs AFTER upgrade // This ensures we don't uninstall versions still needed by other projects let versions_needed_by_tracked = - get_versions_needed_by_tracked_configs(config, false).await?; + get_versions_needed_by_tracked_configs(config, false, false).await?; // Only uninstall old versions of tools that were successfully upgraded // and are not needed by any tracked config diff --git a/src/cli/use.rs b/src/cli/use.rs index b47a37955a..3a6a788a7e 100644 --- a/src/cli/use.rs +++ b/src/cli/use.rs @@ -144,6 +144,7 @@ impl Use { latest_versions: false, use_locked_version: true, before_date: self.get_before_date()?, + offline: false, }; let versions: Vec<_> = self .tool diff --git a/src/toolset/mod.rs b/src/toolset/mod.rs index c500e28786..fe8d7e5c63 100644 --- a/src/toolset/mod.rs +++ b/src/toolset/mod.rs @@ -602,13 +602,17 @@ impl From for Toolset { pub async fn get_versions_needed_by_tracked_configs( config: &Arc, use_locked_version: bool, + offline: bool, ) -> Result> { let mut needed = std::collections::HashSet::new(); // `mise prune` should keep versions pinned by lockfiles. `mise upgrade` // passes false because it checks what tracked configs resolve to after an // upgrade, before their lockfiles have been updated. + // Prune also passes offline=true: it only protects installed versions, so + // remote resolution can never affect the outcome and just adds latency. let opts = ResolveOptions { use_locked_version, + offline, ..Default::default() }; for (path, cf) in config.get_tracked_config_files().await? { @@ -639,6 +643,20 @@ pub async fn get_versions_needed_by_tracked_configs( ts.resolve_with_opts(config, &opts).await?; for (_, tv) in ts.list_current_versions() { needed.insert((tv.ba().short.to_string(), tv.tv_pathname())); + // Offline can't resolve `sub-N:latest` to a concrete version + // (no remote latest available). Conservatively protect every + // installed version of this backend so we don't delete the + // active one. + if offline + && let crate::toolset::ToolRequest::Sub { orig_version, .. } = &tv.request + && orig_version == "latest" + && let Ok(backend) = tv.backend() + { + let short = tv.ba().short.to_string(); + for v in backend.list_installed_versions() { + needed.insert((short.clone(), v)); + } + } } } Ok(needed) diff --git a/src/toolset/tool_version.rs b/src/toolset/tool_version.rs index 4ab859df5c..bd58f8a246 100644 --- a/src/toolset/tool_version.rs +++ b/src/toolset/tool_version.rs @@ -227,6 +227,7 @@ impl ToolVersion { latest_versions: true, use_locked_version: false, before_date: base_opts.before_date, + offline: base_opts.offline, }; let tv = self.request.resolve(config, &opts).await?; // map cargo backend specific prefixes to ref @@ -339,7 +340,7 @@ impl ToolVersion { } let settings = Settings::get(); - let is_offline = settings.offline(); + let is_offline = settings.offline() || opts.offline; if v == "latest" { if !opts.latest_versions @@ -362,6 +363,13 @@ impl ToolVersion { return build(v); } } + // Prune-style offline (opts.offline) wants a non-erroring no-op + // when nothing is installed — the literal "latest" can't match + // any installed pathname so it's safe. Global MISE_OFFLINE keeps + // the original error to avoid surprising upgrade/outdated callers. + if opts.offline { + return build(v); + } return Err(Self::no_versions_found(&backend, opts.before_date)); } if !opts.latest_versions { @@ -449,6 +457,15 @@ impl ToolVersion { opts: &ResolveOptions, ) -> Result { let backend = request.backend()?; + if v == "latest" && opts.offline { + // Can't resolve sub-N:latest offline (no remote latest, and + // applying version_sub to latest_installed_version would shift + // one step too low). Return the raw spec; callers that care + // (`get_versions_needed_by_tracked_configs`) over-protect by + // keeping all installed versions of this backend. + let version = request.version(); + return Ok(Self::new(request, version)); + } let v = match v { "latest" => backend .latest_version(config, None, opts.before_date) @@ -472,6 +489,9 @@ impl ToolVersion { { return Ok(Self::new(request, v.to_string())); } + if opts.offline { + return Ok(Self::new(request, prefix.to_string())); + } let matches = backend .list_versions_matching_with_opts(config, prefix, opts.before_date) .await?; @@ -553,6 +573,8 @@ pub struct ResolveOptions { pub use_locked_version: bool, /// Only consider versions released before this timestamp pub before_date: Option, + /// Additive to `Settings::offline()` — either being true skips remote version listing. + pub offline: bool, } impl Default for ResolveOptions { @@ -561,6 +583,7 @@ impl Default for ResolveOptions { latest_versions: false, use_locked_version: true, before_date: None, + offline: false, } } } @@ -598,6 +621,9 @@ impl Display for ResolveOptions { if let Some(ts) = &self.before_date { opts.push(format!("before_date={ts}")); } + if self.offline { + opts.push("offline".to_string()); + } write!(f, "({})", opts.join(", ")) } }