Basic parsing and circular dependency detection working
This commit is contained in:
parent
c53b6b0ce2
commit
2d8e3d9abe
94
Cargo.lock
generated
94
Cargo.lock
generated
@ -1,4 +1,98 @@
|
|||||||
[root]
|
[root]
|
||||||
name = "j"
|
name = "j"
|
||||||
version = "0.1.5"
|
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"
|
||||||
|
@ -5,3 +5,7 @@ authors = ["Casey Rodarmor <casey@rodarmor.com>"]
|
|||||||
license = "WTFPL/MIT/Apache-2.0"
|
license = "WTFPL/MIT/Apache-2.0"
|
||||||
description = "a command runner"
|
description = "a command runner"
|
||||||
homepage = "https://github.com/casey/j"
|
homepage = "https://github.com/casey/j"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
||||||
|
regex = "*"
|
||||||
|
6
justfile
6
justfile
@ -31,3 +31,9 @@ compile:
|
|||||||
# clean up
|
# clean up
|
||||||
clean:
|
clean:
|
||||||
rm -r tmp
|
rm -r tmp
|
||||||
|
|
||||||
|
a: b
|
||||||
|
echo a
|
||||||
|
|
||||||
|
b: a
|
||||||
|
echo b
|
||||||
|
16
notes
16
notes
@ -1,14 +1,24 @@
|
|||||||
notes
|
notes
|
||||||
-----
|
-----
|
||||||
|
|
||||||
- look through all justfiles for features of make that I use
|
|
||||||
- ask travis for his justfile
|
|
||||||
|
|
||||||
- add tests
|
- add tests
|
||||||
. test all existing behavior
|
. test all existing behavior
|
||||||
. add parsing tests
|
. add parsing tests
|
||||||
. check dependency ordering
|
. 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
|
- remove make dependency
|
||||||
. recipes must be '[a-z][a-z0-9-]*'
|
. recipes must be '[a-z][a-z0-9-]*'
|
||||||
. only allow tabs or spaces, but not both in a recipe
|
. only allow tabs or spaces, but not both in a recipe
|
||||||
|
236
src/main.rs
236
src/main.rs
@ -1,3 +1,9 @@
|
|||||||
|
extern crate regex;
|
||||||
|
|
||||||
|
use std::io::prelude::*;
|
||||||
|
|
||||||
|
use std::{io, fs, env, collections};
|
||||||
|
|
||||||
macro_rules! warn {
|
macro_rules! warn {
|
||||||
($($arg:tt)*) => {{
|
($($arg:tt)*) => {{
|
||||||
extern crate std;
|
extern crate std;
|
||||||
@ -13,106 +19,183 @@ macro_rules! die {
|
|||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Copy)]
|
fn re(pattern: &str) -> regex::Regex {
|
||||||
enum Make {
|
regex::Regex::new(pattern).unwrap()
|
||||||
GNU, // GNU Make installed as `gmake`
|
|
||||||
GNUStealthy, // GNU Make installed as `make`
|
|
||||||
Other, // Another make installed as `make`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Make {
|
struct Recipe<'a> {
|
||||||
fn command(self) -> &'static str {
|
_line: u64,
|
||||||
if self == Make::GNU {
|
name: &'a str,
|
||||||
"gmake"
|
leading_whitespace: &'a str,
|
||||||
} else {
|
commands: Vec<&'a str>,
|
||||||
"make"
|
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::<Vec<&str>>().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<std::process::ExitStatus> {
|
|
||||||
command
|
|
||||||
.stdin(std::process::Stdio::null())
|
|
||||||
.stdout(std::process::Stdio::null())
|
|
||||||
.stderr(std::process::Stdio::null())
|
|
||||||
.status()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn which_make() -> Option<Make> {
|
|
||||||
// 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() {
|
fn main() {
|
||||||
let make = match which_make() {
|
|
||||||
None => die!("Could not execute `make` or `gmake`."),
|
|
||||||
Some(make) => make,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut justfile = "justfile";
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match std::fs::metadata("justfile") {
|
match fs::metadata("justfile") {
|
||||||
Ok(metadata) => if metadata.is_file() { break; },
|
Ok(metadata) => if metadata.is_file() { break; },
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
if error.kind() != std::io::ErrorKind::NotFound {
|
if error.kind() != io::ErrorKind::NotFound {
|
||||||
die!("Error fetching justfile metadata: {}", error)
|
die!("Error fetching justfile metadata: {}", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match std::fs::metadata("Justfile") {
|
match env::current_dir() {
|
||||||
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() {
|
|
||||||
Ok(pathbuf) => if pathbuf.as_os_str() == "/" { die!("No justfile found."); },
|
Ok(pathbuf) => if pathbuf.as_os_str() == "/" { die!("No justfile found."); },
|
||||||
Err(error) => die!("Error getting current dir: {}", error),
|
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);
|
die!("Error changing directory: {}", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let recipes: Vec<String> = std::env::args().skip(1).take_while(|arg| arg != "--").collect();
|
|
||||||
let arguments: Vec<String> = std::env::args().skip(1 + recipes.len() + 1).collect();
|
|
||||||
|
|
||||||
for (i, argument) in arguments.into_iter().enumerate() {
|
let mut contents = String::new();
|
||||||
std::env::set_var(format!("ARG{}", i), argument);
|
|
||||||
|
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<Recipe> = 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<String> = std::env::args().skip(1).collect();
|
||||||
|
// for request in requests {
|
||||||
|
// println!("{}", request);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let arguments: Vec<String> = 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());
|
let mut command = std::process::Command::new(make.command());
|
||||||
|
|
||||||
command.arg("MAKEFLAGS=");
|
command.arg("MAKEFLAGS=");
|
||||||
@ -121,7 +204,7 @@ fn main() {
|
|||||||
command.arg("--always-make").arg("--no-print-directory");
|
command.arg("--always-make").arg("--no-print-directory");
|
||||||
}
|
}
|
||||||
|
|
||||||
command.arg("-f").arg(justfile);
|
command.arg("-f").arg("justfile");
|
||||||
|
|
||||||
for recipe in recipes {
|
for recipe in recipes {
|
||||||
command.arg(recipe);
|
command.arg(recipe);
|
||||||
@ -134,4 +217,5 @@ fn main() {
|
|||||||
None => std::process::exit(-1),
|
None => std::process::exit(-1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user