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::{DepsEngine, DepsOptions, DepsStepResult};
pub use rule::DepsConfig;
pub(crate) mod deps_ordering;
mod engine;
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 DepsCommand {
pub program: String,
pub args: Vec<String>,
pub env: BTreeMap<String, String>,
pub cwd: Option<PathBuf>,
pub description: String,
}
impl DepsCommand {
pub fn from_string(
run: &str,
project_root: &Path,
config: &rule::DepsProviderConfig,
) -> Result<Self> {
if run.trim().is_empty() {
bail!("deps 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 DepsProvider: 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 install_command(&self) -> Result<DepsCommand>;
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!("deps: {}: invalid timeout {t:?}: {err}", self.id());
None
}
}
})
}
fn add_command(&self, _packages: &[&str], _dev: bool) -> Result<DepsCommand> {
bail!("provider '{}' does not support adding packages", self.id())
}
fn remove_command(&self, _packages: &[&str]) -> Result<DepsCommand> {
bail!(
"provider '{}' does not support removing packages",
self.id()
)
}
}
pub fn notify_if_stale(config: &Arc<Config>) {
if *env::__MISE_SHIM || Settings::get().quiet {
return;
}
if !Settings::get().status.show_deps_stale {
return;
}
let Ok(engine) = DepsEngine::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!("deps: {summary} — run `mise deps`");
}
}
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::DepsProviderConfig;
let default_config = DepsProviderConfig::default();
let mut applicable = Vec::new();
let checks: &[(&str, Box<dyn DepsProvider>)] = &[
(
"npm",
Box::new(NpmDepsProvider::new(project_root, default_config.clone())),
),
(
"yarn",
Box::new(YarnDepsProvider::new(project_root, default_config.clone())),
),
(
"pnpm",
Box::new(PnpmDepsProvider::new(project_root, default_config.clone())),
),
(
"bun",
Box::new(BunDepsProvider::new(project_root, default_config.clone())),
),
(
"go",
Box::new(GoDepsProvider::new(project_root, default_config.clone())),
),
(
"pip",
Box::new(PipDepsProvider::new(project_root, default_config.clone())),
),
(
"poetry",
Box::new(PoetryDepsProvider::new(
project_root,
default_config.clone(),
)),
),
(
"uv",
Box::new(UvDepsProvider::new(project_root, default_config.clone())),
),
(
"bundler",
Box::new(BundlerDepsProvider::new(
project_root,
default_config.clone(),
)),
),
(
"composer",
Box::new(ComposerDepsProvider::new(
project_root,
default_config.clone(),
)),
),
(
"git-submodule",
Box::new(GitSubmoduleDepsProvider::new(
project_root,
default_config.clone(),
)),
),
];
for (name, provider) in checks {
if provider.is_applicable() {
applicable.push(name.to_string());
}
}
applicable
}
pub fn create_provider(
ecosystem: &str,
project_root: &Path,
config: Option<&crate::config::Config>,
) -> Result<Box<dyn DepsProvider>> {
let (provider_root, provider_config) = config
.and_then(|c| {
c.config_files.values().find_map(|cf| {
cf.deps_config()
.and_then(|dc| dc.providers.get(ecosystem).cloned())
.map(|provider_config| (cf.config_root(), provider_config))
})
})
.unwrap_or_else(|| {
(
project_root.to_path_buf(),
rule::DepsProviderConfig::default(),
)
});
DepsEngine::build_provider(ecosystem, &provider_root, provider_config)
.ok_or_else(|| eyre::eyre!("unknown deps provider '{ecosystem}'"))
}