From 2ed5e6a831046dcc6e70191af9c53c2964613482 Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Thu, 7 May 2026 12:05:09 +1000 Subject: [PATCH 1/3] fix(vfox): run pre_uninstall hook --- .../plugins/dummy/hooks/pre_uninstall.lua | 9 +++ crates/vfox/src/hooks/mod.rs | 1 + crates/vfox/src/hooks/pre_uninstall.rs | 30 ++++++++ .../vfox__vfox__tests__metadata.snap | 2 +- crates/vfox/src/vfox.rs | 52 ++++++++++++- e2e/backend/test_vfox_pre_uninstall | 76 +++++++++++++++++++ src/backend/vfox.rs | 25 ++++++ 7 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 crates/vfox/plugins/dummy/hooks/pre_uninstall.lua create mode 100644 crates/vfox/src/hooks/pre_uninstall.rs create mode 100644 e2e/backend/test_vfox_pre_uninstall diff --git a/crates/vfox/plugins/dummy/hooks/pre_uninstall.lua b/crates/vfox/plugins/dummy/hooks/pre_uninstall.lua new file mode 100644 index 0000000000..cabfaf2269 --- /dev/null +++ b/crates/vfox/plugins/dummy/hooks/pre_uninstall.lua @@ -0,0 +1,9 @@ +function PLUGIN:PreUninstall(ctx) + local main = ctx.main + local marker = main.path .. "/pre_uninstall_marker" + local marker_file = io.open(marker, "w") + if marker_file then + marker_file:write(main.name .. ":" .. main.version .. ":" .. string.gsub(main.path, "\\", "/")) + marker_file:close() + end +end diff --git a/crates/vfox/src/hooks/mod.rs b/crates/vfox/src/hooks/mod.rs index 165b29619d..bdd51e95e1 100644 --- a/crates/vfox/src/hooks/mod.rs +++ b/crates/vfox/src/hooks/mod.rs @@ -8,4 +8,5 @@ pub mod mise_path; pub mod parse_legacy_file; pub mod post_install; pub mod pre_install; +pub mod pre_uninstall; pub mod pre_use; diff --git a/crates/vfox/src/hooks/pre_uninstall.rs b/crates/vfox/src/hooks/pre_uninstall.rs new file mode 100644 index 0000000000..8f3990a9a6 --- /dev/null +++ b/crates/vfox/src/hooks/pre_uninstall.rs @@ -0,0 +1,30 @@ +use crate::Plugin; +use crate::error::Result; +use crate::sdk_info::SdkInfo; +use mlua::{IntoLua, Lua, Value}; +use std::collections::BTreeMap; + +impl Plugin { + pub async fn pre_uninstall(&self, ctx: PreUninstallContext) -> Result<()> { + debug!("[vfox:{}] pre_uninstall", &self.name); + self.exec_async(chunk! { + require "hooks/pre_uninstall" + PLUGIN:PreUninstall($ctx) + }) + .await + } +} + +pub struct PreUninstallContext { + pub main: SdkInfo, + pub sdk_info: BTreeMap, +} + +impl IntoLua for PreUninstallContext { + fn into_lua(self, lua: &Lua) -> mlua::Result { + let table = lua.create_table()?; + table.set("main", self.main)?; + table.set("sdkInfo", self.sdk_info)?; + Ok(Value::Table(table)) + } +} diff --git a/crates/vfox/src/snapshots/vfox__vfox__tests__metadata.snap b/crates/vfox/src/snapshots/vfox__vfox__tests__metadata.snap index 43205feec0..fc5395ed44 100644 --- a/crates/vfox/src/snapshots/vfox__vfox__tests__metadata.snap +++ b/crates/vfox/src/snapshots/vfox__vfox__tests__metadata.snap @@ -2,4 +2,4 @@ source: crates/vfox/src/vfox.rs expression: out --- -Metadata { name: "dummy", legacy_filenames: [".dummy-version"], depends: [], version: "0.3.0", description: Some("Dummy plugin for testing."), author: None, license: Some("Apache 2.0"), homepage: Some("https://github.com/version-fox/vfox-nodejs"), hooks: {"available", "env_keys", "parse_legacy_file", "post_install", "pre_install"} } +Metadata { name: "dummy", legacy_filenames: [".dummy-version"], depends: [], version: "0.3.0", description: Some("Dummy plugin for testing."), author: None, license: Some("Apache 2.0"), homepage: Some("https://github.com/version-fox/vfox-nodejs"), hooks: {"available", "env_keys", "parse_legacy_file", "post_install", "pre_install", "pre_uninstall"} } diff --git a/crates/vfox/src/vfox.rs b/crates/vfox/src/vfox.rs index 240bfcf77b..aa32f12ec3 100644 --- a/crates/vfox/src/vfox.rs +++ b/crates/vfox/src/vfox.rs @@ -19,6 +19,7 @@ use crate::hooks::mise_path::MisePathContext; use crate::hooks::parse_legacy_file::ParseLegacyFileResponse; use crate::hooks::post_install::PostInstallContext; use crate::hooks::pre_install::{PreInstall, PreInstallAttestation, VerifiedAttestation}; +use crate::hooks::pre_uninstall::PreUninstallContext; use crate::http::{CLIENT, retry_async}; use crate::metadata::Metadata; use crate::plugin::Plugin; @@ -234,8 +235,31 @@ impl Vfox { }) } - pub fn uninstall(&self, sdk: &str, version: &str) -> Result<()> { + pub async fn pre_uninstall>( + &self, + sdk: &str, + version: &str, + install_dir: ID, + ) -> Result<()> { + let sdk = self.get_sdk_with_env(sdk)?; + if sdk.get_metadata()?.hooks.contains("pre_uninstall") { + let sdk_info = sdk.sdk_info(version.to_string(), install_dir.as_ref().to_path_buf())?; + sdk.pre_uninstall(PreUninstallContext { + main: sdk_info.clone(), + sdk_info: BTreeMap::from([(sdk_info.name.clone(), sdk_info)]), + }) + .await?; + } + Ok(()) + } + + pub async fn uninstall(&self, sdk: &str, version: &str) -> Result<()> { let path = self.install_dir.join(sdk).join(version); + if self.plugin_dir.join(sdk).exists() + || crate::embedded_plugins::get_embedded_plugin(sdk).is_some() + { + self.pre_uninstall(sdk, version, &path).await?; + } file::remove_dir_all(&path)?; Ok(()) } @@ -676,12 +700,34 @@ mod tests { vfox.install("dummy", "1.0.0", &install_dir).await.unwrap(); // dummy plugin doesn't actually install binaries, so we just check the directory assert!(vfox.install_dir.join("dummy").join("1.0.0").exists()); - vfox.uninstall("dummy", "1.0.0").unwrap(); + vfox.uninstall("dummy", "1.0.0").await.unwrap(); assert!(!vfox.install_dir.join("dummy").join("1.0.0").exists()); file::remove_dir_all(vfox.install_dir).unwrap(); file::remove_dir_all(vfox.download_dir).unwrap(); } + #[tokio::test] + async fn test_pre_uninstall() { + let temp_dir = tempfile::tempdir().unwrap(); + let mut vfox = Vfox::test(); + vfox.install_dir = temp_dir.path().join("installs"); + let install_dir = vfox.install_dir.join("dummy").join("1.0.0"); + std::fs::create_dir_all(&install_dir).unwrap(); + + vfox.pre_uninstall("dummy", "1.0.0", &install_dir) + .await + .unwrap(); + + let marker = std::fs::read_to_string(install_dir.join("pre_uninstall_marker")).unwrap(); + assert_eq!( + marker, + format!( + "dummy:1.0.0:{}", + install_dir.to_string_lossy().replace('\\', "/") + ) + ); + } + #[tokio::test] #[ignore] // disable for now async fn test_install_cmake() { @@ -721,7 +767,7 @@ mod tests { } vfox.uninstall_plugin("cmake").unwrap(); assert!(!vfox.plugin_dir.join("cmake").exists()); - vfox.uninstall("cmake", "3.21.0").unwrap(); + vfox.uninstall("cmake", "3.21.0").await.unwrap(); assert!(!vfox.install_dir.join("cmake").join("3.21.0").exists()); file::remove_dir_all(vfox.plugin_dir.join("cmake")).unwrap(); file::remove_dir_all(vfox.install_dir).unwrap(); diff --git a/e2e/backend/test_vfox_pre_uninstall b/e2e/backend/test_vfox_pre_uninstall new file mode 100644 index 0000000000..185d02198c --- /dev/null +++ b/e2e/backend/test_vfox_pre_uninstall @@ -0,0 +1,76 @@ +#!/usr/bin/env bash + +PLUGIN_DIR="$PWD/vfox-pre-uninstall" +MARKER="$MISE_DATA_DIR/pre-uninstall-marker" + +mkdir -p "$PLUGIN_DIR/hooks" + +cat >"$PLUGIN_DIR/metadata.lua" <<'LUA' +PLUGIN = {} +PLUGIN.name = "pre-uninstall" +PLUGIN.version = "0.1.0" +PLUGIN.homepage = "https://example.com/pre-uninstall" +PLUGIN.license = "MIT" +PLUGIN.description = "pre uninstall test plugin" +LUA + +cat >"$PLUGIN_DIR/hooks/available.lua" <<'LUA' +function PLUGIN:Available(ctx) + return { + { version = "1.0.0" }, + } +end +LUA + +cat >"$PLUGIN_DIR/hooks/pre_install.lua" <<'LUA' +function PLUGIN:PreInstall(ctx) + return { + version = ctx.version, + } +end +LUA + +cat >"$PLUGIN_DIR/hooks/post_install.lua" <<'LUA' +function PLUGIN:PostInstall(ctx) + os.execute("mkdir -p " .. ctx.rootPath .. "/bin") + local bin = io.open(ctx.rootPath .. "/bin/pre-uninstall", "w") + if bin then + bin:write("#!/bin/sh\necho pre-uninstall\n") + bin:close() + os.execute("chmod +x " .. ctx.rootPath .. "/bin/pre-uninstall") + end +end +LUA + +cat >"$PLUGIN_DIR/hooks/env_keys.lua" <<'LUA' +function PLUGIN:EnvKeys(ctx) + return { + { key = "PATH", value = ctx.path .. "/bin" }, + } +end +LUA + +cat >"$PLUGIN_DIR/hooks/pre_uninstall.lua" <<'LUA' +function PLUGIN:PreUninstall(ctx) + local marker = os.getenv("MISE_DATA_DIR") .. "/pre-uninstall-marker" + local file = io.open(marker, "w") + if file then + file:write(ctx.main.name .. ":" .. ctx.main.version .. ":" .. ctx.sdkInfo[ctx.main.name].path) + file:close() + end +end +LUA + +git -C "$PLUGIN_DIR" init -q +git -C "$PLUGIN_DIR" add . +git -C "$PLUGIN_DIR" -c user.email=mise@example.com -c user.name=mise commit -m "init" + +mise install "vfox:file://$PLUGIN_DIR@1.0.0" + +mise uninstall "vfox:file://$PLUGIN_DIR@1.0.0" --dry-run +if [[ -f $MARKER ]]; then + fail "pre_uninstall hook ran during dry-run" +fi + +mise uninstall "vfox:file://$PLUGIN_DIR@1.0.0" +assert_contains "cat '$MARKER'" "pre-uninstall:1.0.0:" diff --git a/src/backend/vfox.rs b/src/backend/vfox.rs index d269a6008f..36ce004af6 100644 --- a/src/backend/vfox.rs +++ b/src/backend/vfox.rs @@ -271,6 +271,31 @@ impl Backend for VfoxBackend { Some(&self.plugin_enum) } + async fn uninstall_version_impl( + &self, + config: &Arc, + pr: &dyn crate::ui::progress_report::SingleReport, + tv: &ToolVersion, + ) -> eyre::Result<()> { + if self.is_backend_plugin() || !self.plugin.is_installed() { + return Ok(()); + } + + let (mut vfox, log_rx) = self.plugin.vfox(); + thread::spawn(|| { + for line in log_rx { + info!("{}", line); + } + }); + if let Ok(dep_env) = self.dependency_env(config).await { + vfox.cmd_env = Some(dep_env.into_iter().collect()); + } + pr.set_message("pre_uninstall".into()); + vfox.pre_uninstall(&self.pathname, &tv.version, tv.install_path()) + .await?; + Ok(()) + } + async fn _idiomatic_filenames(&self) -> eyre::Result> { let (vfox, _log_rx) = self.plugin.vfox(); From d032abdf8ac380ec2345a44329cfbd3b972b3324 Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Thu, 7 May 2026 12:11:45 +1000 Subject: [PATCH 2/3] style: format vfox pre_uninstall e2e --- e2e/backend/test_vfox_pre_uninstall | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/backend/test_vfox_pre_uninstall b/e2e/backend/test_vfox_pre_uninstall index 185d02198c..302ca08261 100644 --- a/e2e/backend/test_vfox_pre_uninstall +++ b/e2e/backend/test_vfox_pre_uninstall @@ -69,7 +69,7 @@ mise install "vfox:file://$PLUGIN_DIR@1.0.0" mise uninstall "vfox:file://$PLUGIN_DIR@1.0.0" --dry-run if [[ -f $MARKER ]]; then - fail "pre_uninstall hook ran during dry-run" + fail "pre_uninstall hook ran during dry-run" fi mise uninstall "vfox:file://$PLUGIN_DIR@1.0.0" From cdb71f9f92fea546b4d147782e9c72191fca8848 Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Thu, 7 May 2026 12:14:26 +1000 Subject: [PATCH 3/3] fix(vfox): address pre_uninstall review feedback --- crates/vfox/src/vfox.rs | 11 +++-------- src/backend/vfox.rs | 3 +-- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/crates/vfox/src/vfox.rs b/crates/vfox/src/vfox.rs index aa32f12ec3..42a5f0cc85 100644 --- a/crates/vfox/src/vfox.rs +++ b/crates/vfox/src/vfox.rs @@ -253,13 +253,8 @@ impl Vfox { Ok(()) } - pub async fn uninstall(&self, sdk: &str, version: &str) -> Result<()> { + pub fn uninstall(&self, sdk: &str, version: &str) -> Result<()> { let path = self.install_dir.join(sdk).join(version); - if self.plugin_dir.join(sdk).exists() - || crate::embedded_plugins::get_embedded_plugin(sdk).is_some() - { - self.pre_uninstall(sdk, version, &path).await?; - } file::remove_dir_all(&path)?; Ok(()) } @@ -700,7 +695,7 @@ mod tests { vfox.install("dummy", "1.0.0", &install_dir).await.unwrap(); // dummy plugin doesn't actually install binaries, so we just check the directory assert!(vfox.install_dir.join("dummy").join("1.0.0").exists()); - vfox.uninstall("dummy", "1.0.0").await.unwrap(); + vfox.uninstall("dummy", "1.0.0").unwrap(); assert!(!vfox.install_dir.join("dummy").join("1.0.0").exists()); file::remove_dir_all(vfox.install_dir).unwrap(); file::remove_dir_all(vfox.download_dir).unwrap(); @@ -767,7 +762,7 @@ mod tests { } vfox.uninstall_plugin("cmake").unwrap(); assert!(!vfox.plugin_dir.join("cmake").exists()); - vfox.uninstall("cmake", "3.21.0").await.unwrap(); + vfox.uninstall("cmake", "3.21.0").unwrap(); assert!(!vfox.install_dir.join("cmake").join("3.21.0").exists()); file::remove_dir_all(vfox.plugin_dir.join("cmake")).unwrap(); file::remove_dir_all(vfox.install_dir).unwrap(); diff --git a/src/backend/vfox.rs b/src/backend/vfox.rs index 36ce004af6..35e9fce0de 100644 --- a/src/backend/vfox.rs +++ b/src/backend/vfox.rs @@ -274,7 +274,7 @@ impl Backend for VfoxBackend { async fn uninstall_version_impl( &self, config: &Arc, - pr: &dyn crate::ui::progress_report::SingleReport, + _pr: &dyn crate::ui::progress_report::SingleReport, tv: &ToolVersion, ) -> eyre::Result<()> { if self.is_backend_plugin() || !self.plugin.is_installed() { @@ -290,7 +290,6 @@ impl Backend for VfoxBackend { if let Ok(dep_env) = self.dependency_env(config).await { vfox.cmd_env = Some(dep_env.into_iter().collect()); } - pr.set_message("pre_uninstall".into()); vfox.pre_uninstall(&self.pathname, &tv.version, tv.install_path()) .await?; Ok(())