#[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, ExitStatus), } impl From 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) -> 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, builders: &Vec>, ) -> 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(result: Result) -> Result<(), ()> { match result { Err(_) => Err(()), Ok(_) => Ok(()), } } /// Converts a string to a pair (PathBuf, Vec). pub fn builder_arguments_from_string(uri: &str) -> (PathBuf, Vec) { let split = uri.split('?').collect::>(); 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: P, ignore: Vec, command: Option, ) -> Result<(), Error> { let path = PathBuf::from(p.as_ref()).canonicalize()?; let ignore = ignore .into_iter() .map(|x| PathBuf::from(x).canonicalize()) .collect::, _>>()?; 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>, /// The id of the notification to update a notification if possible. notification_handle: Option, } 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) -> 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) -> 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) -> 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) -> 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) -> Result<(), Error> { if args.is_empty() { run_command_with_args("cargo", path, &vec!["build".to_owned()]) } else { run_command_with_args("cargo", path, args) } } }