use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::process::Command; use crate::db::{is_executable, read_extension, read_or_none}; use crate::dvcs::{Backend, Repo}; use crate::log; #[derive(Debug)] pub struct Task { pub name: String, pub bin: PathBuf, pub source: Option, pub repo: Option, pub config: HashMap, pub branch: Option, pub hosts: Vec, pub cloned: bool, pub subupdates: bool, pub context: log::Context, } /// config returns an option of (settings directory, ignored tasks) as /// (PathBuf, Vec) pub fn config(basedir: &Path) -> (PathBuf, Vec) { let hostname = std::env::var("HOST").unwrap_or_else(|_| hostname::get().unwrap().into_string().unwrap()); let path = basedir.join(hostname); if path.is_dir() { let ignored = path .read_dir() .unwrap() .filter_map(|x| { if x.is_err() { return None; } let name = x.unwrap().file_name().into_string().unwrap(); if name.ends_with(".ignore") { return Some(name.trim_end_matches(".ignore").to_string()); } return None; }) .collect(); (path, ignored) } else { // TODO: load .ignore in default config? (basedir.join("config"), Vec::new()) } } impl Task { pub fn from_path(path: &Path, context: &log::Context) -> Option { let name = path.file_name().unwrap().to_str().unwrap().to_string(); // We don't return a task if: // - the path is not a file // - the file is not executable (can't be run) // - the file starts with . (hidden file) if !path.is_file() || !is_executable(&path) || name.starts_with('.') { return None; } let basedir = path.parent().unwrap(); // Calling a task in / (FS root) will panic let source = read_extension(path, "source"); let dest = source_dir_from_basedir(&basedir, &name); let cloned = source.clone().map_or(false, |_| dest.is_dir()); let subupdates = read_extension(path, "subupdates").is_some(); let checkout = read_extension(path, "checkout"); // Copy the global context so we have a more local scope let mut context = context.clone(); context.insert("$i18n_task".to_string(), name.clone()); if let Some(branch) = &checkout { context.insert("$i18n_branch".to_string(), branch.to_string()); } if let Some(source_url) = &source { context.insert("$i18n_source".to_string(), source_url.clone()); } let dvcs = if let Some(dvcs) = Backend::from_setting(read_extension(path, "dvcs")) { dvcs } else { // TODO: forgebuild error message eprintln!("Unrecognized dvcs for task {}, possible options are 'git' or 'mercurial'. Skipped", &name); return None; }; Some(Task { name, bin: path.to_path_buf(), // None source = None repo repo: source.as_ref().map(|s| { Repo::new( dvcs, s, &dest, subupdates, ) }), source, config: HashMap::new(), branch: checkout, hosts: read_extension(path, "hosts").map_or(Vec::new(), |c| { c.split("\n").map(|line| line.to_string()).collect() }), cloned, subupdates: read_extension(path, "subupdates").is_some(), context, }) } pub fn checkout(&self) { if let Some(branch) = &self.branch { if let Some(repo) = &self.repo { self.info("to_branch"); self.debug("checkout"); repo.checkout(branch); } } } pub fn run_on_host(&self) -> bool { if self.hosts.len() == 0 { return true; } // $HOSTNAME env is a bashism, we need to call libc (through hostname crate) // to find out the actual hostname let hostname = std::env::var("HOST") .unwrap_or_else(|_| hostname::get().unwrap().into_string().unwrap()); if self.hosts.contains(&hostname) { return true; } return false; } pub fn update_and_run(&self, force: &bool) { if let Some(repo) = &self.repo { if repo.update() { self.run(); } else if *force { self.debug("forcing"); self.run(); } else { self.debug("no_update"); } } else { unreachable!("this function should never be called on a task whcih doesnt have a repo"); } } pub fn run(&self) { if !self.run_on_host() { // TODO: Skip host debug? return; } self.info("run"); let cmd_out = Command::new("bash") // TODO: no need to call bash? .arg(&self.bin) .arg(&self.name) .output() .expect(&format!("Failed to run {:?}", &self.bin)); let mut log_path = self.bin.clone(); log_path.set_extension("log"); std::fs::write(&log_path, cmd_out.stderr) .expect(&format!("Failed to write log to {:?}", &log_path)); } pub fn run_once(&self) { if !self.run_on_host() { return; } let mut done_path = self.bin.clone(); done_path.set_extension("done"); if !done_path.exists() { self.run(); std::fs::write(&done_path, "").expect("Failed to register task as done"); } } #[allow(dead_code)] pub fn debug(&self, message: &str) { log::debug(message, &self.context); } #[allow(dead_code)] pub fn info(&self, message: &str) { log::info(message, &self.context); } #[allow(dead_code)] pub fn warn(&self, message: &str) { log::warn(message, &self.context); } #[allow(dead_code)] pub fn error(&self, message: &str) { log::error(message, &self.context); } } /// Loads a task list from a given base directory. Fails if the directory /// is not readable with std::io::Error. pub fn from_dir(basedir: &Path, context: &log::Context) -> Result, std::io::Error> { Ok( basedir.read_dir()?.filter_map(|f| { if f.is_err() { // Dismiss individual file errors return None; } return Task::from_path(&f.unwrap().path(), context); }).collect() ) } pub struct MissingTask(pub String); /// Contains a mapping of sources to their corresponding tasks pub struct SourceSet { mapping: HashMap>, } impl SourceSet { /// Loads a SourceSet from a basedir pub fn from(basedir: &Path) -> Result { let source_urls = basedir.read_dir()?.filter_map(|p| { if p.is_err() { return None; } // Skip individual errors let p = p.unwrap().path(); let path_str = p.to_str().unwrap(); if !path_str.ends_with(".source") { // Filter out non-source files return None; } return Some(( path_str.trim_end_matches(".source").to_string(), // Task name read_or_none(&p).unwrap() // Source URL )); }); let mut sources_map: HashMap> = HashMap::new(); for (task, source) in source_urls { if let Some(list) = sources_map.get_mut(&source) { list.push(task.to_string()); } else { sources_map.insert(source.clone(), vec!(task.to_string())); } } Ok(SourceSet { mapping: sources_map }) } /// Returns the task names associated with a given source pub fn tasks_for(&self, source: &str) -> Option> { self.mapping.get(source).map(|x| x.clone()) } } /// Loads a task list from a given base directory, taking only tasks that are in requested list. /// Given tasks can be either a task name or a task URL. This function will panic if the basedir /// does not exist, or error if a requested task/source does not exist. pub fn from_dir_and_list(basedir: &Path, list: Vec, context: &log::Context) -> Result, MissingTask> { // Safe unwrap because we checked that basedir existed before // or maybe can crash for permission problem? TODO: write tests let sourceset = SourceSet::from(basedir).unwrap(); let mut tasks = Vec::new(); for t in list { if let Some(task) = Task::from_path(&basedir.join(&t), context) { tasks.push(task); } else { // Maybe it's not a task name, but a task URL? if let Some(list) = sourceset.tasks_for(&t) { // Hopefully safe unwrap (unless there's a source without a corresponding task?) let task_list = list.iter().map(|t_name| Task::from_path(&basedir.join(&t_name), context).unwrap()); tasks.extend(task_list); } else { return Err(MissingTask(t)); } } } Ok(tasks) } /// Takes a &Path to a basedir, and a &str task_name, and return /// the corresponding source directory as a PathBuf. Does not check /// if the target exists. pub fn source_dir_from_basedir(basedir: &Path, task_name: &str) -> PathBuf { basedir.join(&format!(".{}", task_name)) }