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
29 changes: 3 additions & 26 deletions docs/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,12 @@ leave = "echo 'I left the project'"

## Preinstall/postinstall hook

These hooks are run before and after each tool is installed (respectively). Unlike other hooks, these hooks do not require `mise activate`.

The hooks run once per tool being installed, with `MISE_TOOL_NAME` and `MISE_TOOL_VERSION` environment variables set to the tool being processed.

```toml
[hooks]
preinstall = "echo 'About to install $MISE_TOOL_NAME@$MISE_TOOL_VERSION'"
postinstall = "echo 'Finished installing $MISE_TOOL_NAME@$MISE_TOOL_VERSION'"
```

You can use these variables to run conditional logic based on the tool:
These hooks are run before and after tools are installed (respectively). Unlike other hooks, these hooks do not require `mise activate`.

```toml
[hooks]
postinstall = '''
if [ "$MISE_TOOL_NAME" = "node" ]; then
echo "Node.js $MISE_TOOL_VERSION installed, running npm setup..."
npm config set prefix ~/.npm-global
fi
'''
preinstall = "echo 'I am about to install tools'"
postinstall = "echo 'I just installed tools'"
```

## Watch files hook
Expand All @@ -77,15 +63,6 @@ Hooks are executed with the following environment variables set:
- `MISE_PROJECT_ROOT`: The root directory of the project.
- `MISE_PREVIOUS_DIR`: The directory that the user was in before the directory change (only if a directory change occurred).

For `preinstall` and `postinstall` hooks, the following additional environment variables are set:

- `MISE_TOOL_NAME`: The short name of the tool being installed (e.g., `node`, `python`, `go`).
- `MISE_TOOL_VERSION`: The version of the tool being installed (e.g., `20.10.0`, `3.12.0`).

For tool-level postinstall hooks (defined on the tool itself), an additional variable is available:

- `MISE_TOOL_INSTALL_PATH`: The installation path of the tool.

## Shell hooks

Hooks can be executed in the current shell, for example if you'd like to add bash completions when entering a directory:
Expand Down
20 changes: 1 addition & 19 deletions e2e/config/test_hooks_postinstall_env
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ EXPLICIT_PRE_TOOLS = {value = "explicitly_pre_tools", tools = false}
POST_TOOLS_VAR = {value = "available_after_tools", tools = true}

[tools]
dummy = { version = "latest", postinstall = "echo PRE_TOOLS_VAR=\$PRE_TOOLS_VAR; echo EXPLICIT_PRE_TOOLS=\$EXPLICIT_PRE_TOOLS; echo POST_TOOLS_VAR=\$POST_TOOLS_VAR; echo MISE_TOOL_NAME=\$MISE_TOOL_NAME; echo MISE_TOOL_VERSION=\$MISE_TOOL_VERSION" }
dummy = { version = "latest", postinstall = "echo PRE_TOOLS_VAR=\$PRE_TOOLS_VAR; echo EXPLICIT_PRE_TOOLS=\$EXPLICIT_PRE_TOOLS; echo POST_TOOLS_VAR=\$POST_TOOLS_VAR" }
EOF

# Remove any existing dummy installation to force reinstall
Expand Down Expand Up @@ -58,21 +58,3 @@ else
echo "Output: $output"
exit 1
fi

# Verify MISE_TOOL_NAME is set
if [[ $output == *"MISE_TOOL_NAME=dummy"* ]]; then
echo "✓ MISE_TOOL_NAME is set correctly"
else
echo "✗ MISE_TOOL_NAME is not set correctly"
echo "Output: $output"
exit 1
fi

# Verify MISE_TOOL_VERSION is set (should be "latest" resolved version)
if [[ $output == *"MISE_TOOL_VERSION="* ]] && [[ $output != *"MISE_TOOL_VERSION=$"* ]]; then
echo "✓ MISE_TOOL_VERSION is set"
else
echo "✗ MISE_TOOL_VERSION is not set"
echo "Output: $output"
exit 1
fi
20 changes: 0 additions & 20 deletions e2e/config/test_hooks_tool_env

This file was deleted.

2 changes: 0 additions & 2 deletions src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -851,8 +851,6 @@ pub trait Backend: Debug + Send + Sync {
CmdLineRunner::new(&*env::SHELL)
.env(&*env::PATH_KEY, plugins::core::path_env_with_tv_path(tv)?)
.env("MISE_TOOL_INSTALL_PATH", tv.install_path())
.env("MISE_TOOL_NAME", &tv.ba().short)
.env("MISE_TOOL_VERSION", &tv.version)
.with_pr(ctx.pr.as_ref())
.arg(env::SHELL_COMMAND_FLAG)
.arg(script)
Expand Down
54 changes: 2 additions & 52 deletions src/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,6 @@ use std::sync::Mutex;
use std::{iter::once, sync::Arc};
use tokio::sync::OnceCell;

/// Context for tool-specific hooks (preinstall/postinstall)
#[derive(Debug, Clone)]
pub struct HookToolContext {
pub name: String,
pub version: String,
}

#[derive(
Debug,
Clone,
Expand Down Expand Up @@ -91,38 +84,6 @@ pub async fn run_one_hook(
ts: &Toolset,
hook: Hooks,
shell: Option<&dyn Shell>,
) {
run_one_hook_with_shell(config, ts, hook, shell).await;
}

/// Run a hook with optional tool context for preinstall/postinstall hooks (used during installation)
/// This version doesn't take a shell parameter and can be used in spawned async tasks.
#[async_backtrace::framed]
pub async fn run_one_hook_with_tool(
config: &Arc<Config>,
ts: &Toolset,
hook: Hooks,
tool_ctx: &HookToolContext,
) {
for (root, h) in all_hooks(config).await {
if hook != h.hook || h.shell.is_some() {
// Skip shell-specific hooks during installation
continue;
}
trace!("running hook {hook} in {root:?}");
if let Err(e) = execute(config, ts, root, h, Some(tool_ctx)).await {
warn!("error executing hook: {e}");
}
}
}

/// Run a hook with optional shell context (used during activate)
#[async_backtrace::framed]
async fn run_one_hook_with_shell(
config: &Arc<Config>,
ts: &Toolset,
hook: Hooks,
shell: Option<&dyn Shell>,
) {
for (root, h) in all_hooks(config).await {
if hook != h.hook || (h.shell.is_some() && h.shell != shell.map(|s| s.to_string())) {
Expand Down Expand Up @@ -155,7 +116,7 @@ async fn run_one_hook_with_shell(
}
if h.shell.is_some() {
println!("{}", h.script);
} else if let Err(e) = execute(config, ts, root, h, None).await {
} else if let Err(e) = execute(config, ts, root, h).await {
warn!("error executing hook: {e}");
}
}
Expand Down Expand Up @@ -198,13 +159,7 @@ impl Hook {
}
}

async fn execute(
config: &Arc<Config>,
ts: &Toolset,
root: &Path,
hook: &Hook,
tool_ctx: Option<&HookToolContext>,
) -> Result<()> {
async fn execute(config: &Arc<Config>, ts: &Toolset, root: &Path, hook: &Hook) -> Result<()> {
Settings::get().ensure_experimental("hooks")?;
let shell = Settings::get().default_inline_shell()?;

Expand All @@ -231,11 +186,6 @@ async fn execute(
old.to_string_lossy().to_string(),
);
}
// Add tool context for preinstall/postinstall hooks
if let Some(ctx) = tool_ctx {
env.insert("MISE_TOOL_NAME".to_string(), ctx.name.clone());
env.insert("MISE_TOOL_VERSION".to_string(), ctx.version.clone());
}
// TODO: this should be different but I don't have easy access to it
// env.insert("MISE_CONFIG_ROOT".to_string(), root.to_string_lossy().to_string());
cmd(&shell[0], args)
Expand Down
50 changes: 14 additions & 36 deletions src/toolset/toolset_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use tokio::{sync::Semaphore, task::JoinSet};
use crate::config::Config;
use crate::config::settings::Settings;
use crate::errors::Error;
use crate::hooks::{HookToolContext, Hooks};
use crate::hooks::Hooks;
use crate::install_context::InstallContext;
use crate::toolset::Toolset;
use crate::toolset::helpers::{get_leaf_dependencies, show_python_install_hint};
Expand Down Expand Up @@ -100,6 +100,12 @@ impl Toolset {
};
mpr.init_footer(opts.dry_run, &footer_reason, versions.len());

// Skip hooks in dry-run mode
if !opts.dry_run {
// Run pre-install hook
hooks::run_one_hook(config, self, Hooks::Preinstall, None).await;
}

self.init_request_options(&mut versions);
show_python_install_hint(&versions);

Expand Down Expand Up @@ -170,6 +176,12 @@ impl Toolset {
}
}

// Skip hooks in dry-run mode
if !opts.dry_run {
// Run post-install hook (ignoring errors)
let _ = hooks::run_one_hook(config, self, Hooks::Postinstall, None).await;
}

// Finish the global footer
if !opts.dry_run {
mpr.footer_finish();
Expand Down Expand Up @@ -308,21 +320,6 @@ impl Toolset {
let result = async {
let tv = tr.resolve(&config, &opts.resolve_options).await?;

// Run per-tool preinstall hook
if !opts.dry_run {
let tool_ctx = HookToolContext {
name: tv.ba().short.clone(),
version: tv.version.clone(),
};
hooks::run_one_hook_with_tool(
&config,
&ts,
Hooks::Preinstall,
&tool_ctx,
)
.await;
}

let ctx = InstallContext {
config: config.clone(),
ts: ts.clone(),
Expand All @@ -333,26 +330,7 @@ impl Toolset {
};
// Avoid wrapping the backend error here so the error location
// points to the backend implementation (more helpful for debugging).
let result = ba.install_version(ctx, tv).await;

// Run per-tool postinstall hook (only on success)
if !opts.dry_run
&& let Ok(ref installed_tv) = result
{
let tool_ctx = HookToolContext {
name: installed_tv.ba().short.clone(),
version: installed_tv.version.clone(),
};
hooks::run_one_hook_with_tool(
&config,
&ts,
Hooks::Postinstall,
&tool_ctx,
)
.await;
}

result
ba.install_version(ctx, tv).await
}
.await;

Expand Down
Loading