From 66d399cd5ecfe7c27cc7ed4ecc8dcc9f38545445 Mon Sep 17 00:00:00 2001 From: ARaspiK Date: Sun, 17 Feb 2019 12:33:54 +0100 Subject: [PATCH 1/2] Added second half of parsing process All and any format-specific code has been removed; It will be added in a separate crate that will be included by this by default. Tests have been removed, sinc the structure has changed so significantly. The binary crate has been removed. It will be rewritten using the Clap crate. Targets and formats have been completely redone. Targets are now basically finished, but more features may be added later. The main work to do now is add formats, glue together parsing with combining, and stuffing that into the binary crate. `Cargo.lock` is now ignored; I see no point in keeping it. The spec has been updated with the new changes; If I missed something, I'll add it soon. SMake has effectively been minimized into a format-independent target management library. More stuff will be added soon! --- .gitignore | 3 +- Cargo.lock | 118 ------------------ Cargo.toml | 5 +- spec.md | 84 ++++++++++--- src/file.rs | 64 ---------- src/format.rs | 32 +++++ src/lib.rs | 18 +-- src/main.rs | 94 --------------- src/prelude.rs | 64 ---------- src/rule.rs | 214 --------------------------------- src/target.rs | 318 +++++++++++++++++++++++++++++++++++++++++++++++++ src/test.rs | 24 ---- 12 files changed, 423 insertions(+), 615 deletions(-) delete mode 100644 Cargo.lock delete mode 100644 src/file.rs create mode 100644 src/format.rs delete mode 100644 src/main.rs delete mode 100644 src/prelude.rs delete mode 100644 src/rule.rs create mode 100644 src/target.rs delete mode 100644 src/test.rs diff --git a/.gitignore b/.gitignore index 1f0e790..fb38a94 100644 --- a/.gitignore +++ b/.gitignore @@ -17,5 +17,6 @@ build/ app # Cargo -/target +target **/*.rs.bk +Cargo.lock diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 2df17f5..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,118 +0,0 @@ -[[package]] -name = "custom_error" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "dtoa" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "getopts" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "linked-hash-map" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "proc-macro2" -version = "0.4.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "quote" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "serde" -version = "1.0.85" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "serde_derive" -version = "1.0.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "serde_yaml" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", - "linked-hash-map 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.85 (registry+https://github.com/rust-lang/crates.io-index)", - "yaml-rust 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "smake" -version = "0.0.2" -dependencies = [ - "custom_error 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "getopts 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.85 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.85 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_yaml 0.8.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "syn" -version = "0.15.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "unicode-width" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "unicode-xid" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "yaml-rust" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "linked-hash-map 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[metadata] -"checksum custom_error 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "994da086d8391ca0b3cceebb4478fc761199fc6850bf3c51de8f0f5dae6fddca" -"checksum dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6d301140eb411af13d3115f9a562c85cc6b541ade9dfa314132244aaee7489dd" -"checksum getopts 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "0a7292d30132fb5424b354f5dc02512a86e4c516fe544bb7a25e7f266951b797" -"checksum linked-hash-map 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "70fb39025bc7cdd76305867c4eccf2f2dcf6e9a57f5b21a93e1c2d86cd03ec9e" -"checksum proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)" = "38fddd23d98b2144d197c0eca5705632d4fe2667d14a6be5df8934f8d74f1978" -"checksum quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)" = "cdd8e04bd9c52e0342b406469d494fcb033be4bdbe5c606016defbb1681411e1" -"checksum serde 1.0.85 (registry+https://github.com/rust-lang/crates.io-index)" = "534b8b91a95e0f71bca3ed5824752d558da048d4248c91af873b63bd60519752" -"checksum serde_derive 1.0.85 (registry+https://github.com/rust-lang/crates.io-index)" = "a915306b0f1ac5607797697148c223bedeaa36bcc2e28a01441cd638cc6567b4" -"checksum serde_yaml 0.8.8 (registry+https://github.com/rust-lang/crates.io-index)" = "0887a8e097a69559b56aa2526bf7aff7c3048cf627dff781f0b56a6001534593" -"checksum syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)" = "f92e629aa1d9c827b2bb8297046c1ccffc57c99b947a680d3ccff1f136a3bee9" -"checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" -"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" -"checksum yaml-rust 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "95acf0db5515d07da9965ec0e0ba6cc2d825e2caeb7303b66ca441729801254e" diff --git a/Cargo.toml b/Cargo.toml index bfeaf42..349f8cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,5 @@ categories = ["development-tools::build-utils", "development-tools"] travis-ci = {repository = "araspik/smake", branch = "rust"} [dependencies] -serde = "1.0.85" -serde_derive = "1.0.85" -serde_yaml = "0.8.8" -getopts = "0.2.18" custom_error = "1.3.0" +regex = "1.1.0" diff --git a/spec.md b/spec.md index 2883c07..c5aa9df 100644 --- a/spec.md +++ b/spec.md @@ -1,7 +1,7 @@ -# SMake # +# `SMake` # -[SMake][smake] is a simple Make program to run multiple commands easily. It is -programmer-oriented, but can be used in minimal form with little to no +[`SMake`][smake] is a simple Make program to run multiple commands easily. It +is programmer-oriented, but can be used in minimal form with little to no understanding. This is a functional/technical specification, which is _not_ complete. It is a @@ -10,8 +10,7 @@ live document, and changes will keep coming. ## Examples ## ## Goals ## -* [ ] New syntax (SDLang) while providing backward compatibility for typical - Makefiles +* [ ] New format while providing backward compatibility for typical `Makefile`s * [ ] Machine-parseable versions of commands * [ ] Functions as a library (so that other programs can wrap core functionality without having to call the application as a program) @@ -29,8 +28,13 @@ live document, and changes will keep coming. ### `SMakefile` A `SMakefile` is a file which contains descriptions of targets in such a format -that SMake can understand it. By default, SMake looks for this file as being -named `SMakefile` and residing in the current directory. +that `SMake` can understand it. By default, `SMake` looks for this file as +being named `SMakefile` and residing in the current directory. + +### Format +A format specifies which Makefile format to use when parsing the makefile. +Different formats have different features, and this allows `SMake` to work +differently for different targets. ### Target / Rule A target is a method to convert some input files into some output files. It @@ -47,15 +51,16 @@ dependency on the target so that that target runs before A. ### Virtual dependency Target `B` is a virtual dependency of target `A` if one of `B`'s input files is -one of `A`'s output files. +one of `A`s output files. + The difference between a dependency and a _virtual_ dependency is that virtual -dependencies are _not_ declared by targets, even though they should be. SMake -detects virtual dependecies and warns about them automatically, since a virtual -dependency is generally a sign of a dependency that the programmer forgot to -declare. +dependencies are _not_ declared by targets, even though they should be. `SMake` +detects virtual dependencies and warns about them automatically, since a +virtual dependency is generally a sign of a dependency that the programmer +forgot to declare. ### Cyclic dependency -This is a situation where two targets depend on each other. Since SMake will +This is a situation where two targets depend on each other. Since `SMake` will (by default) update the dependencies of a target before updating the target itself, this leads to an infinite loop, and so is not allowed. @@ -64,9 +69,8 @@ itself, this leads to an infinite loop, and so is not allowed. - Parse options - Parse command * Build - - Find, parse SMakefile + - Find, parse `SMakefile` - Not found: Print error and fail - - Find targets - Recursively add 'virtual dependencies' of targets + Ignore declared dependencies + Use hash table keyed by target name to prevent infinite recursion in @@ -86,9 +90,8 @@ itself, this leads to an infinite loop, and so is not allowed. - Execute update - Update file modification times * Info - - Find, parse SMakefile + - Find, parse `SMakefile` - Not found: Print error and fail - - Find targets - For each target: - Collect parsed info - Get target status: @@ -133,6 +136,8 @@ itself, this leads to an infinite loop, and so is not allowed. + Options + Examples +## Commands + ### Begin The application begins with some options, a command, and additional arguments to that command (if any). @@ -179,10 +184,53 @@ named. If the `SMakefile` does not exist, an error occurs. ### Help -Provides help information about either SMake in general (global options, +Provides help information about either `SMake` in general (global options, available commands, invocation examples, etc.) or provides information about a specific command (with options, subcommands if any, examples, etc.). Target names are not recognized, and any build configurations are ignored. The `SMakefile` is not required, and is never used, by this command. +## Parsing +Parsing is the act of converting inputted text of some sort (in our case from +a `Makefile`) into some internal representation (the internal `Target` type). +Since `SMake` supports multiple formats (mostly for backward compatibility), a +description of the process is added here. This is for technical purposes only. + +Different formats (POSIX, GNU, and `SMake`) each have different file and target +types. All target types implement a special `Target` trait that provides +uniform access to them, and all file types implement a `File` trait for the +same reason. + +Parsing works by getting each format to parse to a single global target type, +which hosts extra data in the form of a trait. This makes parsing +format-independent (so different formats can be used simultaneously), and +simplifies the process significantly. However, the immediately-parsed targets +are not ready to use - their dependencies must be resolved. This occurs in a +process called finalization, wherein the list of targets is converted into a +hash map, and dependencies, stored by name, are "standardized" such that they +refer to the dependency's primary name (which is used as the key to the hash +map). The finalization process is recursive, and automagically fails on missing +dependencies, cyclic dependencies, as well as duplicate target names. + +TODO: Virtual dependency checking + +### Parsing Flowchart +* For every file: + * Match file to format + * Parse file into unfinalized target list +* Create a final hash map of targets (size is the size of the target list) +* For each unfinalized target (pop off list, since each call removes multiple) + * Remove it from the list + * Finalize it + * Split dependencies if not already done so + * Standardizes dependency names into the primary name of the dependency + * Checks for missing dependencies + * For every dependency, find matching target + * Fail if the dependency creates a cyclic dependency + * No missing dependencies exist here! + * Finalize that target (recursive)! + * Store the now-finalized target in the output hash map + * TODO: Find virtual dependencies here +* Return target hash map + [smake]: https://github.com/araspik/smake diff --git a/src/file.rs b/src/file.rs deleted file mode 100644 index 8a98d83..0000000 --- a/src/file.rs +++ /dev/null @@ -1,64 +0,0 @@ -/*! File: Representation of a SMakefile. - * - * This represents SMakefiles, which currently only consist of rules. - * - * Author: ARaspiK - * License: MIT - */ - -use crate as smake; -use crate::rule::{Rule, RuleData}; -use std::{io, fs}; -use std::path::PathBuf; -use std::collections::HashMap; -use serde_yaml; - -/// Representation of a SMakefile. -pub struct File { - pub rules: HashMap, -} - -impl File { - /// Parses from the given file. - pub fn from_file(path: &String) -> smake::Result { - let path = PathBuf::from(path); - let file = fs::File::open(&path) - .map_err(|e| match e.kind() { - io::ErrorKind::NotFound => smake::Error::NoFile{path}, - _ => smake::Error::Other{source: e}, - })?; - Self::from_reader(file) - } - - /// Parses from the given Reader. - pub fn from_reader(read: R) -> smake::Result { - Ok(File { - rules: serde_yaml::from_reader::<_,HashMap>(read)? - .into_iter() - .map(|(name, rule)| Rule::from_data(rule) - .map(|rule| (name, rule))) - .collect::>>()? - }) - } - - /// Parses from the given string. - pub fn from_str(text: &str) -> smake::Result { - Ok(File { - rules: serde_yaml::from_str::>(text)? - .into_iter() - .map(|(name, rule)| Rule::from_data(rule) - .map(|rule| (name, rule))) - .collect::>>()? - }) - } - - /// Returns a reference to a rule if it exists. - pub fn get(&self, name: &String) -> Option<&Rule> { - self.rules.get(name) - } - - /// Returns a mutable reference to a rule if it exists. - pub fn get_mut(&mut self, name: &String) -> Option<&mut Rule> { - self.rules.get_mut(name) - } -} diff --git a/src/format.rs b/src/format.rs new file mode 100644 index 0000000..3679e05 --- /dev/null +++ b/src/format.rs @@ -0,0 +1,32 @@ +//! Formats define different formats to parse from. +//! +//! A format specifies which `Makefile`-like format to use when parsing a file. +//! Different formats have different features, and this allows specializing for +//! each format. +//! +//! All formats implement `Format`. This trait provides parsing routines, as +//! well as some related information. + +use crate::target::Target; + +use regex::Regex; + +use std::error::Error; +use std::path::Path; + +/// Defines specializations for a given format. +pub trait Format { + /// The error type when parsing. + type ParseErr: Error; + + /// Returns a regex which matches valid file names. + /// This used when searching for a file to use. + fn file_name() -> Regex; + + /// Parses the file at the given path, outputting into the given list. + /// The targets are not finalized - finalization will be done later. + /// + /// The function will panic if the file does not exist or cannot be read + /// from. + fn parse>(path: P, output: &mut Vec) -> Result<(), Self::ParseErr>; +} diff --git a/src/lib.rs b/src/lib.rs index 86afcb3..a31f5dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,5 @@ -extern crate serde; -extern crate serde_derive; -extern crate serde_yaml; -#[macro_use] extern crate custom_error; +extern crate custom_error; +extern crate regex; -pub mod rule; -pub mod file; -mod prelude; - -#[cfg(test)] -mod test; - -pub use crate::rule::Rule; -pub use crate::file::File; -pub use crate::prelude::{Error, Result}; +pub mod format; +pub mod target; diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index cd247d4..0000000 --- a/src/main.rs +++ /dev/null @@ -1,94 +0,0 @@ -//! # Frontend for SMake. -//! -//! The power of SMake is in the library, but this is the glue that connects -//! that power to the CLI (and so to the users). -//! -//! Author: ARaspiK -//! License: MIT - -#[macro_use] extern crate custom_error; -extern crate getopts; - -use smake; -use std::{env, error::Error, fmt, process::exit}; -use getopts as getopt; - -custom_error!{ ErrStr - Data{data: T} = "{data}" -} - -impl ErrStr - where T: fmt::Display { - pub fn new(data: T) -> Self { - ErrStr::Data {data} - } - - pub fn result(data: T) -> Result { - Err(ErrStr::Data {data}) - } -} - -fn box_err<'a, T: Error + 'a>(err: T) -> Box { - Box::new(err) as Box -} - -struct Opts { - path: String, - targets: Vec, -} - -fn print_help(prog: &String, opts: &getopt::Options) { - let usage = format!("Usage: {} [options] TARGET", prog); - eprint!("{}", opts.usage(&usage)); -} - -fn parse_opts() -> Result, Box> { - // Get args - let args = env::args().collect::>(); - let prog = args[0].to_string(); - - // Set up options - let mut opts = getopt::Options::new(); - opts.optopt( "f", "file", "The SMakefile to read from", "PATH"); - opts.optflag("h", "help", "Provides help information"); - - // Parse - let matches = opts.parse(&args[1..]).map_err(box_err)?; - - // Special case: help info - if matches.opt_present("h") || matches.free.is_empty() { - print_help(&prog, &opts); - return Ok(None); - } - - // Gather args and return - Ok(Some(Opts { - path: matches.opt_str("f").unwrap_or("SMakefile".to_string()), - targets: matches.free, - })) -} - -fn work(opts: Opts) -> Result<(), Box> { - // Parse file - let file = smake::File::from_file(&opts.path)?; - - // For every target - for target in opts.targets.iter() { - let rule = file.rules.get(target) - .map_or_else( - || ErrStr::result(format!("Target \"{}\" not found!", target)) - .map_err(box_err), - |rule| Ok(rule))?; - println!("{}", rule); - } - - Ok(()) -} - -fn main() { - if let Err(err) = parse_opts() - .and_then(|opts| opts.map(|opts| work(opts)).unwrap_or(Ok(()))) { - eprintln!("{}", err); - exit(1) - } -} diff --git a/src/prelude.rs b/src/prelude.rs deleted file mode 100644 index 5b598dd..0000000 --- a/src/prelude.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! # Prelude: Common items used across SMake. -//! -//! This include Error and Result. -//! -//! Author: ARaspiK -//! License: MIT - -use std::{io, path::PathBuf}; -use serde_yaml; - -custom_error! {pub Error - NoFile {path: PathBuf} - = @{format!("File \"{}\" not found!", path.to_str().unwrap())}, - Parsing{source: serde_yaml::Error} = "Parsing error", - Other {source: io::Error} = "I/O error" -} - -/// A Result type for SMake. -pub type Result = std::result::Result; - -/*/// An error type for SMake. -#[derive(Debug)] -pub enum Error { - NoFile(PathBuf), - Parsing(serde_yaml::Error), - Other(io::Error), -} - -impl error::Error for Error { - /// Returns a cause for this error, if any. - fn source(&self) -> Option<&(dyn error::Error + 'static)> { - match self { - Error::Parsing(err) => Some(err), - Error::Other(err) => Some(err), - _ => None - } - } -} - -impl fmt::Display for Error { - /// Displays the error as a human-readable string. - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Error::NoFile(path) => write!(f, "File \"{}\" not found!", - path.to_str().unwrap()), - Error::Parsing(err) => write!(f, "Parsing error: {}", err), - Error::Other(err) => write!(f, "I/O error: {}", err), - } - } -} - -impl From for Error { - /// Converts from a YAML parsing error. - fn from(err: serde_yaml::Error) -> Self { - Error::Parsing(err) - } -} - -impl From for Error { - /// Converts from a I/O error. - fn from(err: io::Error) -> Self { - Error::Other(err) - } -}*/ diff --git a/src/rule.rs b/src/rule.rs deleted file mode 100644 index 93b9818..0000000 --- a/src/rule.rs +++ /dev/null @@ -1,214 +0,0 @@ -/*! Rule: A basic rule to execute. - * - * They consist of commands to execute, along with inputs and outputs. - * - * They have the special condition that all of their inputs must exist. As - * such, attempting to create one returns as a io::Result, and update_mtimes() - * takes ownership (since the inputs may no longer exist, and so the rule may - * be invalidated). The Result (should!) provide information about any I/O - * errors that popped up. - * - * Author: ARaspiK - * License: MIT - */ - -use crate as smake; -use std::{io, fs}; -use std::path::{Path, PathBuf}; -use std::time::SystemTime; -use std::fmt; -use serde_yaml; -use serde_derive::{Serialize, Deserialize}; - -/// A basic rule to execute. -pub struct Rule { - /// Commands to run to execute the rule. - cmds: Vec, - /// Input files (paths and modification times). - inps: Vec<(PathBuf, SystemTime)>, - /// Output files (paths and optional modification times, it may not exist). - outs: Vec<(PathBuf, Option)>, -} - -/// Information about an output's update requirements. -pub struct UpdateReq<'a> { - /// The path to the output. - pub path: &'a Path, - /// The modification time of the output (if it existed) - time: Option, - /// The path to an input (if an input was modified after the output). - inps: Option>, -} - -/// A Rule created for (de)serialization purposes. -#[derive(Serialize, Deserialize)] -pub(crate) struct RuleData { - pub cmds: Vec, - #[serde(alias = "ins")] - pub inputs: Vec, - #[serde(alias = "outs")] - pub outputs: Vec, -} - -impl Rule { - /** - * Creates a Rule given the commands, inputs and outputs. - * - * The inputs and outputs are searched for and (if found) their last - * modification time is recorded. - */ - pub fn new(cmds: Vec, inps: Vec, outs: Vec) - -> smake::Result { - Ok(Rule { - cmds, // Same command list - // Look for inputs that can't be accessed and fail on them. - // Simultaneously, grab their latest modification time. - inps: inps.iter() - .map(move |s| PathBuf::from(s)) - .map(|path| fs::metadata(path.as_path()) - .and_then(|m| m.modified()) - .map(|mt| (path.clone(), mt)) - .map_err(|e| match e.kind() { - io::ErrorKind::NotFound => smake::Error::NoFile{path}, - _ => smake::Error::Other{source:e} - })) - .collect::>>()?, - // Outputs don't have to exist, but grab their modification time if - // they do. - outs: outs.iter() - .map(move |s| PathBuf::from(s)) - .map(|p| (p.clone(), fs::metadata(p).ok().map(|m| - m.modified().ok()).unwrap_or(None))) - .collect::>(), - }) - } - - /** - * Updates modification times for inputs and outputs. - * - * It takes ownership as the rule may be invalidated if an input no longer - * exists. - */ - pub fn update_mtimes(mut self) -> io::Result { - // Modify each input - for (ref path, ref mut time) in self.inps.iter_mut() { - // by grabbing its modification time and stopping on failure. - *time = fs::metadata(path)?.modified()?; - } - // Modify each output - for (ref path, ref mut time) in self.outs.iter_mut() { - // by trying to grab its timestamp and ignoring failure. - *time = fs::metadata(path).ok().map(|m| m.modified().ok()) - .unwrap_or(None); - } - // If successful (w.r.t finding inputs) return the rule. - Ok(self) - } - - /** - * Whether an update is necessary for the rule or not. - * - * It calculates the latest that an input has been modified and compares - * that to the earliest that an output was modified. It assumes that all - * inputs map to all outputs, and so any input that was modified _after_ the - * latest any output has been modified means that an update is required to - * update that output file. - */ - pub fn needs_update(&self) -> bool { - // Get latest input modification time. - self.inps.iter().map(|e| e.1).max() - // If no inputs, always update - // Otherwise, update if any output can be found that - .map_or(true, |inp_mt| self.outs.iter().any(|o| - // either doesn't exist - // or was modified before the latest input modification time. - o.1.map_or(true, |out_mt| out_mt < inp_mt))) - } - - /** - * Returns specific information about the update requirements of each - * output. - * - * Returns detailed information suitable for verbose reasoning to why a rule - * must be executed. - */ - pub fn update_reqs<'a>(&'a self) -> Vec> { - let mut res = Vec::with_capacity(self.outs.len()); - for (ref path, ref time) in self.outs.iter() { - res.push(UpdateReq { - path: path.as_path(), - time: *time, - inps: time.map(|mt| self.inps.iter() - .filter(|(_, ref intime)| *intime > mt) - .map(|(ref inpath, _)| inpath.as_path()) - .collect::>()), - }); - } - res - } - - /** - * Converts a RuleData into a Rule. - * - * For serialization purposes. - */ - pub(crate) fn from_data(data: RuleData) -> smake::Result { - Self::new(data.cmds, data.inputs, data.outputs) - } -} - -impl fmt::Display for Rule { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?} -> {:?} via {:?}", - self.inps.iter().map(|i| i.0.to_str().unwrap()) - .collect::>(), - self.outs.iter().map(|o| o.0.to_str().unwrap()) - .collect::>(), - self.cmds) - } -} - -impl From for RuleData { - fn from(data: Rule) -> Self { - Self { - cmds: data.cmds, - inputs: data.inps.iter() - .map(|i| i.0.to_str().unwrap().to_string()) - .collect::>(), - outputs: data.outs.iter() - .map(|o| o.0.to_str().unwrap().to_string()) - .collect::>(), - } - } -} - -impl<'a> UpdateReq<'a> { - /** - * Whether the output file requires an update. - * - * An output requires an update when one of the following are true: - * * It does not exist. - * * Input files exist which are newer than it. - * * No input files were associated with the rule. - */ - pub fn needs_update(&self) -> bool { - self.time.is_none() || self.inps.is_some() - } -} - -impl<'a> fmt::Display for UpdateReq<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if let Some(inps) = self.inps.as_ref() { - write!(f, "\"{}\" older than {}, needs update.", - self.path.to_str().unwrap(), - inps.iter().map(|i| i.to_str().unwrap()) - .fold(String::new(), |s, i| s + i + ", ")) - } else if self.time.is_some() { - write!(f, "\"{}\" is newer than all inputs, does not need update.", - self.path.to_str().unwrap()) - } else { - write!(f, "\"{}\" does not exist, needs update.", - self.path.to_str().unwrap()) - } - } -} diff --git a/src/target.rs b/src/target.rs new file mode 100644 index 0000000..2231a8e --- /dev/null +++ b/src/target.rs @@ -0,0 +1,318 @@ +//! A target is a format-independent method to create outputs from inputs. +//! +//! A target is a method to convert some input files into some output files +//! using a given set of commands. A target may depend upon others to create +//! its input files, such that these dependencies will be run first in order to +//! generate the input files. +//! +//! Formats can create format-dependent extraneous information to be held by +//! targets parsed from files of that format by creating an implementation of +//! `TargetExtra`. This additional information will be included with each +//! target, but by virtue of boxing, targets parsed from different formats can +//! be mixed together. + +use custom_error::custom_error; + +use std::collections::HashMap; +use std::fs; +use std::io; +use std::path::PathBuf; +use std::process::Command; + +/// A uniform interface to format-specific extraneous data. +pub trait TargetExtra { + /// Returns whether the current target may be referred to by the given + /// name. + /// + /// This is most useful to `Makefile` formats, where targets have multiple + /// names, corresponding to output files. + /// + /// A reasonable default implementation has been provided. + fn has_name(&self, tgt: &Target, name: &str) -> bool { + tgt.name == name + } +} + +/// A structure that differentiates mixed dependencies from unmixed (or split) +/// dependencies. +/// +/// Useful primarily for `Makefile` formats, where dependencies may be input +/// files or other targets. +pub enum MixedDeps { + Mixed(Vec), + UnMixed { + inputs: Vec, + dependencies: Vec, + }, +} + +impl MixedDeps { + /// Converts mixed dependencies to unmixed dependencies, by resolving names + /// given a predicate that defines whether the dependency exists. + /// + /// The predicate returns whether the given name is a dependency as an + /// optional structure, which, when nothing, represents an input file. + /// When, however, the name is found to be a dependency, an additional name + /// is given which is considered a "more correct" reference to it (i.e the + /// primary name of the matching target). This is useful as it standardizes + /// names, allowing the result to easily reference dependencies from a hash + /// map of primary names. + /// + /// Panics if a dependency (from split state) is not found by the + /// predicate. + fn split

(self, mut predicate: P) -> (Vec, Vec) + where + P: FnMut(&str) -> Option>, + { + match self { + MixedDeps::Mixed(deps) => { + deps.into_iter() + .fold((Vec::new(), Vec::new()), |mut res, dep| { + if let Some(name) = predicate(&dep) { + res.1.push(name.unwrap_or(dep)); + } else { + res.0.push(dep.into()); + } + res + }) + } + MixedDeps::UnMixed { + inputs, + dependencies, + } => { + // TODO: Convert this to report multiple missing dependencies + // at a time? + ( + inputs, + dependencies.into_iter().fold(Vec::new(), |mut res, dep| { + if let Some(name) = predicate(&dep) { + res.push(name.unwrap_or(dep)); + } else { + panic!("Dependency {} not found!", dep); + } + res + }), + ) + } + } + } +} + +/// A format-independent method to create outputs from inputs. +/// +/// See the module-level documentation for more info. +pub struct Target { + /// Name of the target. + pub name: String, + /// Files produced by the target. + pub outputs: Vec, + /// Inputs and dependencies, mixed or unmixed. + pub dependencies: MixedDeps, + /// Commands to run. + /// + /// Due to the fact that executing a command needs to be done mutably, a + /// whole bunch of errors come up because of the way updates are laid out. + /// As such, a command is created and executed at the time of update, not + /// created beforehand. + pub commands: Vec, + /// Extraneous format-specific data. + pub extra: Box, +} + +/// An error type for updates. +custom_error! {pub UpdateErr + Io{source: io::Error} = "I/O Error", + Status{status: i32} = "Process exited with error code {status}", + Signal = "Process exited with signal", +} + +/// Creates a command from a string. +/// +/// The command will be wrappped in a platform-specific shell. +fn string_to_command(command: &str) -> Command { + let mut cmd = Command::new(if cfg!(windows) { "cmd" } else { "sh" }); + cmd.arg(if cfg!(windows) { "/C" } else { "-c" }); + cmd.arg(command); + cmd +} + +impl Target { + /// Creates a new target. + pub fn new( + name: String, + outputs: Vec, + dependencies: MixedDeps, + commands: Vec, + extra: Box, + ) -> Target { + Target { + name, + outputs: outputs.into_iter().map(|p| p.into()).collect(), + dependencies, + commands, + extra, + } + } + + /// Returns input files of the target, if known. + /// + /// Panics if the input files are unknown. + /// This is done as these functions are only expected to be called after + /// finalization is completed, at which point they are known for sure. + pub fn inputs(&self) -> &Vec { + if let MixedDeps::UnMixed { inputs, .. } = &self.dependencies { + inputs + } else { + panic!("Input files are still mixed!"); + } + } + + /// Returns dependencies, if known. + /// + /// Panics if the dependencies are unknown. + /// It panics as these functions are only expected to be called after + /// finalization is complete, at which point they are known for sure. + pub fn dependencies(&self) -> &Vec { + if let MixedDeps::UnMixed { dependencies, .. } = &self.dependencies { + dependencies + } else { + panic!("Dependencies are still mixed!"); + } + } + + /// Updates the target. + /// + /// Returns `None` if it failed. + /// Otherwise, returns a boolean indicating whether an update was needed. + /// The commands are executed sequentially and synchronously. + /// + /// Returns any errors that may have occurred during updating, including if + /// the commands failed to run. + pub fn update(&self, list: &HashMap) -> Result { + // First, update dependencies, stopping on failure. + if self.dependencies().iter() + .try_fold(false, |res, dep| { + list.get(dep).unwrap().update(list).map(|r| res || r) + })? + // If a dependency was updated, force update. + // Otherwise, check modification times. + || self.inputs().iter() // TODO: Better error messages + .map(|p| fs::metadata(p).unwrap().modified().unwrap()) + .max() // If no inputs, force update + .map_or(true, |latest| self.outputs.iter() + .map(|o| fs::metadata(o).and_then(|md| md.modified()).ok()) + // If missing output, update + // If output updated earlier than input, update + .any(|o| o.map_or(true, |o| o < latest))) + { + // Update: Run all commands, printing exit status on failure of + // any. + self.commands + .iter() + .map(|cmd| string_to_command(&cmd)) + .try_for_each(|mut cmd| { + cmd.status()? + .code() + .map_or(Err(UpdateErr::Signal), |status| { + if status == 0 { + Ok(()) + } else { + Err(UpdateErr::Status { status }) + } + }) + })?; + Ok(true) + } else { + Ok(false) + } + } + + /// Finalizes a whole list of targets. + /// + /// Handles some external bookkeeping required by `finalize`. + pub fn finalize_list(mut list: Vec) -> HashMap { + let mut post = HashMap::with_capacity(list.len()); + let mut path = Vec::new(); + + // Loop over the targets. Keep popping, since we cannot iterate + // normally (because recursiveness may absorb multiple elements). + while let Some(elem) = list.pop() { + elem.finalize(&mut list, &mut post, &mut path); + } + + post + } + + /// Finalizes the target. + /// + /// Finalization involves verifying dependencies, differentiating inputs + /// from dependencies (if necessary), translating dependencies into primary + /// names for the referred-to targets, finalizing dependencies, and putting + /// the target into the given output hash map. + /// + /// This function is recursive - it further finalizes all of its + /// dependencies. In order to prevent circular dependencies, which would + /// cause the application to hang, a "path" is taken, which describes which + /// targets called each other (in a stack-like list) until they reached + /// this call. If a dependency of the current function is found which + /// already exists on the path, then this function panics. + /// + /// Additionally, this function panics if a dependency is not found or if a + /// target with the same primary name already exists in the output hashmap. + pub fn finalize( + mut self, + list: &mut Vec, + post: &mut HashMap, + path: &mut Vec, + ) { + // First, we resolve (not finalize) dependencies. + let (inputs, dependencies) = self.dependencies.split(|dep| { + list.iter() + .chain(post.values()) + .find(|tgt| tgt.extra.has_name(tgt, &dep)) + .map(|target| { + if target.name == dep { + None + } else { + Some(target.name.clone()) + } + }) + }); + + // Then, we finalize each dependency, checking for cyclic or missing + // dependencies. + // Note that we push the name onto the path stack, and pop it off + // afterwards. This means that the path will be modified, but in the + // same state as how it was passed to the function. + path.push(self.name); + for dep in dependencies.iter() { + if path.contains(dep) { + panic!("Cyclic dependency found for {}!", dep); + } + + // Now, we check to see if we have to finalize the dependency. + if let Some(loc) = list.iter().position(|t| &t.name == dep) { + // We remove it (ownership) and then finalize it. + list.remove(loc).finalize(list, post, path); + } + + // Note that all dependencies exist, since the `MixedDeps::split` + // function checked it for all dependencies. As such, any + // dependencies not in `list` are in the output hash map already. + } + self.name = path.pop().unwrap(); + + // Now, the target is stored on the output hash map. + // NOTE: At the moment, the key is cloned from the name. If possible, + // this should be prevented. + self.dependencies = MixedDeps::UnMixed { + inputs, + dependencies, + }; + if let Some(tgt) = post.insert(self.name.clone(), self) { + // Duplicate found! Panic. + panic!("Duplicate target {} found!", tgt.name); + // Note that tgt.name == key == self.name + } + } +} diff --git a/src/test.rs b/src/test.rs deleted file mode 100644 index ab420af..0000000 --- a/src/test.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! Tests -//! -//! Tests for everything SMake. -//! -//! -//! Author: ARaspiK -//! License: MIT - -use crate as smake; - -//#[test] -fn test_parser() { - let test_str = "main: - cmds: - - \"gcc -c hello.c\" - ins: - - \"hello.c\" - outs: - - \"hello.o\""; - - let rules = smake::File::from_str(test_str); - - assert!(rules.is_ok()); -} From a83c70a8d75bd6ae46c6db4ec31b7709b6539ab1 Mon Sep 17 00:00:00 2001 From: ARaspiK Date: Sun, 17 Feb 2019 16:34:08 +0100 Subject: [PATCH 2/2] Renamed to Samurai I'd been planning to change the name to Samurai whenever something big happened, and this fits the bill. Why Samurai? Because `ninja` already exists, and I wanted to make a heavy-duty, more complex program (`ninja` is quite low-level). The package has been split into 3: * The library crate. This is the primary focus. * A formats crate. This provides default formats, and moves parsing and stuff away from the main Samurai code. * The binary crate. This is the actual application. Here option handling and all that stuff happens. --- .gitignore | 19 ------- Cargo.toml | 54 +++++++++++++++---- README.md | 8 +-- app/Cargo.toml | 47 +++++++++++++++++ app/src/main.rs | 3 ++ formats/Cargo.toml | 46 ++++++++++++++++ formats/pegs/sdlang.rustpeg | 101 ++++++++++++++++++++++++++++++++++++ formats/src/lib.rs | 0 spec.md | 58 +++++++++++---------- 9 files changed, 274 insertions(+), 62 deletions(-) create mode 100644 app/Cargo.toml create mode 100644 app/src/main.rs create mode 100644 formats/Cargo.toml create mode 100644 formats/pegs/sdlang.rustpeg create mode 100644 formats/src/lib.rs diff --git a/.gitignore b/.gitignore index fb38a94..98e5fcf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,3 @@ -# Compiled Object files -build/ -*.o -*.obj - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Compiled Static libraries -*.a -*.lib - -# Executables -*.exe -app - -# Cargo target **/*.rs.bk Cargo.lock diff --git a/Cargo.toml b/Cargo.toml index 349f8cb..ef6a23a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,51 @@ [package] -name = "smake" -version = "0.0.2" -authors = ["ARaspiK "] + +# General Information +name = "samurai" +version = "0.0.3" +authors = [ + "ARaspiK " +] +description = "A heavy-duty Make library" license = "MIT" -description = "A simple Make program" -edition = "2018" -repository = "https://github.com/araspik/smake" +# Public Use Metadata +publish = true +documentation = "https://docs.rs/samurai" +homepage = "https://github.com/araspik/samurai/blob/master/README.md" readme = "README.md" -keywords = ["make"] -categories = ["development-tools::build-utils", "development-tools"] +keywords = [ + "make", +] +categories = [ + "development-tools", + "development-tools::build-utils", +] + +# Code-specific Metadata +edition = '2018' +# Public Display Badges [badges] -travis-ci = {repository = "araspik/smake", branch = "rust"} +travis-ci = { repository = "araspik/samurai", branch = "master" } +codecov = { repository = "araspik/samurai", branch = "master", service = "github" } +is-it-maintained-issue-resolution = { repository = "araspik/samurai" } +is-it-maintained-open-issues = { repository = "araspik/samurai" } +maintenance = { status = "actively-developed" } + +# Profiles +# Dependencies [dependencies] -custom_error = "1.3.0" -regex = "1.1.0" +custom_error = "~1.4.0" +regex = "~1.1.0" + +# Features +[features] + +# Workspace +[workspace] +members = [ + "formats", + "app", +] diff --git a/README.md b/README.md index afaf9ac..893624b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# SMake -SMake is a simple Make program, written in Rust. +# Samurai +Samurai is (will be) a heavy-duty Make program, written in Rust. -Hopefully my first actually complete project. +Hopefully, this is my first actually complete project. View the [specification][spec] to see what this project will look like. @@ -28,5 +28,5 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` -[spec]: https:?/github.com/araspik/smake/wiki/Specification +[spec]: https://github.com/araspik/samurai/wiki/Specification "SMake Specification" diff --git a/app/Cargo.toml b/app/Cargo.toml new file mode 100644 index 0000000..44b1cd6 --- /dev/null +++ b/app/Cargo.toml @@ -0,0 +1,47 @@ +[package] + +# General Information +name = "samurai_app" +version = "0.0.1" +authors = [ + "ARaspiK " +] +description = "A heavy-duty make program" +license = "MIT" + +# Public Use Metadata +publish = true +documentation = "https://docs.rs/samurai" +homepage = "https://github.com/araspik/samurai/blob/master/README.md" +readme = "README.md" +keywords = [ + "make", + "make-program", +] +categories = [ + "command-line-utilities", + "development-tools", + "development-tools::build-utils", +] + +# Code-specific Metadata +edition = '2018' + +# Workspace +workspace = ".." + +# Public Display Badges +[badges] +travis-ci = { repository = "araspik/samurai", branch = "master" } +codecov = { repository = "araspik/samurai", branch = "master", service = "github" } +is-it-maintained-issue-resolution = { repository = "araspik/samurai" } +is-it-maintained-open-issues = { repository = "araspik/samurai" } +maintenance = { status = "actively-developed" } + +# Profiles + +# Dependencies +[dependencies] + +# Features +[features] diff --git a/app/src/main.rs b/app/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/app/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/formats/Cargo.toml b/formats/Cargo.toml new file mode 100644 index 0000000..ee1c7e2 --- /dev/null +++ b/formats/Cargo.toml @@ -0,0 +1,46 @@ +[package] + +# General Information +name = "samurai_formats" +version = "0.0.1" +authors = [ + "ARaspiK " +] +description = "Default formats for Samurai" +license = "MIT" + +# Public Use Metadata +publish = true +documentation = "https://docs.rs/samurai_formats" +homepage = "https://github.com/araspik/samurai/blob/master/README.md" +readme = "../README.md" +keywords = [ + "formats", + "parsing", + "make", +] +categories = [ + "parsing", +] + +# Code-specific Metadata +edition = '2018' + +# Worspace +workspace = ".." + +# Public Display Badges +[badges] +travis-ci = { repository = "araspik/samurai", branch = "master" } +codecov = { repository = "araspik/samurai", branch = "master", service = "github" } +is-it-maintained-issue-resolution = { repository = "araspik/samurai" } +is-it-maintained-open-issues = { repository = "araspik/samurai" } +maintenance = { status = "actively-developed" } + +# Profiles + +# Dependencies +[dependencies] + +# Features +[features] diff --git a/formats/pegs/sdlang.rustpeg b/formats/pegs/sdlang.rustpeg new file mode 100644 index 0000000..dd28d3b --- /dev/null +++ b/formats/pegs/sdlang.rustpeg @@ -0,0 +1,101 @@ +use chrono::prelude::*; +use chrono::{DateTime}; +use chrono::{NaiveDate, NaiveDateTime}; +use chrono::{Local, Utc, FixedOffset, TimeZone}; + +use std::time::Duration; + +use super::Value; + +// Whitespace and comments +white + = (" " / "\n" / "\r" / "\t" / "\\\n" / comment)+ +comment + = "/*" (!"*/" .)* "*/" + / ("//" / "--" / "#") (!"\n" .)* "\n" + +// Global stuffs +ident -> &'input str + = $([a-zA-Z][a-zA-Z0-9.$-]+) +digit -> char + = c:[0-9] {c} + +// Values +value -> Value + = data:string { Value::String(data) } + / data:base64 { Value::Base64(data) } + / data:date { Value::Date(data) } + / data:datetime { Value::DateTime(data) } + / data:duration { Value::Duration(data) } + / data:number { Value::Number(data) } + / data:decimal { Value::Decimal(data) } + / data:boolean { Value::Boolean(data) } + / null { Value::Null } + +string -> String + = "\"" data:("\\\"" {"\""} / !("\n" / "\"") c:. {c})* "\"" { + String::from_iter(data) + } / "`" data:$($(!("\n" / "`") .)*) "`" { + String::from_iter(data) + } + +date -> NaiveDate + = year:$([0-9]*<4>) "/" month:$([0-9]*<2>) "/" day:$([0-9]*<2>) {? + let year = year.parse::()?; + let month = month.parse::()?; + let day = day.parse::()?; + + Ok(NaiveDate::from_ymd_opt(year, month, day)?) + } + +naive_datetime -> NaiveDateTime + = date:date white h:$([0-9]*<2>) ":" m:$([0-9]*<2>) ":" s:$([0-9]*<2>) "." ms:$([0-9]*<3>) {? + let h = h.parse::()?; + let m = m.parse::()?; + let s = s.parse::()?; + let ms = ms.parse::()?; + + Ok(date?.and_hms_micro_opt(h, m, s, ms)?) + } + +datetime -> DateTime + = datetime:naive_datetime utc:"-UTC"? { + if utc.is_some() { + Utc.fix().from_utc_datetime(datetime) + } else { + Local.timestamp(0,0).timezone() + .offset_from_utc_date(&Utc.timestamp(0,0).naive_utc()) + .from_local_datetime(datetime).unwrap() + } + } + +duration -> Duration + = d:(n:$([0-9]+) "d:" {n})? h:$([0-9]*<2>) ":" m:$([0-9]*<2>) ":" s:$([0-9]*<2>) ms:("." n:$([0-9]*<3>) {n})? { + let d = d.map_or(0, |d| d.parse::().unwrap()); + let h = h.parse::().unwrap(); + let m = m.parse::().unwrap(); + let s = s.parse::().unwrap(); + let ms = ms.map_or(0, |ms| ms.parse::().unwrap()); + + Duration::new(s + 60 * (m + 60 * (h + 24 * d)), 1000 * ms) + } + +number -> i128 + = n:$([0-9]+) !("L" / "BD") {? Ok(n.parse::()? as i128) } + / n:$([0-9]+) "L" {? Ok(n.parse::()? as i128) } + / n:$([0-9]+) "BD" {? Ok(n.parse::()?) } + +decimal -> f64 + = n:$([0-9]+ "." [0-9]+) "f" {? Ok(n.parse::()? as f64) } + / n:$([0-9]+ "." [0-9]+) !"f" {? Ok(n.parse::()?) } + +boolean -> bool + = ("true" / "on") {true} + / ("false" / "off") {false} + +null = "null" + +base64 -> Vec + = "[" white? data:(c:[a-zA-Z+/] white? {c})* "]" {? + Ok(base64::decode(data.as_slice())?) + } diff --git a/formats/src/lib.rs b/formats/src/lib.rs new file mode 100644 index 0000000..e69de29 diff --git a/spec.md b/spec.md index c5aa9df..b865d30 100644 --- a/spec.md +++ b/spec.md @@ -1,6 +1,6 @@ -# `SMake` # +# Samurai # -[`SMake`][smake] is a simple Make program to run multiple commands easily. It +[Samurai][samurai] is a simple Make program to run multiple commands easily. It is programmer-oriented, but can be used in minimal form with little to no understanding. @@ -26,14 +26,15 @@ live document, and changes will keep coming. ## Terminology / Definitions ## -### `SMakefile` -A `SMakefile` is a file which contains descriptions of targets in such a format -that `SMake` can understand it. By default, `SMake` looks for this file as -being named `SMakefile` and residing in the current directory. +### `Makefile` +A `Makefile` is a file which contains descriptions of targets in such a format +that Samurai can understand it. By default, Samurai looks for this file as +being named `samurai.*` (in different formats) and residing in the current +directory. ### Format A format specifies which Makefile format to use when parsing the makefile. -Different formats have different features, and this allows `SMake` to work +Different formats have different features, and this allows Samurai to work differently for different targets. ### Target / Rule @@ -54,22 +55,22 @@ Target `B` is a virtual dependency of target `A` if one of `B`'s input files is one of `A`s output files. The difference between a dependency and a _virtual_ dependency is that virtual -dependencies are _not_ declared by targets, even though they should be. `SMake` -detects virtual dependencies and warns about them automatically, since a -virtual dependency is generally a sign of a dependency that the programmer -forgot to declare. +dependencies are _not_ declared by targets, even though they should be. +Samurai detects virtual dependencies and warns about them automatically, +since a virtual dependency is generally a sign of a dependency that the +programmer forgot to declare. ### Cyclic dependency -This is a situation where two targets depend on each other. Since `SMake` will -(by default) update the dependencies of a target before updating the target -itself, this leads to an infinite loop, and so is not allowed. +This is a situation where two targets depend on each other. Since Samurai +will (by default) update the dependencies of a target before updating the +target itself, this leads to an infinite loop, and so is not allowed. ## Process Flowchart ## * Begin - Parse options - Parse command * Build - - Find, parse `SMakefile` + - Find, parse `Makefile` - Not found: Print error and fail - Recursively add 'virtual dependencies' of targets + Ignore declared dependencies @@ -90,7 +91,7 @@ itself, this leads to an infinite loop, and so is not allowed. - Execute update - Update file modification times * Info - - Find, parse `SMakefile` + - Find, parse `Makefile` - Not found: Print error and fail - For each target: - Collect parsed info @@ -142,7 +143,7 @@ itself, this leads to an infinite loop, and so is not allowed. The application begins with some options, a command, and additional arguments to that command (if any). First, options are parsed (using `getopt`). Some global options are: -* `-f|--file PATH`: Selects the path to the `SMakefile` to be used. +* `-f|--file PATH`: Selects the path to the `Samuraifile` to be used. * `-v|--verbose [SEC]`: Increases amount of output, optionally for a specific section (type) of output. * `-q|--quiet [SEC]`: Reduces the amount of output, optionally for a specific @@ -163,7 +164,7 @@ When executing targets, checks are made to ensure that all required input files exist. If they do not, an error occurs, the user is alerted, and processing halts. -If the `SMakefile` does not exist, an error occurs. +If the `Samuraifile` does not exist, an error occurs. ### Info A subcommand can be specified: @@ -181,25 +182,26 @@ Missing input files are marked (by prepending a `!` in front of invalid target names) in all subcommands, but in `g(eneral)` the missing input files are named. -If the `SMakefile` does not exist, an error occurs. +If the `Samuraifile` does not exist, an error occurs. ### Help -Provides help information about either `SMake` in general (global options, +Provides help information about either Samurai in general (global options, available commands, invocation examples, etc.) or provides information about a specific command (with options, subcommands if any, examples, etc.). Target names are not recognized, and any build configurations are ignored. -The `SMakefile` is not required, and is never used, by this command. +The `Samuraifile` is not required, and is never used, by this command. ## Parsing Parsing is the act of converting inputted text of some sort (in our case from a `Makefile`) into some internal representation (the internal `Target` type). -Since `SMake` supports multiple formats (mostly for backward compatibility), a -description of the process is added here. This is for technical purposes only. +Since Samurai supports multiple formats (mostly for backward compatibility), +a description of the process is added here. This is for technical purposes +only. -Different formats (POSIX, GNU, and `SMake`) each have different file and target -types. All target types implement a special `Target` trait that provides -uniform access to them, and all file types implement a `File` trait for the -same reason. +Different formats (e.g POSIX, GNU) each have different file and target types. +All target types implement a special `Target` trait that provides uniform +access to them, and all file types implement a `File` trait for the same +reason. Parsing works by getting each format to parse to a single global target type, which hosts extra data in the form of a trait. This makes parsing @@ -233,4 +235,4 @@ TODO: Virtual dependency checking * TODO: Find virtual dependencies here * Return target hash map -[smake]: https://github.com/araspik/smake +[samurai]: https://github.com/araspik/samurai