mars/src/lib.rs

409 lines
12 KiB
Rust

#[macro_use]
extern crate log;
use std::fs::{create_dir_all, File};
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus};
use std::sync::mpsc::channel;
use std::time::Duration;
use std::{fmt, io, thread};
use colored::*;
use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
use notify_rust::{Notification, NotificationHandle};
/// The different types of error that can occur.
#[derive(Debug)]
pub enum Error {
/// No builder can build this project.
NoBuilderFound,
/// An io::Error happened while running a build command.
IoError(io::Error),
/// A command exited with non-zero exit code.
CommandError(String, Vec<String>, ExitStatus),
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Error {
Error::IoError(e)
}
}
impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::NoBuilderFound => write!(fmt, "no builder found"),
Error::IoError(e) => write!(fmt, "i/o error occured: {}", e),
Error::CommandError(command, _, status) => write!(
fmt,
"command {} exited with status code {}",
command, status
),
}
}
}
/// Tests if a file is in a directory.
pub fn contains_file(path: &str, file: &str) -> bool {
let mut path = PathBuf::from(path);
path.push(file);
path.exists()
}
/// Tries to build a certain directory using the specified builders.
pub fn run_command(command: &str, path: &str) -> Result<(), Error> {
run_command_with_args(command, path, &vec![])
}
/// Run a build commands, and wait untils its finished, returning its result.
pub fn run_command_with_args(command: &str, path: &str, args: &Vec<String>) -> Result<(), Error> {
let mut child = Command::new(command).current_dir(path).args(args).spawn()?;
let exit = child.wait()?;
if exit.success() {
Ok(())
} else {
Err(Error::CommandError(command.to_owned(), args.clone(), exit))
}
}
/// Tries to build a certain directory using the specified builders.
pub fn build(
path: &PathBuf,
args: &Vec<String>,
builders: &Vec<Box<dyn Builder>>,
) -> Result<(), Error> {
let mut path = path.clone();
loop {
if path.to_str().unwrap() == "/" {
// Couldn't find a buildable directory
return Err(Error::NoBuilderFound);
}
for builder in builders {
if builder.is_buildable(path.to_str().unwrap()) {
builder.build(path.to_str().unwrap(), &args)?;
return Ok(());
}
}
path.pop();
}
}
/// Destucture a Result to return a Result<(), ()>
pub fn destroy<T, E>(result: Result<T, E>) -> Result<(), ()> {
match result {
Err(_) => Err(()),
Ok(_) => Ok(()),
}
}
/// Converts a string to a pair (PathBuf, Vec<String>).
pub fn builder_arguments_from_string(uri: &str) -> (PathBuf, Vec<String>) {
let split = uri.split('?').collect::<Vec<_>>();
let path = PathBuf::from(split[0]);
let args = if split.len() > 1 {
split[1].split('&').map(|x| x.to_owned()).collect()
} else {
vec![]
};
(path, args)
}
/// Watches a directory and builds it when a modification occurs.
pub fn watch<P: AsRef<Path>>(
p: P,
ignore: Vec<String>,
command: Option<String>,
) -> Result<(), Error> {
let path = PathBuf::from(p.as_ref()).canonicalize()?;
let ignore = ignore
.into_iter()
.map(|x| PathBuf::from(x).canonicalize())
.collect::<Result<Vec<_>, _>>()?;
let command = match command {
Some(c) => vec![c],
_ => vec![],
};
thread::spawn(move || {
let mut builder = GeneralBuilder::new();
let (tx, rx) = channel();
let mut watcher = match watcher(tx, Duration::from_millis(200)) {
Ok(w) => w,
Err(e) => {
warn!("couldn't watch directory {}: {}", path.display(), e);
return;
}
};
if let Err(e) = watcher.watch(&path, RecursiveMode::Recursive) {
warn!("couldn't watch directory {}: {}", path.display(), e);
return;
}
info!("watching {}", path.display());
loop {
match rx.recv() {
Ok(DebouncedEvent::NoticeWrite(x))
| Ok(DebouncedEvent::Write(x))
| Ok(DebouncedEvent::Create(x))
| Ok(DebouncedEvent::Rename(_, x))
| Ok(DebouncedEvent::Chmod(x)) => {
if ignore.iter().any(|y| x.starts_with(y)) {
continue;
}
let start_string = format!("---- STARTING BUILD ---- from {}", path.display());
println!("{}", start_string.bold().green());
match builder.build(&path, &command) {
Err(_) => {
println!("{}", "--------- FAIL ---------".bold().red());
}
Ok(_) => {
println!("{}", "----- SUCCESSFUL -----".bold().green());
}
};
println!();
}
Err(e) => error!("watch error: {:?}", e),
_ => (),
}
}
});
Ok(())
}
/// A general builders that contains many builders and can build any type of code.
pub struct GeneralBuilder {
/// The path to the success icon.
success: PathBuf,
/// The path to the failure icon.
failure: PathBuf,
/// The builders contained.
builders: Vec<Box<dyn Builder>>,
/// The id of the notification to update a notification if possible.
notification_handle: Option<NotificationHandle>,
}
impl GeneralBuilder {
/// Creates a new general builder with the defaults builders.
pub fn new() -> GeneralBuilder {
let mut config = PathBuf::from(dirs::config_dir().unwrap());
config.push("mars");
let mut success = config.clone();
success.push("success.png");
let mut failure = config.clone();
failure.push("failure.png");
if !success.exists() || !failure.exists() {
create_dir_all(&config).unwrap();
const SUCCESS_BYTES: &[u8] = include_bytes!("../assets/success.png");
const FAILURE_BYTES: &[u8] = include_bytes!("../assets/failure.png");
use std::io::Write;
let mut path = config.clone();
path.push("success.png");
let mut success_file = File::create(&success).unwrap();
success_file.write_all(SUCCESS_BYTES).unwrap();
let mut path = config.clone();
path.push("success.png");
let mut failure_file = File::create(&failure).unwrap();
failure_file.write_all(FAILURE_BYTES).unwrap();
}
GeneralBuilder {
success: success,
failure: failure,
builders: vec![
Box::new(MakeBuilder::new()),
Box::new(CMakeBuilder::new()),
Box::new(CargoBuilder::new()),
],
notification_handle: None,
}
}
/// Triggers a build.
pub fn build(&mut self, path: &PathBuf, args: &Vec<String>) -> Result<(), Error> {
let result = build(path, args, &self.builders);
match result {
Ok(_) => self.notify_success(),
Err(ref e) => self.notify_error(&e),
}
result
}
/// Sends a notification of a successful build.
pub fn notify_success(&mut self) {
match self.notification_handle.as_mut() {
Some(handle) => {
handle
.appname("Mars")
.summary("Success")
.body("Mars finished successfully")
.icon(self.success.to_str().unwrap());
handle.update();
}
None => {
let handle = Notification::new()
.appname("Mars")
.summary("Success")
.body("Mars finished successfully")
.icon(self.success.to_str().unwrap())
.show();
self.notification_handle = handle.ok();
}
}
let _ = Notification::new().show();
}
/// Sends a notification of an error.
pub fn notify_error(&self, e: &Error) {
let body = match e {
Error::NoBuilderFound => "No builder was found for this directory".to_string(),
Error::IoError(ref e) => format!("Error while running command: {:?}", e).to_string(),
Error::CommandError(ref command, ref args, ref status) => {
let status = match status.code() {
None => "None".to_owned(),
Some(e) => e.to_string(),
};
format!(
"Command \"{} {}\" failed: {}",
command,
args.join(" "),
status
)
.to_string()
}
};
let _ = Notification::new()
.appname("Mars")
.summary("Failure")
.body(&body)
.icon(self.failure.to_str().unwrap())
.show();
}
}
/// A generic builder. It can build some projects.
pub trait Builder {
/// Checks if a directory has corresponding files so it can build it.
/// e.g.: if there is a Makefile
fn is_buildable(&self, path: &str) -> bool;
/// Trigger all the commands to build the project.
fn build(&self, path: &str, args: &Vec<String>) -> Result<(), Error>;
}
/// The builder that looks for makefiles.
pub struct MakeBuilder;
impl MakeBuilder {
/// Creates a new make builder.
pub fn new() -> MakeBuilder {
MakeBuilder
}
}
impl Builder for MakeBuilder {
fn is_buildable(&self, path: &str) -> bool {
contains_file(path, "Makefile")
}
fn build(&self, path: &str, args: &Vec<String>) -> Result<(), Error> {
run_command_with_args("make", path, args)?;
Ok(())
}
}
/// The builder that builds cmake projects.
pub struct CMakeBuilder;
impl CMakeBuilder {
/// Creates a new cmake builder.
pub fn new() -> CMakeBuilder {
CMakeBuilder
}
}
impl Builder for CMakeBuilder {
fn is_buildable(&self, path: &str) -> bool {
contains_file(path, "CMakeLists.txt")
}
fn build(&self, path: &str, args: &Vec<String>) -> Result<(), Error> {
let mut build = PathBuf::from(path);
build.push("build");
// If there's a build/Makefile, we'll only make
if !contains_file(path, "build/Makefile") {
// Create build directory
create_dir_all(&build)?;
// Run cmake .. in build directory
run_command_with_args("cmake", build.to_str().unwrap(), &vec!["..".to_owned()])?;
}
// Run make in build directory
run_command_with_args("make", build.to_str().unwrap(), args)?;
Ok(())
}
}
/// The builder that looks for Cargo.toml.
pub struct CargoBuilder;
impl CargoBuilder {
/// Creates a new cargo builder.
pub fn new() -> CargoBuilder {
CargoBuilder
}
}
impl Builder for CargoBuilder {
fn is_buildable(&self, path: &str) -> bool {
contains_file(path, "Cargo.toml")
}
fn build(&self, path: &str, args: &Vec<String>) -> Result<(), Error> {
if args.is_empty() {
run_command_with_args("cargo", path, &vec!["build".to_owned()])
} else {
run_command_with_args("cargo", path, args)
}
}
}