diff --git a/.gitignore b/.gitignore index 1f0e790..98e5fcf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,3 @@ -# Compiled Object files -build/ -*.o -*.obj - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Compiled Static libraries -*.a -*.lib - -# Executables -*.exe -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..ef6a23a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,22 +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] -serde = "1.0.85" -serde_derive = "1.0.85" -serde_yaml = "0.8.8" -getopts = "0.2.18" -custom_error = "1.3.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 2883c07..b865d30 100644 --- a/spec.md +++ b/spec.md @@ -1,7 +1,7 @@ -# SMake # +# Samurai # -[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 +[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. 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) @@ -27,10 +26,16 @@ 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 Samurai to work +differently for different targets. ### Target / Rule A target is a method to convert some input files into some output files. It @@ -47,26 +52,26 @@ 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. +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 - - 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 +91,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 `Makefile` - Not found: Print error and fail - - Find targets - For each target: - Collect parsed info - Get target status: @@ -133,11 +137,13 @@ 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). 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 @@ -158,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: @@ -176,13 +182,57 @@ 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. - -[smake]: https://github.com/araspik/smake +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 Samurai supports multiple formats (mostly for backward compatibility), +a description of the process is added here. This is for technical purposes +only. + +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 +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 + +[samurai]: https://github.com/araspik/samurai 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()); -}