Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions src/cli/self_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,20 @@ impl SelfUpdate {
let version = status.version().to_string();
let styled_version = style(&version).bright().yellow();
miseprintln!("Updated mise to {styled_version}");
// On Windows, "exe"/"hardlink" shims are copies of mise-shim.exe and
// go stale after an update. Refresh mise-shim.exe, and ONLY if that
// succeeds rebuild the shim copies from it. Reshimming on failure
// would re-copy the OLD mise-shim.exe yet still stamp the new version
// in the `.version` marker, masking the staleness from future
// (non-forced) reshims. Best-effort. See discussion #10022.
#[cfg(windows)]
if let Err(e) = Self::update_mise_shim(&version).await {
warn!("Failed to update mise-shim.exe: {e}");
match Self::update_mise_shim(&version).await {
Ok(()) => {
if let Err(e) = Self::reshim_after_update().await {
warn!("Failed to reshim after self-update: {e}");
}
}
Err(e) => warn!("Failed to update mise-shim.exe: {e}"),
}
} else {
miseprintln!("mise is already up to date");
Expand Down Expand Up @@ -172,6 +183,18 @@ impl SelfUpdate {
Ok(status)
}

// Rebuild the Windows shim copies in-process instead of shelling out to
// `mise reshim --force`. Mirrors `cli::reshim::Reshim::run`.
#[cfg(windows)]
async fn reshim_after_update() -> Result<()> {
use crate::config::Config;
use crate::toolset::ToolsetBuilder;

let config = Config::get().await?;
let ts = ToolsetBuilder::new().build(&config).await?;
crate::shims::reshim(&config, &ts, true).await
}

#[cfg(windows)]
async fn update_mise_shim(version: &str) -> Result<()> {
use crate::http::HTTP;
Expand Down
61 changes: 59 additions & 2 deletions src/shims.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,20 @@ pub async fn reshim(config: &Arc<Config>, ts: &Toolset, force: bool) -> Result<(
.then(|| fs::read_to_string(&mode_file).unwrap_or_default())
.is_some_and(|prev| prev.trim() != shim_mode)
};
if force || shim_mode_changed {
// On Windows, "exe"/"hardlink" shims are literal copies of the mise(-shim)
// binary, so they go stale when mise is updated (by self-update or an
// external package manager) until a forced reshim. Track the mise version
// that generated the shims in a `.version` marker (mirroring `.mode`) and
// rebuild from scratch whenever it changes. The marker is written by
// whichever binary runs reshim, so after an update the new binary stamps
// the new version. See discussion #10022.
let shim_version = env!("CARGO_PKG_VERSION");
let shim_version_changed = cfg!(windows) && {
let version_file = dirs::SHIMS.join(".version");
let prev = fs::read_to_string(&version_file).ok();
shim_version_stale(prev.as_deref(), shim_version, &shim_mode)
};
if force || shim_mode_changed || shim_version_changed {
// On Windows, .exe shims may be locked by processes or the shell (they
// are on PATH). Instead of removing the entire directory (which fails
// with "Access is denied"), remove individual files with a rename-first
Expand All @@ -150,9 +163,15 @@ pub async fn reshim(config: &Arc<Config>, ts: &Toolset, force: bool) -> Result<(
if cfg!(windows) {
let mode_file = dirs::SHIMS.join(".mode");
file::write(&mode_file, &shim_mode)?;
// Written for every shim mode (like `.mode`) even though it is only
// consulted for "exe"/"hardlink" modes; for "file"/"symlink" it is
// harmless and keeps the marker current if the mode later changes
// (mode transitions themselves are handled by `shim_mode_changed`).
let version_file = dirs::SHIMS.join(".version");
file::write(&version_file, shim_version)?;
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

let (shims_to_add, shims_to_remove) = if force || shim_mode_changed {
let (shims_to_add, shims_to_remove) = if force || shim_mode_changed || shim_version_changed {
// After a full wipe, all desired shims need to be re-created.
let desired = get_desired_shims(config, &mise_bin, ts).await?;
(
Expand Down Expand Up @@ -488,6 +507,19 @@ fn is_hidden_shim_name(name: &std::ffi::OsStr) -> bool {
name.to_string_lossy().starts_with('.')
}

/// Whether existing shims were generated by a different mise version AND the
/// current shim mode produces version-dependent shim files (a literal copy of
/// the mise/mise-shim binary). Script ("file") and "symlink" modes are never
/// version-stale because their content does not embed the mise binary.
/// `prev == None` (no `.version` marker yet) heals installs that predate the
/// marker by forcing a one-time rebuild. See discussion #10022.
fn shim_version_stale(prev: Option<&str>, current: &str, shim_mode: &str) -> bool {
Comment thread
JamBalaya56562 marked this conversation as resolved.
if !matches!(shim_mode, "exe" | "hardlink") {
return false;
}
prev.map(|p| p.trim() != current).unwrap_or(true)
}

async fn get_desired_shims(
config: &Arc<Config>,
mise_bin: &Path,
Expand Down Expand Up @@ -661,4 +693,29 @@ mod tests {

assert!(bins.is_empty());
}

#[test]
fn shim_version_stale_detects_version_changes() {
// exe/hardlink copies embed the binary: a version change makes them stale
assert!(shim_version_stale(Some("2026.5.13"), "2026.5.16", "exe"));
assert!(shim_version_stale(
Some("2026.5.13"),
"2026.5.16",
"hardlink"
));
// matching version is not stale
assert!(!shim_version_stale(Some("2026.5.16"), "2026.5.16", "exe"));
// surrounding whitespace in the marker is ignored
assert!(!shim_version_stale(Some("2026.5.16\n"), "2026.5.16", "exe"));
// no marker yet: heal once (covers installs created before this marker)
assert!(shim_version_stale(None, "2026.5.16", "exe"));
// script/symlink shims never embed the binary, so never version-stale
assert!(!shim_version_stale(Some("2026.5.13"), "2026.5.16", "file"));
assert!(!shim_version_stale(
Some("2026.5.13"),
"2026.5.16",
"symlink"
));
assert!(!shim_version_stale(None, "2026.5.16", "file"));
}
}
Loading