254 lines
8.4 KiB
Rust
254 lines
8.4 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,
|
|
}
|
|
|
|
/// 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) -> 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();
|
|
|
|
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(),
|
|
})
|
|
}
|
|
|
|
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) {
|
|
let mut context = HashMap::new();
|
|
context.insert("$i18n_task", self.name.as_str());
|
|
|
|
if let Some(repo) = &self.repo {
|
|
if repo.has_updates() {
|
|
self.run();
|
|
} else if *force {
|
|
log::debug("forcing", Some(&context));
|
|
self.run();
|
|
} else {
|
|
log::debug("no_update", Some(&context));
|
|
}
|
|
} 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;
|
|
}
|
|
|
|
let mut context = HashMap::new();
|
|
context.insert("$i18n_task", self.name.as_str());
|
|
log::info("run", Some(&context));
|
|
|
|
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");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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) -> 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());
|
|
}).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>) -> 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)) {
|
|
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)).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))
|
|
}
|