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
11 changes: 10 additions & 1 deletion docs/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ String hooks are shorthand for `run` hooks. Use a hook table when you need to se
postinstall = { run = "echo 'installed'", shell = "bash -c" }
```

Like tasks, inline hook tables may define a Windows-specific command with `run_windows`.
On Windows, mise uses `run_windows` when it is set; otherwise it uses `run`. On other
platforms, a hook with only `run_windows` is skipped.

```toml
[hooks]
postinstall = { run = "echo installed", run_windows = "Write-Output installed" }
```

For `preinstall` and `postinstall`, `script = ...` is a legacy alias for `run = ...`. If a `shell` is also set on a `script`/`scripts` hook, mise warns that the shell is ignored and still runs the script with the default inline shell. Use `run = ...` with `shell = "bash -c"` to choose the inline shell command. The `script` alias for install hooks is deprecated.

The `postinstall` hook receives a `MISE_INSTALLED_TOOLS` environment variable containing a JSON array of the tools that were just installed:
Expand Down Expand Up @@ -149,7 +158,7 @@ Hooks are executed with the following environment variables set:
Inline `run` hooks can be written as `{ run = "..." }` for any hook type. The string shorthand
(`enter = "echo hi"`) is equivalent to `{ run = "echo hi" }`.

`run` must be a string. `run = ["echo one", "echo two"]` is not supported.
`run` and `run_windows` must be strings. `run = ["echo one", "echo two"]` is not supported.

To run separate spawned inline commands, define multiple hooks. Each hook entry is a separate
execution, so mise starts one subprocess per `run` entry:
Expand Down
22 changes: 22 additions & 0 deletions e2e/config/test_hooks_run_windows
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash

cat <<'EOF' >mise.toml
[settings]
unix_default_inline_shell_args = "bash -c"

[tools]
dummy = 'latest'

[hooks]
preinstall = [
{ run_windows = '{{ exec(command="mise-run-windows-template-should-not-execute") }}' },
{ run_windows = 'echo PRE_WINDOWS_ONLY' },
{ run = 'echo PRE_UNIX', run_windows = 'echo PRE_WINDOWS' },
]
EOF

output=$(mise install 2>&1)

assert_contains "echo '$output'" "PRE_UNIX"
assert_not_contains "echo '$output'" "PRE_WINDOWS"
assert_not_contains "echo '$output'" "PRE_WINDOWS_ONLY"
13 changes: 12 additions & 1 deletion schema/mise.json
Original file line number Diff line number Diff line change
Expand Up @@ -2062,12 +2062,23 @@
"description": "inline hook command to run",
"type": "string"
},
"run_windows": {
"description": "inline hook command to run on Windows",
"type": "string"
},
"shell": {
"description": "inline shell command to use for run hooks, such as \"bash -c\"",
"type": "string"
}
},
"required": ["run"],
"anyOf": [
{
"required": ["run"]
},
{
"required": ["run_windows"]
}
],
"type": "object"
},
"hook_script_value": {
Expand Down
116 changes: 104 additions & 12 deletions src/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ pub enum HookDef {
/// Simple run string: `enter = "echo hello"`
RunString(String),
/// Table with run: `enter = { run = "echo hello" }`
Run { run: String, shell: Option<String> },
Run(HookRunTable),
/// Table with script and optional shell: `enter = { script = "echo hello", shell = "bash" }`
ScriptTable {
script: HookScripts,
Expand All @@ -90,25 +90,61 @@ pub enum HookDef {
Array(Vec<HookDef>),
}

#[derive(Debug, Clone)]
pub struct HookRunTable {
run: Option<String>,
run_windows: Option<String>,
shell: Option<String>,
}

impl<'de> serde::Deserialize<'de> for HookRunTable {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Helper {
run: Option<String>,
run_windows: Option<String>,
shell: Option<String>,
}

let helper = <Helper as serde::Deserialize>::deserialize(deserializer)?;
if helper.run.is_none() && helper.run_windows.is_none() {
return Err(serde::de::Error::custom(
"hook run table must define `run` or `run_windows`",
));
}
Ok(Self {
run: helper.run,
run_windows: helper.run_windows,
shell: helper.shell,
})
}
}

impl HookDef {
/// Convert to a list of Hook structs with the given hook type
pub fn into_hooks(self, hook_type: Hooks) -> Vec<Hook> {
match self {
HookDef::RunString(script) => vec![Hook {
hook: hook_type,
action: HookAction::Run {
run: script,
run: Some(script),
run_windows: None,
shell: None,
legacy_script: false,
ignored_shell: None,
},
global: false,
}],
HookDef::Run { run, shell } => vec![Hook {
HookDef::Run(table) => vec![Hook {
hook: hook_type,
action: HookAction::Run {
run,
shell,
run: table.run,
run_windows: table.run_windows,
shell: table.shell,
legacy_script: false,
ignored_shell: None,
},
Expand Down Expand Up @@ -148,7 +184,8 @@ fn script_hook_action(
HookAction::CurrentShell { script, shell }
}
(_, shell) => HookAction::Run {
run: script,
run: Some(script),
run_windows: None,
shell: None,
legacy_script,
ignored_shell: shell,
Expand All @@ -167,7 +204,8 @@ pub struct Hook {
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum HookAction {
Run {
run: String,
run: Option<String>,
run_windows: Option<String>,
shell: Option<String>,
legacy_script: bool,
ignored_shell: Option<String>,
Expand All @@ -189,16 +227,24 @@ impl Hook {
match &mut self.action {
HookAction::Run {
run,
run_windows,
shell,
ignored_shell,
..
} => {
*run = render(run)?;
if let Some(s) = shell {
*s = render(s)?;
}
if let Some(s) = ignored_shell {
let run = if cfg!(windows) {
run_windows.as_mut().or(run.as_mut())
} else {
run.as_mut()
};
if let Some(s) = run {
*s = render(s)?;
if let Some(s) = shell {
*s = render(s)?;
}
if let Some(s) = ignored_shell {
*s = render(s)?;
}
}
}
HookAction::CurrentShell { script, shell } => {
Expand Down Expand Up @@ -437,6 +483,7 @@ async fn execute(
Settings::get().ensure_experimental("hooks")?;
let HookAction::Run {
run,
run_windows,
shell,
legacy_script,
ignored_shell,
Expand All @@ -459,6 +506,14 @@ async fn execute(
hook_name
);
}
let run = if cfg!(windows) {
run_windows.as_deref().or(run.as_deref())
} else {
run.as_deref()
};
let Some(run) = run else {
return Ok(());
};
let shell = shell
.as_ref()
.map(|shell| crate::path::split_shell_command(shell))
Expand Down Expand Up @@ -589,3 +644,40 @@ async fn execute_task(
.run()?;
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;

#[derive(Deserialize)]
struct TestHook {
hook: HookDef,
}

#[test]
fn run_table_supports_run_windows() {
let parsed: TestHook = toml::from_str(
r#"
hook = { run = "echo unix", run_windows = "echo windows", shell = "bash -c" }
"#,
)
.unwrap();
let hooks = parsed.hook.into_hooks(Hooks::Postinstall);

assert_eq!(hooks.len(), 1);
match &hooks[0].action {
HookAction::Run {
run,
run_windows,
shell,
..
} => {
assert_eq!(run.as_deref(), Some("echo unix"));
assert_eq!(run_windows.as_deref(), Some("echo windows"));
assert_eq!(shell.as_deref(), Some("bash -c"));
}
action => panic!("expected run hook, got {action:?}"),
}
}
}
Loading