use std::collections::{BTreeMap, HashSet};
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use std::sync::{Arc, LazyLock, Mutex};
use eyre::{Result, bail};
use crate::config::{Config, Settings};
use crate::env;
pub use engine::{PrepareEngine, PrepareOptions, PrepareStepResult};
pub use rule::PrepareConfig;
mod engine;
pub(crate) mod prepare_deps;
pub mod providers;
mod rule;
pub mod state;
#[derive(Debug, Clone)]
pub enum FreshnessResult {
Fresh,
NoOutputs,
OutputsMissing,
Stale(String),
NoSources,
Forced,
}
impl FreshnessResult {
pub fn is_fresh(&self) -> bool {
matches!(self, FreshnessResult::Fresh | FreshnessResult::NoSources)
}
pub fn reason(&self) -> &str {
match self {
FreshnessResult::Fresh => "outputs are up to date",
FreshnessResult::NoOutputs => "no outputs defined",
FreshnessResult::OutputsMissing => "outputs missing",
FreshnessResult::Stale(reason) => reason,
FreshnessResult::NoSources => "no sources to check",
FreshnessResult::Forced => "forced",
}
}
}
#[derive(Debug, Clone)]
pub struct PrepareCommand {
pub program: String,
pub args: Vec<String>,
pub env: BTreeMap<String, String>,
pub cwd: Option<PathBuf>,
pub description: String,
}
impl PrepareCommand {
pub fn from_string(
run: &str,
project_root: &Path,
config: &rule::PrepareProviderConfig,
) -> Result<Self> {
if run.trim().is_empty() {
bail!("prepare run command cannot be empty");
}
let shell = Settings::get().default_inline_shell()?;
let (program, shell_args) = shell.split_first().ok_or_else(|| {
eyre::eyre!("default inline shell is empty; check unix_default_inline_shell_args / windows_default_inline_shell_args")
})?;
let mut args: Vec<String> = shell_args.to_vec();
args.push(run.to_string());
Ok(Self {
program: program.to_string(),
args,
env: config.env.clone(),
cwd: config
.dir
.as_ref()
.map(|d| project_root.join(d))
.or_else(|| Some(project_root.to_path_buf())),
description: config
.description
.clone()
.unwrap_or_else(|| run.to_string()),
})
}
}
pub trait PrepareProvider: Debug + Send + Sync {
fn base(&self) -> &providers::ProviderBase;
fn id(&self) -> &str {
&self.base().id
}
fn sources(&self) -> Vec<PathBuf>;
fn outputs(&self) -> Vec<PathBuf>;
fn prepare_command(&self) -> Result<PrepareCommand>;
fn is_applicable(&self) -> bool;
fn is_auto(&self) -> bool {
self.base().is_auto()
}
fn depends(&self) -> Vec<String> {
self.base().config.depends.clone()
}
fn timeout(&self) -> Option<std::time::Duration> {
self.base().config.timeout.as_deref().and_then(|t| {
match crate::duration::parse_duration(t) {
Ok(d) => Some(d),
Err(err) => {
warn!("prepare: {}: invalid timeout {t:?}: {err}", self.id());
None
}
}
})
}
}
pub fn notify_if_stale(config: &Arc<Config>) {
if *env::__MISE_SHIM || Settings::get().quiet {
return;
}
if !Settings::get().status.show_prepare_stale {
return;
}
let Ok(engine) = PrepareEngine::new(config) else {
return;
};
let stale = engine.check_staleness();
if !stale.is_empty() {
let providers: Vec<String> = stale
.iter()
.map(|(id, reason)| format!("{id} ({reason})"))
.collect();
let summary = providers.join(", ");
warn!("prepare: {summary} — run `mise prep`");
}
}
static STALE_OUTPUTS: LazyLock<Mutex<HashSet<PathBuf>>> =
LazyLock::new(|| Mutex::new(HashSet::new()));
pub fn mark_output_stale(path: PathBuf) {
if let Ok(mut set) = STALE_OUTPUTS.lock() {
set.insert(path);
}
}
pub fn is_output_stale(path: &PathBuf) -> bool {
STALE_OUTPUTS
.lock()
.map(|set| set.contains(path))
.unwrap_or(false)
}
pub fn clear_output_stale(path: &PathBuf) {
if let Ok(mut set) = STALE_OUTPUTS.lock() {
set.remove(path);
}
}
pub fn detect_applicable_providers(project_root: &Path) -> Vec<String> {
use providers::*;
use rule::PrepareProviderConfig;
let default_config = PrepareProviderConfig::default();
let mut applicable = Vec::new();
let checks: &[(&str, Box<dyn PrepareProvider>)] = &[
(
"npm",
Box::new(NpmPrepareProvider::new(
project_root,
default_config.clone(),
)),
),
(
"yarn",
Box::new(YarnPrepareProvider::new(
project_root,
default_config.clone(),
)),
),
(
"pnpm",
Box::new(PnpmPrepareProvider::new(
project_root,
default_config.clone(),
)),
),
(
"bun",
Box::new(BunPrepareProvider::new(
project_root,
default_config.clone(),
)),
),
(
"go",
Box::new(GoPrepareProvider::new(project_root, default_config.clone())),
),
(
"pip",
Box::new(PipPrepareProvider::new(
project_root,
default_config.clone(),
)),
),
(
"poetry",
Box::new(PoetryPrepareProvider::new(
project_root,
default_config.clone(),
)),
),
(
"uv",
Box::new(UvPrepareProvider::new(project_root, default_config.clone())),
),
(
"bundler",
Box::new(BundlerPrepareProvider::new(
project_root,
default_config.clone(),
)),
),
(
"composer",
Box::new(ComposerPrepareProvider::new(
project_root,
default_config.clone(),
)),
),
(
"git-submodule",
Box::new(GitSubmodulePrepareProvider::new(
project_root,
default_config.clone(),
)),
),
];
for (name, provider) in checks {
if provider.is_applicable() {
applicable.push(name.to_string());
}
}
applicable
}