build.rs/src/task.rs

393 lines
14 KiB
Rust

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::backend::{Repo, backend};
use crate::log;
/// Defines how tasks are selected to be run
#[derive(Clone, Debug)]
pub enum Select {
/// Select all tasks (executable files) from a given basedir
All(PathBuf),
/// Select all tasks with a matching TASK.inbox from a basedir
Inbox(PathBuf),
/// Select all tasks from a basedir with a matching TASK.inbox
/// in a separate inbox directory.
InboxDir(PathBuf, String),
/// Select all tasks from a basedir that are also in a list
List(PathBuf, Vec<String>),
}
impl Select {
/// Removes the inbox file for the given task
pub fn clean(&self, task: &str) {
match self {
Select::Inbox(basedir) => {
std::fs::remove_file(&basedir.join(&format!("{}.inbox", task))).unwrap();
},
Select::InboxDir(_basedir, inboxdir) => {
std::fs::remove_file(&PathBuf::from(inboxdir).join(&format!("{}.inbox", task))).unwrap()
},
_ => {},
}
}
/// Find tasks matched by a selection
pub fn apply(&self, context: &log::Context) -> Result<Vec<Task>, MissingTask> {
// TODO: Dedicated error type for specific IO errors:
// - missing basedir/select
// - permissions problem for basedir/select
match self {
// We load all executable entries from basedir
Select::All(basedir) => {
Ok(from_dir(basedir, self.clone(), context))
},
// We load all entries ending with .inbox from basedir
Select::Inbox(basedir) => {
let inbox_entries = basedir.read_dir().unwrap().filter_map(|f| {
if f.is_err() {
// Dismiss individual file errors
return None;
}
let f_string = f.unwrap().file_name().to_str().unwrap().to_string();
if ! f_string.ends_with(".inbox") || f_string.starts_with(".") {
// We're only looking for non-hidden *.inbox files
return None;
}
return Some(f_string.trim_end_matches(".inbox").to_string());
}).collect();
from_dir_and_list(basedir, inbox_entries, self.clone(), context)
},
// We load all entries ending with .inbox from inboxdir
Select::InboxDir(basedir, inboxdir) => {
let inbox_entries = PathBuf::from(inboxdir).read_dir().unwrap().filter_map(|f| {
if f.is_err() {
// Dismiss individual file errors
return None;
}
let f_string = f.unwrap().file_name().to_str().unwrap().to_string();
if ! f_string.ends_with(".inbox") || f_string.starts_with(".") {
// We're only looking for non-hidden *.inbox files
return None;
}
return Some(f_string.trim_end_matches(".inbox").to_string());
}).collect();
from_dir_and_list(basedir, inbox_entries, self.clone(), context)
},
// Load all entries from list
Select::List(basedir, list) => {
from_dir_and_list(basedir, list.clone(), self.clone(), context)
},
}
}
}
// TODO: Maybe all source/DVCS information should be moved to Repo
// so that task structure is simpler.
#[derive(Debug)]
pub struct Task {
/// The filename for the task
pub name: String,
/// Full path to the task
pub bin: PathBuf,
/// Potentially, a source repository to track for updates
pub source: Option<String>,
/// The full Repo information
pub repo: Option<Repo>,
/// Variables for task config
pub config: HashMap<String, String>,
/// Potentially, a branch/commit to track
pub branch: Option<String>,
/// List of hosts on which this task should run
pub hosts: Vec<String>,
/// Whether the source, if any, has been cloned already
pub cloned: bool,
/// Whether to track subrepository/submodule updates
pub subupdates: bool,
/// The context in which to store variables for translations
pub context: log::Context,
/// The selection context in which a task was created, so that running it can remove it from inbox
pub select: Select,
}
/// config returns an option of (settings directory, ignored tasks) as
/// (PathBuf, Vec<String>)
pub fn config(basedir: &Path) -> (PathBuf, Vec<String>) {
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, select: Select, context: &log::Context) -> Option<Task> {
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(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,
select,
})
}
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");
// TODO: debug message for removing inbox
self.select.clean(&self.name);
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);
}
}
pub struct MissingTask(pub String);
/// Contains a mapping of sources to their corresponding tasks
pub struct SourceSet {
mapping: HashMap<String, Vec<String>>,
}
impl SourceSet {
/// Loads a SourceSet from a basedir
pub fn from(basedir: &Path) -> Result<SourceSet, std::io::Error> {
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<String, Vec<String>> = 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<Vec<String>> {
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<String>, select: Select, context: &log::Context) -> Result<Vec<Task>, MissingTask> {
// TODO: Write tests for permissions problems
// If we're looking up specific tasks, maybe they're referenced by source
// and not by name. SourceSet allows for a source->name mapping.
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), select.clone(), 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), select.clone(), context).unwrap());
tasks.extend(task_list);
} else {
return Err(MissingTask(t));
}
}
}
Ok(tasks)
}
/// 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, select: Select, context: &log::Context) -> Vec<Task> {
basedir.read_dir().unwrap().filter_map(|f| {
if f.is_err() {
// Dismiss individual file errors
return None;
}
return Task::from_path(&f.unwrap().path(), select.clone(), context);
}).collect()
}
/// 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))
}