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;
use crate::file::display_filename;
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,
OutputsMissing,
Stale(String),
NoSources,
Forced,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DepsProviderApplicability {
Applicable,
Inactive(String),
}
impl DepsProviderApplicability {
pub fn require_file(path: &Path) -> Self {
if path.is_file() {
Self::Applicable
} else {
let name = display_filename(path);
Self::Inactive(format!("missing {name}"))
}
}
pub fn require_any_file(paths: &[&Path]) -> Self {
if paths.iter().any(|path| path.is_file()) {
Self::Applicable
} else {
let names = paths
.iter()
.map(display_filename)
.collect::<Vec<_>>()
.join(" or ");
Self::Inactive(format!("missing {names}"))
}
}
pub fn require_nonempty_file(path: &Path) -> Self {
let name = display_filename(path);
if !path.is_file() {
return Self::Inactive(format!("missing {name}"));
}
if path.metadata().map(|m| m.len() > 0).unwrap_or(false) {
Self::Applicable
} else {
Self::Inactive(format!("empty {name}"))
}
}
pub fn require_run(run: Option<&str>) -> Self {
match run {
Some(run) if !run.trim().is_empty() => Self::Applicable,
Some(_) => Self::Inactive("run command is empty".to_string()),
None => Self::Inactive("missing run command".to_string()),
}
}
}
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::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 optional_outputs(&self) -> Vec<PathBuf> {
vec![]
}
fn install_command(&self) -> Result<DepsCommand>;
fn applicability(&self) -> DepsProviderApplicability;
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 DepsProviderApplicability::Applicable;
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())),
),
(
"deno",
Box::new(DenoDepsProvider::new(project_root, default_config.clone())),
),
(
"aube",
Box::new(AubeDepsProvider::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 matches!(provider.applicability(), 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}'"))
}