omake/
makefile.rs

1mod opts;
2mod rule_map;
3
4pub use opts::Opts;
5
6use std::io::{BufRead, BufReader};
7use std::path::PathBuf;
8use std::time::{Duration, SystemTime, UNIX_EPOCH};
9use std::{fs, fs::File};
10
11use crate::context::Context;
12use crate::error::MakeError;
13use crate::expand::expand;
14use crate::logger::Logger;
15use crate::vars::Vars;
16
17use rule_map::{Rule, RuleMap};
18
19const COMMENT_INDICATOR: char = '#';
20
21// struct PhysicalLine {
22//     content: String,
23//     index: usize,
24// }
25
26// struct LogicalLine {
27//     physical_lines: Vec<PhysicalLine>,
28//     smushed: String,
29//     breaks: Vec<usize>,
30// }
31
32/// The internal representation of a makefile.
33#[derive(Debug)]
34pub struct Makefile<L: Logger> {
35    pub opts: Opts,
36    pub logger: Box<L>,
37
38    rule_map: RuleMap,
39    default_target: Option<String>,
40
41    // Parser state.
42    pub vars: Vars,
43    current_rule: Option<Rule>,
44    context: Context,
45}
46
47impl<L: Logger> Makefile<L> {
48    /// Principal interface for reading and parsing a makefile.
49    pub fn new(path: PathBuf, opts: Opts, logger: Box<L>, vars: Vars) -> Result<Self, MakeError> {
50        // Initialize the `Makefile` struct with default values.
51        let mut makefile = Self {
52            opts,
53            logger: logger,
54            rule_map: RuleMap::new(),
55            default_target: None,
56            vars: vars,
57            current_rule: None,
58            context: path.clone().into(),
59        };
60
61        // Open the makefile and run it through the parser.
62        let file = File::open(&path).map_err(|e| {
63            MakeError::new(format!("Could not read makefile ({}).", e), path.into())
64        })?;
65        makefile.parse(BufReader::new(file))?;
66
67        Ok(makefile)
68    }
69
70    /// Iterate over the makefile's lines, call `parse_line` to handle the actual parsing logic, and
71    /// manage context.
72    fn parse<R: BufRead>(&mut self, stream: R) -> Result<(), MakeError> {
73        self.current_rule = None;
74
75        for (i, result) in stream.lines().enumerate() {
76            // Set the context line number and extract the line.
77            self.context.line_index = Some(i);
78            let line = result.map_err(|e| MakeError::new(e.to_string(), self.context.clone()))?;
79            self.context.content = Some(line.clone());
80
81            // Parse the line.
82            self.parse_line(line)?;
83        }
84
85        // Always push two blank lines at the end to terminate trailing rules, even if the last rule
86        // contained a trailing backslash.
87        self.parse_line("".to_string())?;
88        self.parse_line("".to_string())?;
89
90        Ok(())
91    }
92
93    /// The line parser is where the "meat" of the parsing occurs. This is responsible for
94    /// extracting rules from the physical lines of the makefile stream, properly handling escaped
95    /// newlines and semicolons, and also managing state, such as variable assignments and
96    /// annotating when the parser moves in-to and out-of a rule definition.
97    fn parse_line(&mut self, line: String) -> Result<(), MakeError> {
98        // Handle recipe lines.
99        let recipe_prefix = &self.vars.get(".RECIPEPREFIX").value;
100        if line.starts_with(recipe_prefix) {
101            // If line starts with the recipe prefix, then push it to the current rule.
102            match &mut self.current_rule {
103                None => return Err(MakeError::new("recipe without rule", self.context.clone())),
104                Some(r) => {
105                    // Strip the recipe prefix first.
106                    let cmd = line
107                        .strip_prefix(recipe_prefix)
108                        .expect("line known to start with a recipe prefix")
109                        .trim()
110                        .to_string();
111
112                    if !cmd.is_empty() {
113                        r.recipe.push(
114                            expand(cmd.as_str(), &self.vars)
115                                .map_err(|e| MakeError::new(e, self.context.clone()))?,
116                        );
117                    }
118                }
119            }
120            return Ok(());
121        }
122
123        // Anything other than recipe lines terminate a rule definition.
124        if let Some(rule) = self.current_rule.take() {
125            // If there is no default target, see if we can assign one.
126            if self.default_target.is_none() {
127                for target in rule.targets.iter() {
128                    // Set default target if none is specified and this is a normal target.
129                    if self.default_target.is_none() && !target.starts_with('.') {
130                        self.default_target = Some(target.clone());
131                    }
132                }
133            }
134
135            // Add the rule to the `rule_map`.
136            self.rule_map.insert(rule, &self.logger)?;
137        }
138
139        // Ignore pure comments and blank lines.
140        let trimmed_line = line.trim();
141        if trimmed_line.starts_with(COMMENT_INDICATOR) || trimmed_line.is_empty() {
142            return Ok(());
143        }
144
145        // Handle rule definitions.
146        if let Some((targets, mut deps)) = line.split_once(':') {
147            // First, if deps start with another `:`, then this is a double-colon rule, so we should
148            // mark it as such.
149            let mut double_colon = false;
150            if let Some(ch) = deps.chars().next() {
151                if ch == ':' {
152                    deps = &deps[1..];
153                    double_colon = true;
154                }
155            }
156
157            // There could be a semicolon after prerequisites, in which case we should parse
158            // everything after that as a rule line.
159            let rule = deps.split_once(';').map(|(d, r)| {
160                deps = d;
161                r
162            });
163
164            self.current_rule = Some(Rule {
165                targets: expand(targets, &self.vars)
166                    .map_err(|e| MakeError::new(e, self.context.clone()))?
167                    .split_whitespace()
168                    .map(|s| s.to_string())
169                    .collect(),
170                prerequisites: expand(deps, &self.vars)
171                    .map_err(|e| MakeError::new(e, self.context.clone()))?
172                    .split_whitespace()
173                    .map(|s| s.to_string())
174                    .collect(),
175                recipe: vec![],
176                context: self.context.clone(),
177                double_colon,
178            });
179
180            // Add rule line if we found one.
181            if let Some(r) = rule {
182                self.parse_line(format!("{}{}", self.vars.get(".RECIPEPREFIX").value, r))?;
183            }
184
185            return Ok(());
186        }
187
188        // Handle variable assignments.
189        if let Some((k, v)) = line.split_once('=') {
190            if let Err(e) = self.vars.set(
191                k,
192                &expand(v.trim_start(), &self.vars)
193                    .map_err(|e| MakeError::new(e, self.context.clone()))?,
194                false,
195            ) {
196                return Err(MakeError::new(e, self.context.clone()));
197            };
198            return Ok(());
199        }
200
201        // Otherwise, throw error if line is not recognizable.
202        Err(MakeError::new("Invalid line type.", self.context.clone()))
203    }
204
205    /// Principal interface for executing a parsed makefile, given a list of targets.
206    pub fn execute(&self, mut targets: Vec<String>) -> Result<(), MakeError> {
207        // Set targets list to default target if none were provided.
208        if targets.is_empty() {
209            match &self.default_target {
210                None => {
211                    return Err(MakeError::new(
212                        "No target specified and no default target found.",
213                        Context::new(),
214                    ))
215                }
216                Some(t) => targets.push(t.clone()),
217            }
218        }
219
220        for target in targets {
221            self.rule_map.execute(self, &target)?;
222        }
223
224        Ok(())
225    }
226
227    /// Get the `mtime` of a file. Note that the return value also signals whether or not the file
228    /// is accessible, so a `None` value represents either the file not existing or the current user
229    /// not having the appropriate permissions to access the file.
230    ///
231    /// TODO: Consider bailing on a file permissions issue? Not sure if POSIX specifies some
232    /// behavior here or if the major implementations halt execution on a permissions error.
233    fn get_mtime(&self, file: &String) -> Option<SystemTime> {
234        match fs::metadata(file) {
235            Ok(metadata) => {
236                if self.opts.old_file.contains(file) {
237                    Some(UNIX_EPOCH)
238                } else if self.opts.new_file.contains(file) {
239                    // 1 year in the future.
240                    Some(SystemTime::now() + Duration::from_secs(365 * 24 * 60 * 60))
241                } else {
242                    metadata.modified().ok()
243                }
244            }
245            Err(_) => None,
246        }
247    }
248}