401 lines
11 KiB
Rust
401 lines
11 KiB
Rust
#[macro_use]
|
|
extern crate log;
|
|
|
|
use std::{io, thread, fmt};
|
|
use std::env::current_dir;
|
|
use std::process::{Command, ExitStatus};
|
|
use std::path::{Path, PathBuf};
|
|
use std::fs::{File, create_dir_all};
|
|
use std::sync::mpsc::channel;
|
|
use std::time::Duration;
|
|
|
|
use colored::*;
|
|
|
|
use notify::{Watcher, RecursiveMode, watcher, DebouncedEvent};
|
|
|
|
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<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) -> Result<(), Error> {
|
|
let mut path = current_dir()?;
|
|
path.push(p.as_ref());
|
|
|
|
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(_)) |
|
|
Ok(DebouncedEvent::Write(_)) |
|
|
Ok(DebouncedEvent::Create(_)) |
|
|
Ok(DebouncedEvent::Rename(_, _)) |
|
|
Ok(DebouncedEvent::Chmod(_)) => {
|
|
|
|
let start_string = format!("---- STARTING BUILD ---- from {}", path.display());
|
|
println!("{}", start_string.bold().green());
|
|
|
|
match builder.build(&path, &vec![]) {
|
|
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<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)
|
|
}
|
|
}
|
|
}
|