├── .github ├── dependabot.yml └── workflows │ └── rust.yml ├── .gitignore ├── .mergify.yml ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── examples ├── comments.makefile ├── dep.makefile ├── echo.makefile ├── envvar.makefile ├── failure.makefile └── vars.makefile └── src ├── lib.rs ├── main.rs ├── makefile.rs └── plan.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | timezone: America/Toronto 8 | open-pull-requests-limit: 10 9 | 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | timezone: America/Toronto 15 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: Automatically merge PRs 3 | conditions: 4 | - and: 5 | - author=dependabot[bot] 6 | - check-success=build 7 | actions: 8 | merge: 9 | method: merge 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | os: 3 | - linux 4 | - osx 5 | rust: 6 | - stable 7 | - beta 8 | - nightly 9 | matrix: 10 | allow_failures: 11 | - rust: nightly 12 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.15" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "is_terminal_polyfill", 17 | "utf8parse", 18 | ] 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.8" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 25 | 26 | [[package]] 27 | name = "anstyle-parse" 28 | version = "0.2.5" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" 31 | dependencies = [ 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle-query" 37 | version = "1.1.1" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" 40 | dependencies = [ 41 | "windows-sys", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-wincon" 46 | version = "3.0.4" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" 49 | dependencies = [ 50 | "anstyle", 51 | "windows-sys", 52 | ] 53 | 54 | [[package]] 55 | name = "clap" 56 | version = "4.5.39" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" 59 | dependencies = [ 60 | "clap_builder", 61 | ] 62 | 63 | [[package]] 64 | name = "clap_builder" 65 | version = "4.5.39" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" 68 | dependencies = [ 69 | "anstream", 70 | "anstyle", 71 | "clap_lex", 72 | "strsim", 73 | ] 74 | 75 | [[package]] 76 | name = "clap_lex" 77 | version = "0.7.4" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 80 | 81 | [[package]] 82 | name = "colorchoice" 83 | version = "1.0.2" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" 86 | 87 | [[package]] 88 | name = "fab" 89 | version = "0.1.2" 90 | dependencies = [ 91 | "clap", 92 | ] 93 | 94 | [[package]] 95 | name = "is_terminal_polyfill" 96 | version = "1.70.1" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 99 | 100 | [[package]] 101 | name = "strsim" 102 | version = "0.11.1" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 105 | 106 | [[package]] 107 | name = "utf8parse" 108 | version = "0.2.2" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 111 | 112 | [[package]] 113 | name = "windows-sys" 114 | version = "0.52.0" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 117 | dependencies = [ 118 | "windows-targets", 119 | ] 120 | 121 | [[package]] 122 | name = "windows-targets" 123 | version = "0.52.6" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 126 | dependencies = [ 127 | "windows_aarch64_gnullvm", 128 | "windows_aarch64_msvc", 129 | "windows_i686_gnu", 130 | "windows_i686_gnullvm", 131 | "windows_i686_msvc", 132 | "windows_x86_64_gnu", 133 | "windows_x86_64_gnullvm", 134 | "windows_x86_64_msvc", 135 | ] 136 | 137 | [[package]] 138 | name = "windows_aarch64_gnullvm" 139 | version = "0.52.6" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 142 | 143 | [[package]] 144 | name = "windows_aarch64_msvc" 145 | version = "0.52.6" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 148 | 149 | [[package]] 150 | name = "windows_i686_gnu" 151 | version = "0.52.6" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 154 | 155 | [[package]] 156 | name = "windows_i686_gnullvm" 157 | version = "0.52.6" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 160 | 161 | [[package]] 162 | name = "windows_i686_msvc" 163 | version = "0.52.6" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 166 | 167 | [[package]] 168 | name = "windows_x86_64_gnu" 169 | version = "0.52.6" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 172 | 173 | [[package]] 174 | name = "windows_x86_64_gnullvm" 175 | version = "0.52.6" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 178 | 179 | [[package]] 180 | name = "windows_x86_64_msvc" 181 | version = "0.52.6" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 184 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "fab" 4 | version = "0.1.2" 5 | authors = ["Michael Melanson "] 6 | description = "The fabulous, aspirationally Make-compatible, fabricator of things." 7 | license = "MIT" 8 | homepage = "https://github.com/michaelmelanson/fab-rs" 9 | 10 | [dependencies] 11 | clap = "4.5" 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Michael Melanson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: build test 3 | 4 | build: 5 | cargo build 6 | 7 | test: comments dep echo envvar vars failure 8 | 9 | comments: build 10 | target/debug/fab -f examples/comments.makefile 11 | 12 | dep: build 13 | target/debug/fab -f examples/dep.makefile 14 | 15 | echo: build 16 | target/debug/fab -f examples/echo.makefile 17 | 18 | envvar: build 19 | target/debug/fab -f examples/envvar.makefile 20 | 21 | vars: build 22 | target/debug/fab -f examples/vars.makefile 23 | 24 | failure: build 25 | target/debug/fab -f examples/failure.makefile || exit 0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fab-rs [![Rust](https://github.com/michaelmelanson/fab-rs/actions/workflows/rust.yml/badge.svg)](https://github.com/michaelmelanson/fab-rs/actions/workflows/rust.yml) 2 | The fabulous, aspirationally Make-compatible, fabricator of things. 3 | 4 | # Status 5 | This is really early days. Here's the checklist of what's supported and what's not right now: 6 | 7 | - [x] Parsing Makefiles 8 | - [x] Executing commands in rules 9 | - [x] Dependency resolution 10 | - [x] Environment variables passed into commands 11 | - [x] Basic special variable substitution (`$@`, `$<`) 12 | - [ ] Don't rebuild unmodified targets 13 | - [ ] Pattern rules 14 | - [ ] Variable definitions 15 | - [ ] Standard pattern rule library 16 | - [ ] Parallel builds (via https://github.com/alexcrichton/jobserver-rs) 17 | - [ ] ... 18 | 19 | # Usage 20 | 21 | Fab reads Makefiles and executes the rules inside. 22 | 23 | ``` 24 | cargo install fab 25 | cd /path/to/code 26 | fab 27 | ``` 28 | -------------------------------------------------------------------------------- /examples/comments.makefile: -------------------------------------------------------------------------------- 1 | # On its own line 2 | #With or without spaces 3 | all: foo # On a rule definition line 4 | echo "Pass" # On a command line 5 | # Between rules 6 | foo: # On a rule definition without deps 7 | # On a rule without commands. 8 | # At the end of the file. -------------------------------------------------------------------------------- /examples/dep.makefile: -------------------------------------------------------------------------------- 1 | 2 | all: a 3 | echo "Fourth" 4 | 5 | a: b c 6 | echo "Third" 7 | 8 | b: 9 | echo "First" 10 | 11 | c: b 12 | echo "Second" -------------------------------------------------------------------------------- /examples/echo.makefile: -------------------------------------------------------------------------------- 1 | 2 | all: 3 | echo "Hello world!" 4 | -------------------------------------------------------------------------------- /examples/envvar.makefile: -------------------------------------------------------------------------------- 1 | 2 | # We actually get this for free. 3 | all: 4 | echo "Working directory is $(PWD)" 5 | -------------------------------------------------------------------------------- /examples/failure.makefile: -------------------------------------------------------------------------------- 1 | 2 | all: 3 | sh -c "false" -------------------------------------------------------------------------------- /examples/vars.makefile: -------------------------------------------------------------------------------- 1 | 2 | all: foo 3 | 4 | foo: bar 5 | echo "Target is $@ (should be 'foo')" 6 | echo "Dependency is $< (should be 'bar')" 7 | 8 | bar: 9 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod makefile; 2 | pub mod plan; 3 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{arg, Arg}; 2 | 3 | use std::error::Error; 4 | use std::fs::File; 5 | use std::io::prelude::*; 6 | use std::process::Stdio; 7 | 8 | mod makefile; 9 | mod plan; 10 | 11 | use makefile::{parse_makefile, Target}; 12 | use plan::{plan_execution, Invocation}; 13 | 14 | fn main() { 15 | let args = clap::Command::new("fab") 16 | .about("The fabulous, somewhat Make-compatible, fabricator of things.") 17 | .arg(arg!(-f --file "Read FILE as a makefile").default_value("Makefile")) 18 | .arg( 19 | Arg::new("target") 20 | .help("The rule to build") 21 | .default_value("all"), 22 | ) 23 | .get_matches(); 24 | 25 | let file = args.get_one::("file").unwrap(); 26 | let target = Target::named(args.get_one::("target").unwrap()); 27 | 28 | println!("fab: Building target '{}' from '{}'", target.name(), file); 29 | 30 | let mut file = File::open(file).unwrap_or_else(|err| { 31 | panic!( 32 | "Could not open {:?}: {} (caused by {:?})", 33 | file, 34 | err.to_string(), 35 | err.source() 36 | ) 37 | }); 38 | 39 | let mut contents = String::new(); 40 | file.read_to_string(&mut contents) 41 | .expect("failed to read from file"); 42 | 43 | let makefile = parse_makefile(&contents).expect("failed to parse makefile"); 44 | 45 | let plan = plan_execution(makefile.clone(), &target); 46 | println!("fab: Executing plan: {:#?}", plan); 47 | for phase in plan.phases { 48 | for invocation in phase { 49 | execute(&invocation); 50 | } 51 | } 52 | } 53 | 54 | fn execute(invocation: &Invocation) { 55 | let target = &invocation.target; 56 | let rule = &invocation.rule; 57 | println!("fab: Building target '{}'", target.name()); 58 | 59 | for cmd in &invocation.rule.commands { 60 | let cmd = cmd.text().replace("$@", target.name()).replace( 61 | "$<", 62 | &rule 63 | .dependencies 64 | .iter() 65 | .map(|t| t.name().clone()) 66 | .collect::>() 67 | .join(" "), 68 | ); 69 | 70 | let status = std::process::Command::new("sh") 71 | .arg("-c") 72 | .arg(cmd.clone()) 73 | .stdout(Stdio::inherit()) 74 | .status() 75 | .expect("failed to execute"); 76 | 77 | if !status.success() { 78 | println!( 79 | "fab: Target '{}' failed to execute {:?}", 80 | target.name(), 81 | cmd 82 | ); 83 | std::process::exit(1); 84 | } 85 | } 86 | println!("fab: Finished rule '{}'", rule.target.name()); 87 | } 88 | -------------------------------------------------------------------------------- /src/makefile.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] 2 | pub struct Target(String); 3 | 4 | impl Target { 5 | pub fn named>(name: T) -> Target { 6 | Target(name.into()) 7 | } 8 | 9 | pub fn name(&self) -> &String { 10 | &self.0 11 | } 12 | } 13 | 14 | #[derive(Clone, Debug, PartialEq, Eq)] 15 | pub struct Command(String); 16 | 17 | impl Command { 18 | pub fn with>(command: T) -> Command { 19 | Command(command.into()) 20 | } 21 | 22 | pub fn text(&self) -> &String { 23 | &self.0 24 | } 25 | } 26 | 27 | #[derive(Clone, Debug, PartialEq)] 28 | pub struct Rule { 29 | pub target: Target, 30 | pub dependencies: Vec, 31 | pub commands: Vec, 32 | } 33 | 34 | impl Rule { 35 | fn new(target: Target, dependencies: Vec, commands: Vec) -> Rule { 36 | Rule { 37 | target, 38 | dependencies, 39 | commands, 40 | } 41 | } 42 | } 43 | 44 | #[derive(Clone, Debug, PartialEq)] 45 | pub struct Makefile { 46 | pub rules: Vec, 47 | } 48 | 49 | impl Makefile { 50 | fn new(rules: Vec) -> Makefile { 51 | Makefile { rules } 52 | } 53 | } 54 | 55 | #[derive(Debug)] 56 | enum MakefileLine { 57 | EmptyLine, 58 | RuleDefinition(String, Vec), 59 | Command(String), 60 | } 61 | 62 | #[derive(Debug, PartialEq)] 63 | pub enum ParseError { 64 | LineParse, 65 | CommandOutsideOfRule, 66 | } 67 | 68 | fn parse_line(original: &String) -> Result { 69 | // Strip off comments 70 | let line: String = original.split('#').next().unwrap_or("").to_owned().clone(); 71 | 72 | if line.chars().nth(0) == Some('\t') { 73 | Ok(MakefileLine::Command(String::from(line.trim()))) 74 | } else if let Some(index) = line.find(':') { 75 | let (target, dependencies_with_separator) = line.split_at(index); 76 | let (_, dependencies) = dependencies_with_separator.split_at(1); 77 | 78 | Ok(MakefileLine::RuleDefinition( 79 | String::from(target), 80 | dependencies 81 | .split_whitespace() 82 | .map(|s| s.to_owned()) 83 | .collect(), 84 | )) 85 | } else if line.is_empty() { 86 | Ok(MakefileLine::EmptyLine) 87 | } else { 88 | eprintln!("Failed to parse line: {:?}", original); 89 | Err(ParseError::LineParse) 90 | } 91 | } 92 | 93 | pub fn parse_makefile(input: &String) -> Result { 94 | let mut makefile = Makefile::new(Vec::new()); 95 | 96 | for line in input.lines() { 97 | match parse_line(&line.to_owned())? { 98 | MakefileLine::EmptyLine => {} 99 | 100 | MakefileLine::Command(command) => { 101 | let last_index = makefile.rules.len() - 1; 102 | if let Some(rule) = makefile.rules.get_mut(last_index) { 103 | if !command.is_empty() { 104 | rule.commands.push(Command::with(command)); 105 | } 106 | } else { 107 | return Err(ParseError::CommandOutsideOfRule); 108 | } 109 | } 110 | 111 | MakefileLine::RuleDefinition(target, dependencies) => { 112 | makefile.rules.push(Rule::new( 113 | Target::named(target), 114 | dependencies 115 | .iter() 116 | .map(|dep| Target::named(dep.clone())) 117 | .collect::>(), 118 | Vec::new(), 119 | )); 120 | } 121 | } 122 | } 123 | 124 | Ok(makefile) 125 | } 126 | 127 | #[cfg(test)] 128 | mod tests { 129 | use super::*; 130 | 131 | #[test] 132 | fn makefile_empty_test() { 133 | assert_eq!( 134 | parse_makefile(&"".to_owned()), 135 | Ok(Makefile::new(Vec::new())) 136 | ); 137 | } 138 | 139 | #[test] 140 | fn makefile_one_rule_test() { 141 | assert_eq!( 142 | parse_makefile(&"all: build test".to_owned()), 143 | Ok(Makefile::new(vec![Rule::new( 144 | Target::named("all"), 145 | vec![Target::named("build"), Target::named("test")], 146 | vec![] 147 | )])) 148 | ); 149 | } 150 | 151 | #[test] 152 | fn makefile_blank_line_test() { 153 | assert_eq!( 154 | parse_makefile(&"\nall: build test\n".to_owned()), 155 | Ok(Makefile::new(vec![Rule::new( 156 | Target::named("all"), 157 | vec![Target::named("build"), Target::named("test")], 158 | vec![] 159 | )])) 160 | ); 161 | } 162 | 163 | #[test] 164 | fn makefile_with_multiple_rules_test() { 165 | assert_eq!( 166 | parse_makefile(&"all: a\n\na:\n\tfoo".to_owned()), 167 | Ok(Makefile::new(vec![ 168 | Rule::new(Target::named("all"), vec![Target::named("a")], vec![]), 169 | Rule::new(Target::named("a"), vec![], vec![Command::with("foo")]) 170 | ])) 171 | ); 172 | } 173 | 174 | #[test] 175 | fn makefile_with_commands_test() { 176 | assert_eq!( 177 | parse_makefile(&"foo: bar baz\n\tone\n\ttwo\n".to_owned()), 178 | Ok(Makefile::new(vec![Rule::new( 179 | Target::named("foo"), 180 | vec![Target::named("bar"), Target::named("baz")], 181 | vec![Command::with("one"), Command::with("two")] 182 | )])) 183 | ); 184 | } 185 | 186 | #[test] 187 | fn makefile_commands_with_blank_lines_test() { 188 | assert_eq!( 189 | parse_makefile(&"foo:\n\tone\n\n\ttwo\n\t\n\tthree\n".to_owned()), 190 | Ok(Makefile::new(vec![Rule::new( 191 | Target::named("foo"), 192 | vec![], 193 | vec![ 194 | Command::with("one"), 195 | Command::with("two"), 196 | Command::with("three") 197 | ] 198 | )])) 199 | ); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/plan.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::makefile::{Makefile, Rule, Target}; 4 | 5 | #[derive(Clone, Debug, PartialEq)] 6 | pub struct Invocation { 7 | pub rule: Rule, 8 | pub target: Target, 9 | } 10 | 11 | pub type Phase = Vec; 12 | 13 | #[derive(Debug, PartialEq)] 14 | pub struct Plan { 15 | pub makefile: Makefile, 16 | pub phases: Vec, 17 | } 18 | 19 | /// 20 | /// Plans the execution of a `Makefile` to build a `Target`. 21 | /// 22 | /// It breaks up the dependency tree into a set of "phases", such that any 23 | /// dependencies of invocations in a phase are resolved in a prior phase. 24 | /// 25 | /// For example, if we have a makefile with two rules `a: b` and `b:`, then 26 | /// planning to build `a` will create two phases: 1) build `a`, then 2) build `b`: 27 | /// 28 | /// ``` 29 | /// use fab::makefile::{parse_makefile, Target, Rule}; 30 | /// use fab::plan::{plan_execution, Plan, Invocation}; 31 | /// 32 | /// let makefile = parse_makefile(&"a: b\nb:\n".to_string()).unwrap(); 33 | /// let plan = plan_execution( 34 | /// makefile.clone(), 35 | /// &Target::named("a") 36 | /// ); 37 | /// 38 | /// assert_eq!(Plan { makefile: makefile.clone(), phases: vec![ 39 | /// vec![Invocation { rule: makefile.rules[1].clone(), target: Target::named("b") }], 40 | /// vec![Invocation { rule: makefile.rules[0].clone(), target: Target::named("a") }] 41 | /// ] }, plan); 42 | /// ``` 43 | /// 44 | pub fn plan_execution(makefile: Makefile, target: &Target) -> Plan { 45 | let mut rule_for_target = HashMap::new(); 46 | let mut rank_for_target: HashMap = HashMap::new(); 47 | let mut max_rank = 0; 48 | 49 | let mut open: Vec = Vec::new(); 50 | let mut closed: Vec = Vec::new(); 51 | 52 | open.push(target.clone()); 53 | 54 | while let Some(name) = open.pop() { 55 | let rule = rule_for_target 56 | .entry(name.clone()) 57 | .or_insert_with(|| find_rule(&makefile, &name).clone()); 58 | 59 | let mut rank = 0; 60 | let mut missing_dependencies: Vec = Vec::new(); 61 | 62 | for dependency in &rule.dependencies { 63 | if closed.contains(&dependency) { 64 | rank = u32::max(rank, &rank_for_target[&dependency] + 1); 65 | max_rank = u32::max(max_rank, rank); 66 | } else { 67 | missing_dependencies.push(dependency.clone()); 68 | } 69 | } 70 | 71 | if missing_dependencies.is_empty() { 72 | rank_for_target.insert(name.clone(), rank); 73 | closed.push(name.clone()); 74 | } else { 75 | open.push(name.clone()); 76 | for dep in missing_dependencies { 77 | if let Ok(index) = open.binary_search(&&dep) { 78 | open.remove(index); 79 | } 80 | 81 | open.push(dep.clone()); 82 | } 83 | } 84 | } 85 | 86 | let mut phases: Vec = Vec::new(); 87 | phases.resize((max_rank as usize) + 1, Phase::new()); 88 | 89 | for (target, rank) in &rank_for_target { 90 | let rule = rule_for_target[target].clone(); 91 | 92 | phases[*rank as usize].push(Invocation { 93 | rule: rule.clone(), 94 | target: target.clone(), 95 | }); 96 | } 97 | 98 | Plan { makefile, phases } 99 | } 100 | 101 | /// Finds a rule matching the target in a `Makefile`. 102 | /// 103 | /// ``` 104 | /// # use fab::makefile::{Rule, Makefile, Target}; 105 | /// # use fab::plan::{find_rule}; 106 | /// 107 | /// let compile_c_code_rule = Rule { 108 | /// target: Target::named("main.c"), 109 | /// dependencies: vec![Target::named("main.o")], 110 | /// commands: vec![] 111 | /// }; 112 | /// 113 | /// let makefile = Makefile { rules: vec![compile_c_code_rule.clone()] }; 114 | /// assert_eq!(compile_c_code_rule, find_rule(&makefile, &Target::named("main.c"))); 115 | /// ``` 116 | 117 | pub fn find_rule<'a>(makefile: &Makefile, target: &Target) -> Rule { 118 | makefile 119 | .rules 120 | .iter() 121 | .find(|r| r.target == *target) 122 | .unwrap_or_else(|| panic!("No such rule {:?}", target)) 123 | .clone() 124 | } 125 | --------------------------------------------------------------------------------