use crate::cmd::cmd;
use crate::config::{Config, SETTINGS, config_file};
use crate::shell::Shell;
use crate::toolset::Toolset;
use crate::{dirs, hook_env};
use eyre::{Result, eyre};
use indexmap::IndexSet;
use itertools::Itertools;
use std::iter::once;
use std::path::{Path, PathBuf};
use std::sync::LazyLock as Lazy;
use std::sync::Mutex;
#[derive(
Debug,
Clone,
Copy,
serde::Serialize,
serde::Deserialize,
strum::Display,
Ord,
PartialOrd,
Eq,
PartialEq,
Hash,
)]
#[serde(rename_all = "lowercase")]
pub enum Hooks {
Enter,
Leave,
Cd,
Preinstall,
Postinstall,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Hook {
pub hook: Hooks,
pub script: String,
pub shell: Option<String>,
}
pub static SCHEDULED_HOOKS: Lazy<Mutex<IndexSet<Hooks>>> = Lazy::new(Default::default);
pub fn schedule_hook(hook: Hooks) {
let mut mu = SCHEDULED_HOOKS.lock().unwrap();
mu.insert(hook);
}
pub fn run_all_hooks(ts: &Toolset, shell: &dyn Shell) {
let mut mu = SCHEDULED_HOOKS.lock().unwrap();
for hook in mu.drain(..) {
run_one_hook(ts, hook, Some(shell));
}
}
static ALL_HOOKS: Lazy<Vec<(PathBuf, Hook)>> = Lazy::new(|| {
let config = Config::get();
let mut hooks = config.hooks().cloned().unwrap_or_default();
let cur_configs = config.config_files.keys().cloned().collect::<IndexSet<_>>();
let prev_configs = &hook_env::PREV_SESSION.loaded_configs;
let old_configs = prev_configs.difference(&cur_configs);
for p in old_configs {
if let Ok(cf) = config_file::parse(p) {
if let Ok(h) = cf.hooks() {
hooks.extend(h.into_iter().map(|h| (cf.config_root(), h)));
}
}
}
hooks
});
pub fn run_one_hook(ts: &Toolset, hook: Hooks, shell: Option<&dyn Shell>) {
for (root, h) in &*ALL_HOOKS {
if hook != h.hook || (h.shell.is_some() && h.shell != shell.map(|s| s.to_string())) {
continue;
}
trace!("running hook {hook} in {root:?}");
match (hook, hook_env::dir_change()) {
(Hooks::Enter, Some((old, new))) => {
if !new.starts_with(root) {
continue;
}
if old.as_ref().is_some_and(|old| old.starts_with(root)) {
continue;
}
}
(Hooks::Leave, Some((old, new))) => {
if new.starts_with(root) {
continue;
}
if old.as_ref().is_some_and(|old| !old.starts_with(root)) {
continue;
}
}
(Hooks::Cd, Some((_old, new))) => {
if !new.starts_with(root) {
continue;
}
}
_ => {}
}
if h.shell.is_some() {
println!("{}", h.script);
} else if let Err(e) = execute(ts, root, h) {
warn!("error executing hook: {e}");
}
}
}
impl Hook {
pub fn from_toml(hook: Hooks, value: toml::Value) -> Result<Vec<Self>> {
match value {
toml::Value::String(run) => Ok(vec![Hook {
hook,
script: run,
shell: None,
}]),
toml::Value::Table(tbl) => {
let script = tbl
.get("script")
.ok_or_else(|| eyre!("missing `script` key"))?;
let script = script
.as_str()
.ok_or_else(|| eyre!("`run` must be a string"))?;
let shell = tbl
.get("shell")
.and_then(|s| s.as_str())
.map(|s| s.to_string());
Ok(vec![Hook {
hook,
script: script.to_string(),
shell,
}])
}
toml::Value::Array(arr) => {
let mut hooks = vec![];
for v in arr {
hooks.extend(Self::from_toml(hook, v)?);
}
Ok(hooks)
}
v => panic!("invalid hook value: {v}"),
}
}
}
fn execute(ts: &Toolset, root: &Path, hook: &Hook) -> Result<()> {
SETTINGS.ensure_experimental("hooks")?;
let shell = SETTINGS.default_inline_shell()?;
let args = shell
.iter()
.skip(1)
.map(|s| s.as_str())
.chain(once(hook.script.as_str()))
.collect_vec();
let config = Config::get();
let mut env = ts.full_env(&config)?;
if let Some(cwd) = dirs::CWD.as_ref() {
env.insert(
"MISE_ORIGINAL_CWD".to_string(),
cwd.to_string_lossy().to_string(),
);
}
env.insert(
"MISE_PROJECT_ROOT".to_string(),
root.to_string_lossy().to_string(),
);
if let Some((Some(old), _new)) = hook_env::dir_change() {
env.insert(
"MISE_PREVIOUS_DIR".to_string(),
old.to_string_lossy().to_string(),
);
}
cmd(&shell[0], args)
.stdout_to_stderr()
.full_env(env)
.run()?;
Ok(())
}