From d188cd2354488ce6bb82aaf2bf75c05165f937a2 Mon Sep 17 00:00:00 2001 From: Tyce Herrman Date: Tue, 18 Nov 2025 22:25:06 -0500 Subject: [PATCH 1/5] fix(backend): allow upgrading vfox backend tools with symlinked installations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vfox backend tools (like nix) create symlinks as part of their normal installation process (e.g., symlinks to /nix/store). The upgrade check was incorrectly skipping these symlinked versions, causing them to never appear in `mise outdated` or be upgraded. This fix overrides symlink_path() in VfoxBackend to return None, indicating these are not user-created symlinks to other tool versions and should not be skipped during upgrade checks. Fixes #5909 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/backend/vfox.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/backend/vfox.rs b/src/backend/vfox.rs index 6ead722b66..f066a45dd5 100644 --- a/src/backend/vfox.rs +++ b/src/backend/vfox.rs @@ -152,6 +152,13 @@ impl Backend for VfoxBackend { Some(&self.plugin_enum) } + fn symlink_path(&self, _tv: &ToolVersion) -> Option { + // Vfox backend tools (like nix) create symlinks as part of normal installation + // (e.g., symlinks to /nix/store), so we should not skip them during upgrade checks. + // Return None to indicate these are not user-created symlinks to other versions. + None + } + async fn idiomatic_filenames(&self) -> eyre::Result> { let (vfox, _log_rx) = self.plugin.vfox(); From 25011f33ade786d11034aa25b90e7baf37387696 Mon Sep 17 00:00:00 2001 From: Tyce Herrman Date: Wed, 17 Dec 2025 19:10:59 -0500 Subject: [PATCH 2/5] new implementation based on jdx suggestion --- src/backend/mod.rs | 18 +++++++++++++++--- src/backend/vfox.rs | 7 ------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 783f5f12a1..5fcdd9006e 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -541,10 +541,22 @@ pub trait Backend: Debug + Send + Sync { !self.is_version_installed(config, tv, true) || is_outdated_version(&tv.version, &latest) } fn symlink_path(&self, tv: &ToolVersion) -> Option { - match tv.install_path() { - path if path.is_symlink() && !is_runtime_symlink(&path) => Some(path), - _ => None, + let path = tv.install_path(); + if !path.is_symlink() { + return None; + } + // Only skip symlinks pointing within installs (user aliases, not backend-managed) + if let Ok(Some(target)) = file::resolve_symlink(&path) { + let target = if target.is_absolute() { + target + } else { + path.parent().unwrap_or(&path).join(&target) + }; + if target.starts_with(*dirs::INSTALLS) { + return Some(path); + } } + None } fn create_symlink(&self, version: &str, target: &Path) -> Result> { let link = self.ba().installs_path.join(version); diff --git a/src/backend/vfox.rs b/src/backend/vfox.rs index f066a45dd5..6ead722b66 100644 --- a/src/backend/vfox.rs +++ b/src/backend/vfox.rs @@ -152,13 +152,6 @@ impl Backend for VfoxBackend { Some(&self.plugin_enum) } - fn symlink_path(&self, _tv: &ToolVersion) -> Option { - // Vfox backend tools (like nix) create symlinks as part of normal installation - // (e.g., symlinks to /nix/store), so we should not skip them during upgrade checks. - // Return None to indicate these are not user-created symlinks to other versions. - None - } - async fn idiomatic_filenames(&self) -> eyre::Result> { let (vfox, _log_rx) = self.plugin.vfox(); From 1c52146c1bb71d557a1f347ce531de6f44510e28 Mon Sep 17 00:00:00 2001 From: Tyce Herrman Date: Thu, 18 Dec 2025 12:17:17 -0500 Subject: [PATCH 3/5] fix(cli): preserve symlink display for mise link while allowing vfox upgrades MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit (5675a8cd1) changed symlink_path() to only return Some for symlinks pointing within dirs::INSTALLS, which fixed vfox backend upgrade checks but broke the (symlink) display for mise link created external symlinks. This separates the two concerns: - symlink_path() controls upgrade-skip logic (unchanged) - Display logic in ls.rs now checks is_symlink() && !is_runtime_symlink() directly, showing (symlink) for all non-runtime symlinks Fixes #5909 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/cli/ls.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/cli/ls.rs b/src/cli/ls.rs index 2f1f985d2b..c322d7d3be 100644 --- a/src/cli/ls.rs +++ b/src/cli/ls.rs @@ -12,6 +12,7 @@ use crate::cli::args::BackendArg; use crate::cli::prune; use crate::config; use crate::config::Config; +use crate::runtime_symlinks::is_runtime_symlink; use crate::toolset::{ToolSource, ToolVersion, Toolset}; use crate::ui::table::MiseTable; @@ -374,7 +375,9 @@ async fn version_status_from( config: &Arc, (ls, p, tv, source): (&Ls, &dyn Backend, &ToolVersion, &ToolSource), ) -> VersionStatus { - if p.symlink_path(tv).is_some() { + // Check for symlinks directly for display purposes (separate from upgrade-skip logic) + let install_path = tv.install_path(); + if install_path.is_symlink() && !is_runtime_symlink(&install_path) { VersionStatus::Symlink(tv.version.clone(), !source.is_unknown()) } else if !p.is_version_installed(config, tv, true) { VersionStatus::Missing(tv.version.clone()) From 98096b4db7d3eb8bc144c7ff32bcbf3263ec7782 Mon Sep 17 00:00:00 2001 From: Tyce Herrman Date: Thu, 18 Dec 2025 14:44:07 -0500 Subject: [PATCH 4/5] fix(cli): populate symlinked_to in JSON output for all non-runtime symlinks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The JSON output was using symlink_path() which now only returns symlinks pointing within installs (for upgrade-skip logic). This fix checks is_symlink() && !is_runtime_symlink() directly, matching the terminal display logic. Fixes test_sync_nvm test failure. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/cli/ls.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/cli/ls.rs b/src/cli/ls.rs index c322d7d3be..a651d4dc94 100644 --- a/src/cli/ls.rs +++ b/src/cli/ls.rs @@ -340,9 +340,15 @@ impl Row { async fn json_tool_version_from(config: &Arc, row: RuntimeRow<'_>) -> JSONToolVersion { let (ls, p, tv, source) = row; let vs: VersionStatus = version_status_from(config, (ls, p.as_ref(), &tv, &source)).await; + let install_path = tv.install_path(); JSONToolVersion { - symlinked_to: p.symlink_path(&tv), - install_path: tv.install_path(), + // Check for symlinks directly (separate from upgrade-skip logic in symlink_path) + symlinked_to: if install_path.is_symlink() && !is_runtime_symlink(&install_path) { + Some(install_path.clone()) + } else { + None + }, + install_path, version: tv.version.clone(), requested_version: if source.is_unknown() { None From 65b2f1aa926703913b86f773914bb10c0adc1df9 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:26:21 -0600 Subject: [PATCH 5/5] test(e2e): add test for HTTP backend upgrade with symlinked installs Adds an e2e test that validates the fix for symlinked HTTP backend installations being skipped in outdated checks. The test: 1. Installs an HTTP backend tool (which creates symlinks to cache) 2. Verifies mise latest sees the newer version 3. Verifies mise outdated --bump correctly detects it as outdated 4. Verifies mise upgrade --bump --dry-run shows the upgrade This test fails without the fix (symlink_path change) and passes with it. Co-Authored-By: Claude Opus 4.5 --- e2e/backend/test_http_upgrade | 79 +++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 e2e/backend/test_http_upgrade diff --git a/e2e/backend/test_http_upgrade b/e2e/backend/test_http_upgrade new file mode 100644 index 0000000000..64b09cafa8 --- /dev/null +++ b/e2e/backend/test_http_upgrade @@ -0,0 +1,79 @@ +#!/usr/bin/env bash + +# Test that HTTP backend tools (which use symlinks to cache) can be upgraded +# This validates the fix for https://github.com/jdx/mise/pull/7012 +# +# The HTTP backend creates symlinks from installs/ to http-tarballs/ cache. +# Before the fix, these symlinks caused tools to be incorrectly skipped +# in the outdated check, preventing upgrades from working. + +# Start a simple HTTP server to serve version list +VERSION_LIST_DIR="$TMPDIR/version-list-server" +mkdir -p "$VERSION_LIST_DIR" + +# Create version list file with two versions +echo -e "1.0.0\n2.0.0" >"$VERSION_LIST_DIR/versions.txt" + +# Start Python HTTP server in background +(cd "$VERSION_LIST_DIR" && python3 -m http.server 18123 &>/dev/null) & +HTTP_SERVER_PID=$! + +# Wait for server to start +sleep 1 + +# Cleanup function +cleanup() { + kill $HTTP_SERVER_PID 2>/dev/null || true +} +trap cleanup EXIT + +# Verify server is running +if ! curl -s http://localhost:18123/versions.txt | grep -q "1.0.0"; then + fail "HTTP server failed to start" +fi + +# Test 1: Install HTTP tool and verify it's a symlink to cache +echo "Test 1: Install HTTP tool with version_list_url" +cat <mise.toml +[tools."http:hello-upgrade"] +version = "1.0.0" +url = "https://mise.jdx.dev/test-fixtures/hello-world-{{version}}.tar.gz" +version_list_url = "http://localhost:18123/versions.txt" +bin_path = "hello-world-1.0.0/bin" +postinstall = "chmod +x \$MISE_TOOL_INSTALL_PATH/hello-world-1.0.0/bin/hello-world" +EOF + +mise install +assert_contains "mise x -- hello-world" "hello world" + +# Verify install is a symlink (HTTP backend caching behavior) +INSTALL_PATH="$MISE_DATA_DIR/installs/http-hello-upgrade/1.0.0" +if [[ ! -L $INSTALL_PATH ]]; then + fail "Expected install path to be a symlink: $INSTALL_PATH" +fi +echo "Verified: Install path is a symlink (HTTP backend caching)" + +# Test 2: Verify mise latest sees the newer version +echo "Test 2: Check that mise latest sees version 2.0.0" +assert "mise latest http:hello-upgrade" "2.0.0" + +# Test 3: Verify mise outdated --bump shows the tool as outdated +# This is the key test - before the fix, HTTP backend tools were skipped +echo "Test 3: Check that mise outdated --bump detects outdated version" +OUTDATED_OUTPUT=$(mise outdated --bump 2>&1) +if [[ $OUTDATED_OUTPUT != *"hello-upgrade"* ]] || [[ $OUTDATED_OUTPUT != *"2.0.0"* ]]; then + echo "outdated output: $OUTDATED_OUTPUT" + fail "mise outdated --bump should show http:hello-upgrade as outdated to 2.0.0" +fi +echo "Verified: mise outdated --bump correctly detects HTTP backend tool as outdated" + +# Test 4: Verify mise upgrade --bump --dry-run shows the upgrade +echo "Test 4: Check that mise upgrade --bump --dry-run shows the upgrade" +UPGRADE_OUTPUT=$(mise upgrade --bump --dry-run 2>&1) +if [[ $UPGRADE_OUTPUT != *"Would install"* ]] || [[ $UPGRADE_OUTPUT != *"hello-upgrade"* ]]; then + echo "upgrade output: $UPGRADE_OUTPUT" + fail "mise upgrade --bump --dry-run should show install for http:hello-upgrade" +fi +echo "Verified: mise upgrade --bump --dry-run correctly shows upgrade for HTTP backend tool" + +echo "SUCCESS: HTTP backend upgrade test passed!"