use self::task::CommandType;
use self::task::Task;
use self::TaskError as E;
use crate::config;
use crate::env::get_env;
use crate::tasks::task::TaskStatus;
use crate::utils::files;
use crate::utils::user::get_and_keep_sudo;
use camino::Utf8Path;
use camino::Utf8PathBuf;
use color_eyre::eyre::bail;
use color_eyre::eyre::eyre;
use color_eyre::eyre::Result;
use displaydoc::Display;
use itertools::Itertools;
use rayon::prelude::*;
use std::collections::HashMap;
use std::collections::HashSet;
use std::io;
use std::time::Duration;
use std::time::Instant;
use thiserror::Error;
use tracing::debug;
use tracing::error;
use tracing::info;
use tracing::trace;
use tracing::warn;
pub mod completions;
pub mod defaults;
pub mod git;
pub mod link;
pub mod task;
pub mod update_self;
pub trait ResolveEnv {
fn resolve_env<F>(&mut self, _env_fn: F) -> Result<(), E>
where
F: Fn(&str) -> Result<String, E>,
{
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub enum TasksAction {
Run,
List,
}
#[derive(Debug, Clone, Copy)]
pub enum TasksDir {
Tasks,
GenerateTasks,
}
impl TasksDir {
fn to_dir_name(self) -> String {
match self {
TasksDir::Tasks => "tasks".to_owned(),
TasksDir::GenerateTasks => "generate_tasks".to_owned(),
}
}
}
pub fn run(
config: &config::UpConfig,
tasks_dirname: TasksDir,
tasks_action: TasksAction,
) -> Result<()> {
let mut tasks_dir = config
.up_yaml_path
.as_ref()
.ok_or(E::UnexpectedNone)?
.clone();
tasks_dir.pop();
tasks_dir.push(tasks_dirname.to_dir_name());
let env = get_env(
config.config_yaml.inherit_env.as_ref(),
config.config_yaml.env.as_ref(),
)?;
#[cfg(target_os = "macos")]
{
use crate::cmd;
_ = cmd!("caffeinate", "-ds", "-w", &std::process::id().to_string()).start()?;
}
let bootstrap_tasks = match (config.bootstrap, &config.config_yaml.bootstrap_tasks) {
(false, _) => Ok(Vec::new()),
(true, None) => Err(eyre!(
"Bootstrap flag set but no bootstrap_tasks specified in config."
)),
(true, Some(b_tasks)) => Ok(b_tasks.clone()),
}?;
let filter_tasks_set: Option<HashSet<String>> =
config.tasks.clone().map(|v| v.into_iter().collect());
debug!("Filter tasks set: {filter_tasks_set:?}");
let excluded_tasks: HashSet<String> = config
.exclude_tasks
.clone()
.map_or_else(HashSet::new, |v| v.into_iter().collect());
debug!("Excluded tasks set: {excluded_tasks:?}");
let mut tasks: HashMap<String, task::Task> = HashMap::new();
for entry in tasks_dir.read_dir().map_err(|e| E::ReadDir {
path: tasks_dir.clone(),
source: e,
})? {
let entry = entry?;
if entry.file_type()?.is_dir() {
continue;
}
let path = Utf8PathBuf::try_from(entry.path())?;
if !path.exists() && path.symlink_metadata().is_ok() {
files::remove_broken_symlink(&path)?;
continue;
}
let task = task::Task::from(&path)?;
let name = &task.name;
if excluded_tasks.contains(name) {
debug!(
"Not running task '{name}' as it is in the excluded tasks set {excluded_tasks:?}"
);
continue;
}
if let Some(filter) = filter_tasks_set.as_ref() {
if !filter.contains(name) {
debug!("Not running task '{name}' as not in tasks filter {filter:?}",);
continue;
}
}
tasks.insert(name.clone(), task);
}
if matches!(tasks_action, TasksAction::Run)
&& tasks.values().any(|t| t.config.needs_sudo)
&& users::get_current_uid() != 0
{
get_and_keep_sudo(false)?;
}
debug!("Task count: {:?}", tasks.len());
trace!("Task list: {tasks:#?}");
match tasks_action {
TasksAction::List => println!("{}", tasks.keys().join("\n")),
TasksAction::Run => {
let run_tempdir = config.temp_dir.join(format!(
"runs/{start_time}",
start_time = config.start_time.to_rfc3339()
));
run_tasks(
bootstrap_tasks,
tasks,
&env,
&run_tempdir,
config.keep_going,
)?;
}
}
Ok(())
}
fn run_tasks(
bootstrap_tasks: Vec<String>,
mut tasks: HashMap<String, task::Task>,
env: &HashMap<String, String>,
temp_dir: &Utf8Path,
keep_going: bool,
) -> Result<()> {
let mut completed_tasks = Vec::new();
if !bootstrap_tasks.is_empty() {
for task_name in bootstrap_tasks {
let task_tempdir = create_task_tempdir(temp_dir, &task_name)?;
let task = run_task(
tasks
.remove(&task_name)
.ok_or_else(|| eyre!("Task '{task_name}' was missing."))?,
env,
&task_tempdir,
);
if !keep_going {
if let TaskStatus::Failed(e) = task.status {
bail!(e);
}
}
completed_tasks.push(task);
}
}
completed_tasks.extend(
tasks
.into_par_iter()
.filter(|(_, task)| task.config.auto_run.unwrap_or(true))
.map(|(_, task)| {
let task_name = task.name.as_str();
let _span = tracing::info_span!("task", task = task_name).entered();
let task_tempdir = create_task_tempdir(temp_dir, task_name)?;
Ok(run_task(task, env, &task_tempdir))
})
.collect::<Result<Vec<Task>>>()?,
);
let completed_tasks_len = completed_tasks.len();
let mut tasks_passed = Vec::new();
let mut tasks_skipped = Vec::new();
let mut tasks_failed = Vec::new();
let mut tasks_incomplete = Vec::new();
for task in completed_tasks {
match task.status {
TaskStatus::Failed(_) => {
tasks_failed.push(task);
}
TaskStatus::Passed => tasks_passed.push(task),
TaskStatus::Skipped => tasks_skipped.push(task),
TaskStatus::Incomplete => tasks_incomplete.push(task),
}
}
info!(
"Ran {completed_tasks_len} tasks, {} passed, {} failed, {} skipped",
tasks_passed.len(),
tasks_failed.len(),
tasks_skipped.len()
);
if !tasks_passed.is_empty() {
info!(
"Tasks passed: {:?}",
tasks_passed.iter().map(|t| &t.name).collect::<Vec<_>>()
);
}
if !tasks_skipped.is_empty() {
info!(
"Tasks skipped: {:?}",
tasks_skipped.iter().map(|t| &t.name).collect::<Vec<_>>()
);
}
if !tasks_failed.is_empty() {
error!("One or more tasks failed, exiting.");
error!(
"Tasks failed: {:#?}",
tasks_failed.iter().map(|t| &t.name).collect::<Vec<_>>()
);
let mut tasks_failed_iter = tasks_failed.into_iter().filter_map(|t| match t.status {
TaskStatus::Failed(e) => Some(e),
_ => None,
});
let err = tasks_failed_iter.next().ok_or(E::UnexpectedNone)?;
let err = eyre!(err);
tasks_failed_iter.fold(Err(err), color_eyre::Help::error)?;
}
Ok(())
}
fn run_task(mut task: Task, env: &HashMap<String, String>, task_tempdir: &Utf8Path) -> Task {
let env_fn = &|s: &str| {
let home_dir = files::home_dir().map_err(|e| E::EyreError { source: e })?;
let out = shellexpand::full_with_context(
s,
|| Some(home_dir),
|k| env.get(k).ok_or_else(|| eyre!("Value not found")).map(Some),
)
.map(std::borrow::Cow::into_owned)
.map_err(|e| E::ResolveEnv {
var: e.var_name,
source: e.cause,
})?;
Ok(out)
};
let now = Instant::now();
task.run(env_fn, env, task_tempdir);
let elapsed_time = now.elapsed();
if elapsed_time > Duration::from_secs(60) {
warn!("Task took {elapsed_time:?}");
}
task
}
fn create_task_tempdir(temp_dir: &Utf8Path, task_name: &str) -> Result<Utf8PathBuf> {
let task_tempdir = temp_dir.join(task_name);
files::create_dir_all(&task_tempdir)?;
Ok(task_tempdir)
}
#[allow(clippy::doc_markdown)]
#[derive(Error, Debug, Display)]
pub enum TaskError {
TaskError {
source: color_eyre::eyre::Error,
lib: String,
name: String,
},
ReadDir {
path: Utf8PathBuf,
source: io::Error,
},
ReadFile {
path: Utf8PathBuf,
source: io::Error,
},
EnvLookup {
var: String,
source: color_eyre::eyre::Error,
},
EmptyCmd,
MissingCmd {
name: String,
},
CmdFailed {
command_type: CommandType,
name: String,
source: io::Error,
cmd: Vec<String>,
suggestion: String,
},
CmdNonZero {
command_type: CommandType,
name: String,
cmd: Vec<String>,
code: i32,
output_file: Utf8PathBuf,
},
CmdTerminated {
command_type: CommandType,
name: String,
cmd: Vec<String>,
output_file: Utf8PathBuf,
},
UnexpectedNone,
InvalidYaml {
path: Utf8PathBuf,
source: serde_yaml::Error,
},
MissingHomeDir,
ResolveEnv {
var: String,
source: color_eyre::eyre::Error,
},
TaskDataRequired {
task: String,
},
DeserializeError {
source: serde_yaml::Error,
},
EyreError {
source: color_eyre::Report,
},
}