diff --git a/Cargo.lock b/Cargo.lock index 979aa78..edf8f6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,4 +1,98 @@ [root] name = "j" version = "0.1.5" +dependencies = [ + "regex 0.1.77 (registry+https://github.com/rust-lang/crates.io-index)", +] +[[package]] +name = "aho-corasick" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "libc" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "memchr" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex" +version = "0.1.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "thread-id" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thread_local" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "utf8-ranges" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66" +"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +"checksum libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)" = "408014cace30ee0f767b1c4517980646a573ec61a57957aeeabcac8ac0a02e8d" +"checksum memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20" +"checksum regex 0.1.77 (registry+https://github.com/rust-lang/crates.io-index)" = "64b03446c466d35b42f2a8b203c8e03ed8b91c0f17b56e1f84f7210a257aa665" +"checksum regex-syntax 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279401017ae31cf4e15344aa3f085d0e2e5c1e70067289ef906906fdbe92c8fd" +"checksum thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a9539db560102d1cef46b8b78ce737ff0bb64e7e18d35b2a5688f7d097d0ff03" +"checksum thread_local 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "8576dbbfcaef9641452d5cf0df9b0e7eeab7694956dd33bb61515fb8f18cfdd5" +"checksum utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f" +"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" diff --git a/Cargo.toml b/Cargo.toml index d933a82..182d5cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,7 @@ authors = ["Casey Rodarmor "] license = "WTFPL/MIT/Apache-2.0" description = "a command runner" homepage = "https://github.com/casey/j" + +[dependencies] + +regex = "*" diff --git a/justfile b/justfile index 2ecaf6b..28b4f22 100644 --- a/justfile +++ b/justfile @@ -31,3 +31,9 @@ compile: # clean up clean: rm -r tmp + +a: b + echo a + +b: a + echo b diff --git a/notes b/notes index 995e0b4..b06c844 100644 --- a/notes +++ b/notes @@ -1,14 +1,24 @@ notes ----- -- look through all justfiles for features of make that I use -- ask travis for his justfile - - add tests . test all existing behavior . add parsing tests . check dependency ordering +- parsing tests + . --list: make sure the right dependencies are listed + . --show: make sure the recipe contents is parsed + +- look through all justfiles for features of make that I use. so far: + . phony + . SHELL := zsh + . quiet + . make variables + +- ask travis for his justfile + + - remove make dependency . recipes must be '[a-z][a-z0-9-]*' . only allow tabs or spaces, but not both in a recipe diff --git a/src/main.rs b/src/main.rs index a6544b0..3416c8e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,9 @@ +extern crate regex; + +use std::io::prelude::*; + +use std::{io, fs, env, collections}; + macro_rules! warn { ($($arg:tt)*) => {{ extern crate std; @@ -13,106 +19,183 @@ macro_rules! die { }}; } -#[derive(PartialEq, Clone, Copy)] -enum Make { - GNU, // GNU Make installed as `gmake` - GNUStealthy, // GNU Make installed as `make` - Other, // Another make installed as `make` +fn re(pattern: &str) -> regex::Regex { + regex::Regex::new(pattern).unwrap() } -impl Make { - fn command(self) -> &'static str { - if self == Make::GNU { - "gmake" - } else { - "make" +struct Recipe<'a> { + _line: u64, + name: &'a str, + leading_whitespace: &'a str, + commands: Vec<&'a str>, + dependencies: collections::HashSet<&'a str>, +} + +struct Resolver<'a> { + recipes: &'a collections::BTreeMap<&'a str, Recipe<'a>>, + resolved: collections::HashSet<&'a str>, + seen: collections::HashSet<&'a str>, + stack: Vec<&'a str>, +} + +fn resolve<'a> (recipes: &'a collections::BTreeMap<&'a str, Recipe<'a>>) { + let mut resolver = Resolver { + recipes: recipes, + resolved: collections::HashSet::new(), + seen: collections::HashSet::new(), + stack: vec![], + }; + + for (_, recipe) in recipes { + resolver.resolve(recipe); + } +} + +impl<'a> Resolver<'a> { + fn resolve(&mut self, recipe: &'a Recipe) { + self.stack.push(recipe.name); + self.seen.insert(recipe.name); + for dependency_name in &recipe.dependencies { + match self.recipes.get(dependency_name) { + Some(dependency) => if !self.resolved.contains(dependency.name) { + if self.seen.contains(dependency.name) { + let first = self.stack[0]; + self.stack.push(first); + die!("Circular dependency: {}", + self.stack.iter() + .skip_while(|name| **name != dependency.name) + .cloned().collect::>().join(" -> ")); + } + self.resolve(dependency); + }, + None => die!("Recipe \"{}\" depends on recipe \"{}\", which doesn't exist.", + recipe.name, dependency_name), + } + } + self.resolved.insert(recipe.name); + self.stack.pop(); } - - fn gnu(self) -> bool { - self != Make::Other - } -} - -fn status(command: &mut std::process::Command) -> std::io::Result { - command - .stdin(std::process::Stdio::null()) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status() -} - -fn which_make() -> Option { - // check `gmake` - let result = status(std::process::Command::new("gmake").arg("-v")); - - if let Ok(exit_status) = result { - if exit_status.success() { - return Some(Make::GNU); - } - } - - // check `make`. pass gmake specific flags to see if it's actually gmake - let result = status(std::process::Command::new("make").arg("-v").arg("--always-make")); - - if let Ok(exit_status) = result { - return if exit_status.success() { - Some(Make::GNUStealthy) - } else { - Some(Make::Other) - }; - } - - return None; } fn main() { - let make = match which_make() { - None => die!("Could not execute `make` or `gmake`."), - Some(make) => make, - }; - - let mut justfile = "justfile"; - loop { - match std::fs::metadata("justfile") { + match fs::metadata("justfile") { Ok(metadata) => if metadata.is_file() { break; }, Err(error) => { - if error.kind() != std::io::ErrorKind::NotFound { + if error.kind() != io::ErrorKind::NotFound { die!("Error fetching justfile metadata: {}", error) } } } - match std::fs::metadata("Justfile") { - Ok(metadata) => if metadata.is_file() { - justfile = "Justfile"; - break; - }, - Err(error) => { - if error.kind() != std::io::ErrorKind::NotFound { - die!("Error fetching Justfile metadata: {}", error) - }; - } - } - - match std::env::current_dir() { + match env::current_dir() { Ok(pathbuf) => if pathbuf.as_os_str() == "/" { die!("No justfile found."); }, Err(error) => die!("Error getting current dir: {}", error), } - if let Err(error) = std::env::set_current_dir("..") { + if let Err(error) = env::set_current_dir("..") { die!("Error changing directory: {}", error); } } - - let recipes: Vec = std::env::args().skip(1).take_while(|arg| arg != "--").collect(); - let arguments: Vec = std::env::args().skip(1 + recipes.len() + 1).collect(); - for (i, argument) in arguments.into_iter().enumerate() { - std::env::set_var(format!("ARG{}", i), argument); + let mut contents = String::new(); + + fs::File::open("justfile") + .unwrap_or_else(|error| die!("Error opening justfile: {}", error)) + .read_to_string(&mut contents) + .unwrap_or_else(|error| die!("Error reading justfile: {}", error)); + + let shebang_re = re(r"^\s*#!(.*)$"); + let comment_re = re(r"^\s*#[^!].*$"); + let command_re = re(r"^(\s+)(.*)$"); + let blank_re = re(r"^\s*$"); + let label_re = re(r"^([a-z](-[a-z]|[a-z])*):(.*)$"); + let name_re = re(r"^[a-z](-[a-z]|[a-z])*$"); + let whitespace_re = re(r"\s+"); + + let mut recipes = collections::BTreeMap::new(); + let mut current_recipe: Option = None; + for (i, line) in contents.lines().enumerate() { + if blank_re.is_match(line) { + continue; + } else if shebang_re.is_match(line) { + die!("Unexpected shebang on line {}: {}", i, line); + } + + if let Some(mut recipe) = current_recipe { + match command_re.captures(line) { + Some(captures) => { + let leading_whitespace = captures.at(1).unwrap(); + if recipe.leading_whitespace == "" { + recipe.leading_whitespace = leading_whitespace; + } else if leading_whitespace != recipe.leading_whitespace { + die!("Command on line {} has inconsistent leading whitespace: {}", + i, line); + } + let command = captures.at(2).unwrap(); + recipe.commands.push(command); + current_recipe = Some(recipe); + continue; + }, + None => { + recipes.insert(recipe.name, recipe); + current_recipe = None; + }, + } + } + + if comment_re.is_match(line) { + // ignore + } else if let Some(captures) = label_re.captures(line) { + let name = captures.at(1).unwrap(); + let rest = captures.at(3).unwrap().trim(); + let mut dependencies = collections::HashSet::new(); + for part in whitespace_re.split(rest) { + if name_re.is_match(part) { + if dependencies.contains(part) { + die!("Duplicate dependency \"{}\" on line {}", part, i); + } + dependencies.insert(part); + } else { + die!("Bad label on line {}: {}", i, line); + } + } + + if recipes.contains_key(name) { + die!("Duplicate recipe name \"{}\" on line {}.", name, i); + } + + current_recipe = Some(Recipe{ + _line: i as u64, + name: name, + leading_whitespace: "", + commands: vec![], + dependencies: dependencies, + }); + } else { + die!("Error parsing line {} of justfile: {}", i, line); + } } + if let Some(recipe) = current_recipe { + recipes.insert(recipe.name, recipe); + } + + resolve(&recipes); + + // let requests: Vec = std::env::args().skip(1).collect(); + // for request in requests { + // println!("{}", request); + // } + + // let arguments: Vec = std::env::args().skip(1 + recipes.len() + 1).collect(); + + // for (i, argument) in arguments.into_iter().enumerate() { + // std::env::set_var(format!("ARG{}", i), argument); + // } + + /* let mut command = std::process::Command::new(make.command()); command.arg("MAKEFLAGS="); @@ -121,7 +204,7 @@ fn main() { command.arg("--always-make").arg("--no-print-directory"); } - command.arg("-f").arg(justfile); + command.arg("-f").arg("justfile"); for recipe in recipes { command.arg(recipe); @@ -134,4 +217,5 @@ fn main() { None => std::process::exit(-1), } } + */ }