use crate::errors::Error;
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::iter::once;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use super::args::ToolArg;
use crate::cli::{render_subcommand_help, unescape_task_args};
use crate::config::{Config, Settings};
use crate::deps::{DepsEngine, DepsOptions, DepsStepResult};
use crate::duration;
use crate::env;
use crate::file::display_path;
use crate::task::has_any_usage_spec;
use crate::task::task_executor::TaskRunContext;
use crate::task::task_helpers::task_needs_permit;
use crate::task::task_list::{get_task_lists, resolve_depends};
use crate::task::task_output::TaskOutput;
use crate::task::task_output_handler::OutputHandler;
use crate::task::{Deps, Task, TaskCacheMode, usage_command_for_args};
use crate::toolset::{InstallOptions, ResolveOptions, ToolVersion, ToolsetBuilder};
use crate::ui::{ctrlc, info, style};
use bytesize::ByteSize;
use eyre::{Context, Result, bail, eyre};
use futures_util::FutureExt;
use itertools::Itertools;
use serde::Serialize;
use std::panic::AssertUnwindSafe;
use tokio::sync::Mutex;
#[derive(usage_rs::Args)]
#[usage(
visible_alias = "r",
verbatim_doc_comment,
disable_help_flag = true,
after_long_help = AFTER_LONG_HELP,
unknown_flags = "value"
)]
pub(crate) struct Run {
#[usage(double_dash = "automatic", verbatim_doc_comment)]
pub task: Option<String>,
#[usage()]
pub args: Vec<String>,
#[usage(hide = true, double_dash = "required")]
pub args_last: Vec<String>,
#[usage(long, verbatim_doc_comment)]
pub affected: bool,
#[usage(long, requires = "affected", value_name = "REV", verbatim_doc_comment)]
pub affected_base: Option<String>,
#[usage(
long,
requires = "affected",
conflicts = "affected_json",
verbatim_doc_comment
)]
pub affected_explain: bool,
#[usage(long, requires = "affected", value_name = "REV", verbatim_doc_comment)]
pub affected_head: Option<String>,
#[usage(
long,
requires = "affected",
conflicts = "affected_explain",
verbatim_doc_comment
)]
pub affected_json: bool,
#[usage(long, conflicts = ["task", "affected"], verbatim_doc_comment)]
pub all: bool,
#[usage(long, short = 'c', verbatim_doc_comment)]
pub continue_on_error: bool,
#[usage(short = 'C', long, value_hint = ValueHint::DirPath)]
pub cd: Option<PathBuf>,
#[usage(long, short, verbatim_doc_comment)]
pub force: bool,
#[usage(long, short, env = "MISE_JOBS", verbatim_doc_comment)]
pub jobs: Option<usize>,
#[usage(long, short = 'n', verbatim_doc_comment)]
pub dry_run: bool,
#[usage(short, long, verbatim_doc_comment, env = "MISE_TASK_OUTPUT")]
pub output: Option<TaskOutput>,
#[usage(long, short, verbatim_doc_comment, env = "MISE_QUIET")]
pub quiet: bool,
#[usage(long, short, verbatim_doc_comment)]
pub raw: bool,
#[usage(long, short, verbatim_doc_comment)]
pub shell: Option<String>,
#[usage(long, short = 'S', verbatim_doc_comment, env = "MISE_SILENT")]
pub silent: bool,
#[usage(short, long, value_name = "TOOL@VERSION")]
pub tool: Vec<ToolArg>,
#[usage(skip)]
pub is_linear: bool,
#[usage(long, value_name = "VAR", verbatim_doc_comment)]
pub allow_env: Vec<String>,
#[usage(long, value_name = "HOST", verbatim_doc_comment)]
pub allow_net: Vec<String>,
#[usage(long, value_name = "PATH", verbatim_doc_comment)]
pub allow_read: Vec<std::path::PathBuf>,
#[usage(long, value_name = "PATH", verbatim_doc_comment)]
pub allow_write: Vec<std::path::PathBuf>,
#[usage(long, verbatim_doc_comment)]
pub deny_all: bool,
#[usage(long, verbatim_doc_comment)]
pub deny_env: bool,
#[usage(long, verbatim_doc_comment)]
pub deny_net: bool,
#[usage(long, verbatim_doc_comment)]
pub deny_read: bool,
#[usage(long, verbatim_doc_comment)]
pub deny_write: bool,
#[usage(long)]
pub fresh_env: bool,
#[usage(long, verbatim_doc_comment, env = "MISE_TASK_REMOTE_NO_CACHE")]
pub no_cache: bool,
#[usage(long)]
pub no_deps: bool,
#[usage(long, alias = "no-timing", verbatim_doc_comment)]
pub no_timings: bool,
#[usage(long, verbatim_doc_comment, env = "MISE_TASK_SKIP_DEPENDS")]
pub skip_deps: bool,
#[usage(long, verbatim_doc_comment)]
pub skip_tools: bool,
#[usage(
long,
value_enum,
default = "read-write",
env = "MISE_TASK_CACHE",
verbatim_doc_comment
)]
pub task_cache: TaskCacheMode,
#[usage(long, verbatim_doc_comment)]
pub task_cache_explain: bool,
#[usage(
long,
requires = "dry_run",
conflicts = "task_cache_explain",
verbatim_doc_comment
)]
pub task_cache_explain_json: bool,
#[usage(long, conflicts = "dry_run", verbatim_doc_comment)]
pub task_cache_stats: bool,
#[usage(long, verbatim_doc_comment)]
pub timeout: Option<String>,
#[usage(long, alias = "timing", verbatim_doc_comment, hide = true)]
pub timings: bool,
#[usage(skip)]
pub tmpdir: PathBuf,
#[usage(skip)]
pub output_handler: Option<OutputHandler>,
#[usage(skip)]
pub context_builder: crate::task::task_context_builder::TaskContextBuilder,
#[usage(skip)]
pub executor: Option<crate::task::task_executor::TaskExecutor>,
#[usage(skip)]
pub cache_session: Option<crate::cache::session::CacheSession>,
}
fn affected_task_args(args: &[String]) -> Vec<String> {
let mut task = true;
args.iter()
.map(|arg| {
if arg == ":::" {
task = true;
return arg.clone();
}
if !task {
return arg.clone();
}
task = false;
if arg.starts_with("//")
|| arg.starts_with(':')
|| crate::task::is_workspace_project_task(arg)
{
arg.clone()
} else {
format!("//...:{arg}")
}
})
.collect()
}
async fn get_affected_task_list(
config: &Arc<Config>,
args: &[String],
only: bool,
base: Option<&str>,
head: Option<&str>,
explain: bool,
json: bool,
) -> Result<Vec<Task>> {
Settings::get().ensure_experimental("affected tasks")?;
let workspace_root = config
.monorepo_root()
.ok_or_else(|| eyre!("--affected requires a monorepo root configuration"))?;
let graph = config.workspace_project_graph()?;
let revisions = crate::task::workspace::git::WorkspaceGitRevisions::resolve(base, head);
let changed_paths = revisions.changed_paths(&workspace_root)?;
let global_inputs = config.monorepo_global_task_inputs().await?;
let git = crate::git::Git::new(&workspace_root);
let cargo = crate::task::workspace::cargo::CargoWorkspaceProvider;
let go = crate::task::workspace::go::GoWorkspaceProvider;
let node = crate::task::workspace::node::NodeWorkspaceProvider;
let uv = crate::task::workspace::uv::UvWorkspaceProvider;
let providers: [&dyn crate::task::workspace::WorkspaceProvider; 4] = [&cargo, &go, &node, &uv];
let mut regular_paths = BTreeSet::new();
let mut lockfile_projects = BTreeMap::<PathBuf, BTreeSet<_>>::new();
let mut comparison_base: Option<String> = None;
for path in changed_paths {
let Some(lockfile_candidates) =
graph.affected_projects_for_lockfile(&providers, &path, None, None)?
else {
regular_paths.insert(path);
continue;
};
if lockfile_candidates.is_empty() {
regular_paths.insert(path);
continue;
}
let comparison_base = match &comparison_base {
Some(base) => base.clone(),
None => {
let base = git.merge_base(&revisions.base, &revisions.head)?;
comparison_base = Some(base.clone());
base
}
};
let before = git.file_at_revision(&comparison_base, &path)?;
let after = git.file_at_revision(&revisions.head, &path)?;
if let Some(projects) = graph.affected_projects_for_lockfile(
&providers,
&path,
before.as_deref(),
after.as_deref(),
)? {
lockfile_projects.entry(path).or_default().extend(projects);
}
}
let affected = graph.affected_projects_for_changes(
&workspace_root,
regular_paths,
&global_inputs,
&lockfile_projects,
)?;
let affected_roots = affected
.projects()
.map(|(id, _)| id)
.filter_map(|id| graph.get(id))
.map(|project| crate::file::desymlink_path(&workspace_root.join(&project.root)))
.collect::<BTreeSet<_>>();
let args = affected_task_args(args);
let mut tasks = get_task_lists(config, &args, true, only, false).await?;
tasks.retain(|task| {
!task.global
&& task
.config_root
.as_deref()
.map(crate::file::desymlink_path)
.is_some_and(|root| affected_roots.contains(&root))
});
if json {
display_affected_json(&revisions, &workspace_root, &graph, &affected, &tasks)?;
} else if explain {
display_affected_explanation(&revisions, &workspace_root, &graph, &affected, &tasks)?;
}
Ok(tasks)
}
#[derive(Serialize)]
struct AffectedSelectionOutput<'a> {
base: &'a str,
head: &'a str,
projects: Vec<AffectedProjectOutput<'a>>,
tasks: Vec<AffectedTaskOutput<'a>>,
}
#[derive(Serialize)]
struct AffectedProjectOutput<'a> {
id: &'a crate::task::workspace::ProjectId,
root: &'a std::path::Path,
reasons: &'a BTreeSet<crate::task::workspace::AffectedProjectReason>,
}
#[derive(Serialize)]
struct AffectedTaskOutput<'a> {
name: &'a str,
projects: Vec<&'a crate::task::workspace::ProjectId>,
}
fn display_affected_json(
revisions: &crate::task::workspace::git::WorkspaceGitRevisions,
workspace_root: &std::path::Path,
graph: &crate::task::workspace::WorkspaceProjectGraph,
affected: &crate::task::workspace::AffectedProjects,
tasks: &[Task],
) -> Result<()> {
let mut projects_by_root = BTreeMap::<PathBuf, Vec<_>>::new();
let projects = affected
.projects()
.map(|(id, reasons)| {
let project = graph.get(id).expect("affected project exists in graph");
projects_by_root
.entry(crate::file::desymlink_path(
&workspace_root.join(&project.root),
))
.or_default()
.push(id);
AffectedProjectOutput {
id,
root: &project.root,
reasons,
}
})
.collect();
let mut tasks = tasks
.iter()
.map(|task| AffectedTaskOutput {
name: &task.display_name,
projects: task
.config_root
.as_deref()
.map(crate::file::desymlink_path)
.and_then(|root| projects_by_root.get(&root))
.cloned()
.unwrap_or_default(),
})
.collect::<Vec<_>>();
tasks.sort_by(|left, right| left.name.cmp(right.name));
let output = AffectedSelectionOutput {
base: &revisions.base,
head: &revisions.head,
projects,
tasks,
};
miseprintln!("{}", serde_json::to_string_pretty(&output)?);
Ok(())
}
fn display_affected_explanation(
revisions: &crate::task::workspace::git::WorkspaceGitRevisions,
workspace_root: &std::path::Path,
graph: &crate::task::workspace::WorkspaceProjectGraph,
affected: &crate::task::workspace::AffectedProjects,
tasks: &[Task],
) -> Result<()> {
use crate::task::workspace::AffectedProjectReason;
miseprintln!(
"Affected projects ({}...{}):{}",
revisions.base,
revisions.head,
if affected.is_empty() { " none" } else { "" }
);
let mut projects_by_root = BTreeMap::<PathBuf, Vec<_>>::new();
for (id, reasons) in affected.projects() {
let project = graph.get(id).expect("affected project exists in graph");
miseprintln!(" {} ({})", id, display_affected_path(&project.root));
for reason in reasons {
match reason {
AffectedProjectReason::ChangedPath { path } => {
miseprintln!(" changed path: {}", display_affected_path(path));
}
AffectedProjectReason::GlobalPath { path } => {
miseprintln!(" workspace-global path: {}", display_affected_path(path));
}
AffectedProjectReason::Lockfile { path } => {
miseprintln!(" lockfile change: {}", display_affected_path(path));
}
AffectedProjectReason::Dependent { dependency } => {
miseprintln!(" depends on affected project: {dependency}");
}
}
}
projects_by_root
.entry(crate::file::desymlink_path(
&workspace_root.join(&project.root),
))
.or_default()
.push(id);
}
miseprintln!(
"Affected tasks:{}",
if tasks.is_empty() { " none" } else { "" }
);
for task in tasks {
miseprintln!(" {}", display_affected_text(&task.display_name));
if let Some(ids) = task
.config_root
.as_deref()
.map(crate::file::desymlink_path)
.and_then(|root| projects_by_root.get(&root))
{
for id in ids {
miseprintln!(" affected project: {id}");
}
}
}
Ok(())
}
fn display_affected_path(path: &std::path::Path) -> String {
display_affected_text(&path.to_string_lossy())
}
fn display_affected_text(text: &str) -> String {
text.escape_debug().to_string()
}
impl Run {
pub(crate) async fn run(mut self) -> Result<()> {
if self.task.as_deref() == Some("-h") {
print!("{}", render_subcommand_help("run", false));
return Ok(());
}
if self.task.as_deref() == Some("--help") {
print!("{}", render_subcommand_help("run", true));
return Ok(());
}
let task = self.task.clone().unwrap_or_else(|| "default".to_string());
Settings::ensure_not_safe("running tasks")?;
self.args = unescape_task_args(&self.args);
self.args_last = unescape_task_args(&self.args_last);
if self.fresh_env {
env::reset_env_cache_key();
}
let has_help_in_task_args =
self.args.contains(&"--help".to_string()) || self.args.contains(&"-h".to_string());
let mut config = Config::get().await?;
if has_help_in_task_args {
let args = once(task.clone())
.chain(
self.args
.iter()
.filter(|a| *a != "--help" && *a != "-h")
.cloned(),
)
.collect_vec();
let task_list = get_task_lists(&config, &args, false, false, false).await?;
if let Some(task) = task_list.first() {
if !task.raw_args {
let spec = task.parse_usage_spec_for_display(&config).await?;
if has_any_usage_spec(&spec) {
println!("{}", render_usage_help(&spec, &self.args));
} else {
display_task_help(task)?;
}
return Ok(());
}
} else {
print!("{}", render_subcommand_help("run", true));
return Ok(());
}
}
if !self.skip_deps {
self.skip_deps = Settings::get().task.skip_depends;
}
time!("run init");
let tmpdir = tempfile::tempdir()?;
self.tmpdir = tmpdir.path().to_path_buf();
let args = if self.all {
vec![]
} else {
once(task).chain(self.args.clone()).collect_vec()
};
let mut task_list = if self.affected {
get_affected_task_list(
&config,
&args,
self.skip_deps,
self.affected_base.as_deref(),
self.affected_head.as_deref(),
self.affected_explain,
self.affected_json,
)
.await?
} else {
get_task_lists(&config, &args, true, self.skip_deps, self.all).await?
};
if self.affected_json {
return Ok(());
}
if !self.args_last.is_empty() {
for task in &mut task_list {
task.args.extend(self.args_last.clone());
task.trailing_args = self.args_last.clone();
}
}
let fetcher = crate::task::task_fetcher::TaskFetcher::new(self.no_cache);
fetcher.fetch_tasks(&config, &mut task_list).await?;
for task in &mut task_list {
let has_usage_deps = |raw: &Option<Vec<_>>| {
raw.as_ref()
.is_some_and(|r| r.iter().any(crate::task::dep_has_usage_ref))
};
if has_usage_deps(&task.depends_raw)
|| has_usage_deps(&task.depends_post_raw)
|| has_usage_deps(&task.wait_for_raw)
{
let usage_values = crate::task::parse_usage_values_from_task(&config, task).await?;
if !usage_values.is_empty() {
task.render_depends_with_usage(&config, &usage_values)
.await?;
}
}
}
time!("run get_task_lists");
let execution_tasks = task_list.clone();
let resolved_tasks = resolve_depends(&config, task_list).await?;
let subdir_configs: Vec<_> = resolved_tasks
.iter()
.filter_map(|task| task.cf.clone())
.collect();
let mut layered_subdir_configs = vec![];
let deps_engine = if self.no_deps {
None
} else if subdir_configs.is_empty() {
Some(DepsEngine::new(&config)?)
} else {
let mut deps_config_files = config.config_files.clone();
let selected_config_roots: HashSet<_> =
subdir_configs.iter().map(|cf| cf.config_root()).collect();
for config_root in subdir_configs.iter().map(|cf| cf.config_root()).unique() {
let (config_paths, idiomatic_filenames) =
crate::config::load_config_hierarchy_from_dir(&config_root).await?;
deps_config_files.extend(
crate::config::load_config_files_from_paths(
&config_paths,
&idiomatic_filenames,
)
.await?,
);
}
deps_config_files.retain(|_, cf| {
let config_root = cf.config_root();
cf.project_root().is_some()
&& selected_config_roots.contains(&config_root)
&& config.project_root.as_ref() != Some(&config_root)
});
layered_subdir_configs.extend(deps_config_files.values().cloned());
Some(DepsEngine::new_task_monorepo(
&config,
deps_config_files.into_values(),
)?)
};
let mut combined_configs = config.config_files.clone();
for cf in layered_subdir_configs {
combined_configs
.entry(cf.get_path().to_path_buf())
.or_insert(cf);
}
for cf in &subdir_configs {
combined_configs
.entry(cf.get_path().to_path_buf())
.or_insert_with(|| cf.clone());
}
let resolve_options = ResolveOptions {
offline: true,
..Default::default()
};
let mut ts = ToolsetBuilder::new()
.with_args(&self.tool)
.with_default_to_latest(true)
.with_config_files(combined_configs)
.with_resolve_options(resolve_options)
.build(&config)
.await?;
let opts = InstallOptions {
jobs: self.jobs,
raw: self.raw,
dry_run: self.dry_run,
missing_args_only: !Settings::get().task.run_auto_install,
skip_auto_install: !Settings::get().task.run_auto_install
|| !Settings::get().auto_install,
..Default::default()
};
let previewed_tools = if !self.skip_tools {
let (installed, _) = ts.install_missing_versions(&mut config, &opts).await?;
if self.dry_run {
installed.into_iter().collect()
} else {
HashSet::new()
}
} else {
HashSet::new()
};
if let Some(engine) = deps_engine {
let env = ts.env_with_path(&config).await?;
let result = engine
.run(DepsOptions {
auto_only: true, dry_run: self.dry_run,
env,
..Default::default()
})
.await?;
for step in result.steps {
if let DepsStepResult::WouldRun(id, reason) = step {
info!("[dry-run] Would install dependency: {id} ({reason})");
}
}
}
let timeout = if let Some(timeout_str) = &self.timeout {
Some(duration::parse_duration(timeout_str)?)
} else {
Settings::get().task_timeout_duration()
};
if let Some(timeout) = timeout {
tokio::time::timeout(
timeout,
self.parallelize_tasks(config, execution_tasks, previewed_tools),
)
.await
.map_err(|_| eyre!("mise run timed out after {:?}", timeout))??
} else {
self.parallelize_tasks(config, execution_tasks, previewed_tools)
.await?
}
time!("run done");
Ok(())
}
async fn parallelize_tasks(
mut self,
mut config: Arc<Config>,
tasks: Vec<Task>,
previewed_tools: HashSet<ToolVersion>,
) -> Result<()> {
time!("parallelize_tasks start");
let tasks = self.prepare_tasks(&config, tasks).await?;
let num_tasks = tasks.all().count();
self.setup_output_and_validate(&tasks)?;
self.output = Some(self.output(None));
if !self.skip_tools {
self.install_task_tools(&mut config, &tasks, &previewed_tools)
.await?;
}
self.setup_cache_session(&tasks).await?;
self.setup_executor()?;
let executor = self.executor.as_ref().expect("task executor initialized");
for task in tasks.all() {
if let Err(err) = executor
.preflight_task_usage(&config, task)
.await
.wrap_err_with(|| format!("failed to validate task {}", task.name))
{
if let Some(session) = &self.cache_session
&& let Err(finish_err) = session.finish().await
{
warn!("failed to finish action cache session: {finish_err:#}");
}
return Err(err);
}
}
ctrlc::exit_on_ctrl_c(false);
let timer = std::time::Instant::now();
let this = Arc::new(self);
let config = config.clone();
let mut scheduler = crate::task::task_scheduler::Scheduler::new(this.jobs());
let main_deps = Arc::new(Mutex::new(tasks));
let mut main_done_rx = scheduler.pump_deps(main_deps.clone()).await;
let spawn_context = scheduler.spawn_context(config.clone());
scheduler
.run_loop(
&mut main_done_rx,
main_deps.clone(),
|| this.is_stopping(),
ctrlc::is_cancelled,
this.continue_on_error,
|task, deps_for_remove, allow_during_interruption| {
let this = this.clone();
let spawn_context = spawn_context.clone();
async move {
Self::spawn_sched_job(
this,
task,
deps_for_remove,
allow_during_interruption,
spawn_context,
)
.await
}
},
)
.await?;
let join_result = scheduler.join_all(this.continue_on_error).await;
if let Some(session) = &this.cache_session {
crate::cache::session::display_stats(session.finish().await?);
}
join_result?;
let results_display = crate::task::task_results_display::TaskResultsDisplay::new(
this.output_handler.clone().unwrap(),
this.executor.as_ref().unwrap().failed_tasks.clone(),
this.continue_on_error,
this.timings(),
this.is_interrupted(),
);
let result = results_display.display_results(num_tasks, timer);
if this.task_cache_stats {
this.display_task_cache_stats();
}
result?;
time!("parallelize_tasks done");
Ok(())
}
async fn spawn_sched_job(
this: Arc<Self>,
task: Task,
deps_for_remove: Arc<Mutex<Deps>>,
inherited_allow_during_interruption: bool,
ctx: crate::task::task_scheduler::SpawnContext,
) -> Result<()> {
if Self::should_abort_while_stopping(
&this,
&task,
&deps_for_remove,
inherited_allow_during_interruption,
)
.await
{
trace!(
"aborting spawn before start while stopping: {} {}",
task.name,
task.args.join(" ")
);
return Ok(());
}
let needs_permit = task_needs_permit(&task);
let permit_opt = if needs_permit {
let wait_start = std::time::Instant::now();
let p = Some(ctx.semaphore.clone().acquire_owned().await?);
trace!(
"semaphore acquired for {} after {}ms",
task.name,
wait_start.elapsed().as_millis()
);
if Self::should_abort_while_stopping(
&this,
&task,
&deps_for_remove,
inherited_allow_during_interruption,
)
.await
{
trace!(
"aborting spawn after wait while stopping: {} {}",
task.name,
task.args.join(" ")
);
return Ok(());
}
p
} else {
trace!("no semaphore needed for orchestrator task: {}", task.name);
None
};
ctx.in_flight
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let in_flight_c = ctx.in_flight.clone();
trace!("running task: {task}");
let allow_during_interruption = inherited_allow_during_interruption
|| deps_for_remove.lock().await.is_runnable_post_dep(&task);
deps_for_remove.lock().await.mark_executed(&task);
let semaphore = ctx.semaphore.clone();
ctx.jset.lock().await.spawn(async move {
let mut permit = permit_opt;
let (completion_state, dependency_state) = {
let deps = deps_for_remove.lock().await;
(deps.completion_state(), deps.dependency_state(&task))
};
let (result, panicked) = match AssertUnwindSafe(this.run_task_sched(TaskRunContext {
task: &task,
config: &ctx.config,
sched_tx: ctx.sched_tx.clone(),
completion_state,
dependency_state,
semaphore,
permit: &mut permit,
allow_during_interruption,
}))
.catch_unwind()
.await
{
Ok(result) => (result, false),
Err(payload) => (
Err(eyre!("task panicked: {}", panic_payload_message(&payload))),
true,
),
};
if let Ok(outcome) = &result {
let mut deps = deps_for_remove.lock().await;
if outcome.did_work && !task.sources.is_empty() {
deps.mark_did_work(&task);
}
if let Some(cache_key) = &outcome.cache_key {
deps.mark_cache_key(&task, cache_key.clone());
}
}
let interrupted = result.as_ref().is_err_and(|err| {
if panicked {
return false;
}
Error::is_sigint(err)
|| (ctrlc::is_cancelled() && Error::is_task_interrupted_before_start(err))
});
if let Err(err) = &result {
if interrupted {
this.mark_interrupted();
}
let status = if panicked {
Some(1)
} else {
Error::get_exit_status(err)
};
if !interrupted && !this.is_stopping() && (panicked || status.is_none()) {
let prefix = task.estyled_prefix();
if Settings::get().verbose {
this.eprint(&task, &prefix, &format!("{} {err:?}", style::ered("ERROR")));
} else {
this.eprint(&task, &prefix, &format!("{} {err}", style::ered("ERROR")));
let mut current_err = err.source();
while let Some(e) = current_err {
this.eprint(&task, &prefix, &format!("{} {e}", style::ered("ERROR")));
current_err = e.source();
}
};
}
if !interrupted {
this.add_failed_task(task.clone(), status);
}
if !interrupted && !this.continue_on_error {
debug!("task {} failed, killing siblings", task.name);
#[cfg(unix)]
crate::cmd::CmdLineRunner::kill_all(nix::sys::signal::SIGTERM);
#[cfg(windows)]
crate::cmd::CmdLineRunner::kill_all();
}
}
if let Some(oh) = &this.output_handler
&& oh.output(Some(&task)) == TaskOutput::KeepOrder
{
oh.keep_order_state.lock().unwrap().on_task_finished(&task);
}
let mut deps = deps_for_remove.lock().await;
if result
.as_ref()
.is_err_and(Error::is_task_interrupted_before_start)
{
deps.unmark_executed(&task);
}
deps.remove(&task);
drop(deps);
trace!("deps removed: {} {}", task.name, task.args.join(" "));
in_flight_c.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
if interrupted {
Ok(())
} else {
result.map(|_| ())
}
});
Ok(())
}
async fn should_abort_while_stopping(
this: &Self,
task: &Task,
deps_for_remove: &Arc<Mutex<Deps>>,
inherited_allow_during_interruption: bool,
) -> bool {
if !this.is_stopping()
|| (this.continue_on_error && !ctrlc::is_cancelled())
|| inherited_allow_during_interruption
{
return false;
}
let mut deps = deps_for_remove.lock().await;
if deps.is_runnable_post_dep(task) {
return false;
}
deps.remove(task);
true
}
async fn prepare_tasks(&mut self, config: &Arc<Config>, mut tasks: Vec<Task>) -> Result<Deps> {
let fetcher = crate::task::task_fetcher::TaskFetcher::new(self.no_cache);
fetcher.fetch_tasks(config, &mut tasks).await?;
let mut tasks = Deps::new(config, tasks).await?;
tasks.mark_ambiguous_prefixes();
self.is_linear = tasks.is_linear();
Ok(tasks)
}
fn setup_output_and_validate(&mut self, tasks: &Deps) -> Result<()> {
let output_config = crate::task::task_output_handler::OutputHandlerConfig {
output: self.output,
silent: self.silent,
quiet: self.quiet,
raw: self.raw,
is_linear: self.is_linear,
jobs: self.jobs,
};
self.output_handler = Some(OutputHandler::new(output_config));
let any_timed = tasks
.all()
.any(|task| self.output(Some(task)) == TaskOutput::Timed);
if any_timed {
let timed_outputs = self.output_handler.as_ref().unwrap().timed_outputs.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_millis(100));
loop {
{
let mut outputs = timed_outputs.lock().unwrap();
for (prefix, out) in outputs.clone() {
let (time, lines) = out;
if time.elapsed().unwrap().as_secs() >= 1 {
for line in lines {
if console::colors_enabled() {
prefix_println!(prefix, "{line}\x1b[0m");
} else {
prefix_println!(prefix, "{line}");
}
}
outputs.shift_remove(&prefix);
}
}
}
interval.tick().await;
}
});
}
for task in tasks.all() {
self.validate_task(task)?;
self.output_handler.as_mut().unwrap().init_task(task);
}
Ok(())
}
fn setup_executor(&mut self) -> Result<()> {
let executor_config = crate::task::task_executor::TaskExecutorConfig {
force: self.force,
cd: self.cd.clone(),
shell: self.shell.clone(),
tool: self.tool.clone(),
timings: self.timings,
continue_on_error: self.continue_on_error,
dry_run: self.dry_run,
skip_deps: self.skip_deps,
task_cache: self.task_cache,
task_cache_explain: self.task_cache_explain,
task_cache_explain_json: self.task_cache_explain_json,
cache_session: self
.cache_session
.as_ref()
.map(crate::cache::session::CacheSession::environment),
sandbox: crate::sandbox::SandboxConfig::from_settings_and_cli(
&Settings::get().sandbox,
self.deny_all,
crate::sandbox::SandboxConfig {
deny_read: self.deny_read,
deny_write: self.deny_write,
deny_net: self.deny_net,
deny_env: self.deny_env,
allow_read: self.allow_read.clone(),
allow_write: self.allow_write.clone(),
allow_net: self.allow_net.clone(),
allow_env: self.allow_env.clone(),
pass_through_env: vec![],
cache_env: vec![],
},
),
};
self.executor = Some(crate::task::task_executor::TaskExecutor::new(
self.context_builder.clone(),
self.output_handler.clone().unwrap(),
executor_config,
));
Ok(())
}
async fn setup_cache_session(&mut self, tasks: &Deps) -> Result<()> {
let enabled = !self.dry_run
&& tasks
.all()
.any(|task| task.rust_cache.as_ref().is_some_and(|cache| cache.enabled));
if !enabled {
return Ok(());
}
if crate::cache::release_cache_context() {
warn!("Rust action caching is disabled for release CI contexts");
return Ok(());
}
self.cache_session = Some(
crate::cache::session::CacheSession::start(
&self.tmpdir,
crate::task::task_cache::task_cache_dir().join("actions"),
)
.await?,
);
Ok(())
}
async fn install_task_tools(
&self,
config: &mut Arc<Config>,
tasks: &Deps,
previewed_tools: &HashSet<ToolVersion>,
) -> Result<()> {
let installer = crate::task::task_tool_installer::TaskToolInstaller::new(
&self.context_builder,
&self.tool,
);
installer
.install_tools(config, tasks, self.dry_run, previewed_tools)
.await
}
fn eprint(&self, task: &Task, prefix: &str, line: &str) {
self.output_handler
.as_ref()
.unwrap()
.eprint(task, prefix, line);
}
fn output(&self, task: Option<&Task>) -> TaskOutput {
self.output_handler.as_ref().unwrap().output(task)
}
fn jobs(&self) -> usize {
self.output_handler.as_ref().unwrap().jobs()
}
fn is_stopping(&self) -> bool {
ctrlc::is_cancelled()
|| self
.executor
.as_ref()
.map(|e| e.is_stopping())
.unwrap_or(false)
}
fn is_interrupted(&self) -> bool {
ctrlc::is_cancelled()
|| self
.executor
.as_ref()
.map(|e| e.is_interrupted())
.unwrap_or(false)
}
fn mark_interrupted(&self) {
if let Some(executor) = &self.executor {
executor.mark_interrupted();
}
}
async fn run_task_sched(
&self,
ctx: TaskRunContext<'_>,
) -> Result<crate::task::task_executor::TaskRunOutcome> {
self.executor
.as_ref()
.expect("executor must be initialized before running tasks")
.run_task_sched(ctx)
.await
}
fn add_failed_task(&self, task: Task, status: Option<i32>) {
if let Some(executor) = &self.executor {
executor.add_failed_task(task, status);
}
}
fn validate_task(&self, task: &Task) -> Result<()> {
use crate::file;
use crate::ui;
if self.task_cache.enabled() && task.cache.as_ref().is_some_and(|cache| cache.enabled) {
Settings::get().ensure_experimental("task artifact caching")?;
}
if task.rust_cache.as_ref().is_some_and(|cache| cache.enabled) {
Settings::get().ensure_experimental("Rust action caching")?;
}
if !task.pass_through_env.is_empty() {
Settings::get().ensure_experimental("task environment pass-through")?;
}
if let Some(path) = &task.file
&& path.exists()
&& !file::is_executable(path)
{
let dp = crate::file::display_path(path);
if cfg!(windows) {
bail!(
"`{dp}` is not executable. {}",
file::make_executable_hint(path)
)
}
let msg = format!("Script `{dp}` is not executable. Make it executable?");
if ui::confirm(msg)?.is_yes() {
file::make_executable(path)?;
} else {
bail!(
"`{dp}` is not executable. {}",
file::make_executable_hint(path)
)
}
}
Ok(())
}
fn timings(&self) -> bool {
!self.quiet(None) && !self.no_timings
}
fn display_task_cache_stats(&self) {
let stats = *self
.executor
.as_ref()
.expect("executor must be initialized before displaying cache stats")
.cache_stats
.lock()
.unwrap();
let lookups = stats.hits.saturating_add(stats.misses);
if lookups == 0 {
safe_eprintln!("Task cache: no lookups");
return;
}
let hit_rate = stats.hits.saturating_mul(100) / lookups;
safe_eprintln!(
"Task cache: {}/{} hits ({}%), {} restored, {} saved",
stats.hits,
lookups,
hit_rate,
ByteSize::b(stats.restored_bytes).display().iec(),
crate::ui::time::format_duration(stats.time_saved),
);
}
fn quiet(&self, task: Option<&Task>) -> bool {
self.output_handler.as_ref().unwrap().quiet(task)
}
}
fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> &str {
if let Some(message) = payload.downcast_ref::<&'static str>() {
message
} else if let Some(message) = payload.downcast_ref::<String>() {
message.as_str()
} else {
"unknown panic payload"
}
}
fn display_task_help(task: &Task) -> Result<()> {
let name = if task.display_name.is_empty() {
&task.name
} else {
&task.display_name
};
info::inline_section("Task", name)?;
if !task.aliases.is_empty() {
info::inline_section("Aliases", task.aliases.join(", "))?;
}
if !task.description.is_empty() {
info::inline_section("Description", &task.description)?;
}
info::inline_section(
"Source",
task.config_sources().iter().map(display_path).join(", "),
)?;
if !task.depends.is_empty() {
info::inline_section("Depends on", task.depends.iter().join(", "))?;
}
let run = task.run();
if !run.is_empty() {
info::section("Run", run.iter().map(|e| e.to_string()).join("\n"))?;
}
miseprintln!();
miseprintln!("This task does not accept any arguments.");
let hint = if task.file.is_some() {
"To define arguments, add #USAGE comments to the script file."
} else {
"To define arguments, add a `usage` field to the task definition in the config file."
};
miseprintln!("{hint}");
miseprintln!("See https://mise.jdx.dev/tasks/task-configuration.html for more information.");
Ok(())
}
fn render_usage_help(spec: &usage::Spec, args: &[String]) -> String {
let cmd = usage_command_for_args(spec, args);
usage::docs::cli::render_help(spec, cmd, true)
}
static AFTER_LONG_HELP: &str = color_print::cstr!(
r#"<bold><underline>Examples:</underline></bold>
# Runs the "lint" tasks. This needs to either be defined in mise.toml
# or as a standalone script. See the project README for more information.
$ <bold>mise run lint</bold>
# Forces the "build" tasks to run even if its sources are up-to-date.
$ <bold>mise run --force build</bold>
# Run "test" with stdin/stdout/stderr all connected to the current terminal.
# This forces `--jobs=1` to prevent interleaving of output.
$ <bold>mise run --raw test</bold>
# Runs the "lint", "test", and "check" tasks in parallel.
$ <bold>mise run lint ::: test ::: check</bold>
# Execute multiple tasks each with their own arguments.
$ <bold>mise run cmd1 arg1 arg2 ::: cmd2 arg1 arg2</bold>
"#
);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_panic_payload_message_from_static_str() {
let payload: Box<dyn std::any::Any + Send> = Box::new("panic message");
assert_eq!(panic_payload_message(&*payload), "panic message");
}
#[test]
fn test_panic_payload_message_from_string() {
let payload: Box<dyn std::any::Any + Send> = Box::new(String::from("panic message"));
assert_eq!(panic_payload_message(&*payload), "panic message");
}
#[test]
fn test_panic_payload_message_from_unknown_payload() {
let payload: Box<dyn std::any::Any + Send> = Box::new(123usize);
assert_eq!(panic_payload_message(&*payload), "unknown panic payload");
}
#[test]
fn affected_patterns_expand_across_projects_and_preserve_arguments() {
assert_eq!(
affected_task_args(&[
"build".into(),
"--release".into(),
":::".into(),
"//apps/...:test".into(),
"unit".into(),
":::".into(),
"node:@scope/app#lint".into(),
]),
vec![
"//...:build",
"--release",
":::",
"//apps/...:test",
"unit",
":::",
"node:@scope/app#lint",
]
);
}
#[test]
fn affected_paths_escape_terminal_control_characters() {
assert_eq!(
display_affected_path(std::path::Path::new("src/\x1b[2J\nfile.rs")),
r"src/\u{1b}[2J\nfile.rs"
);
assert_eq!(
display_affected_text("//app:\x1b]8;;https://example.com\x1b\\build"),
r"//app:\u{1b}]8;;https://example.com\u{1b}\\build"
);
}
}