build.rs/src/task.rs

278 lines
9.0 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::dvcs::{Backend, Repo};
use crate::log;
#[derive(Debug)]
pub struct Task {
pub name: String,
pub bin: PathBuf,
pub source: Option<String>,
pub repo: Option<Repo>,
pub config: HashMap<String, String>,
pub branch: Option<String>,
pub hosts: Vec<String>,
pub cloned: bool,
pub subupdates: bool,
pub context: log::Context,
}
/// 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, 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();
// 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(source_url) = &source {
context.insert("$i18n_source".to_string(), source_url.clone());
}
Some(Task {
name,
bin: path.to_path_buf(),
// None source = None repo
repo: source.as_ref().map(|s| {
Repo::new(
Backend::from_setting(read_extension(path, "dvcs")),
s,
&dest,
subupdates,
)
}),
source,
config: HashMap::new(),
branch: read_extension(path, "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 {
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.has_updates() {
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<Vec<Task>, 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<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>, context: &log::Context) -> Result<Vec<Task>, 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))
}