2022-06-18 21:56:31 -07:00
|
|
|
use super::*;
|
2023-01-12 19:25:28 -08:00
|
|
|
use std::collections::HashSet;
|
|
|
|
|
|
|
|
struct LinesWithEndings<'a> {
|
|
|
|
input: &'a str,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> LinesWithEndings<'a> {
|
|
|
|
fn new(input: &'a str) -> Self {
|
|
|
|
Self { input }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Iterator for LinesWithEndings<'a> {
|
|
|
|
type Item = &'a str;
|
|
|
|
|
|
|
|
fn next(&mut self) -> Option<&'a str> {
|
|
|
|
if self.input.is_empty() {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
let split = self.input.find('\n').map_or(self.input.len(), |i| i + 1);
|
|
|
|
let (line, rest) = self.input.split_at(split);
|
|
|
|
self.input = rest;
|
|
|
|
Some(line)
|
|
|
|
}
|
|
|
|
}
|
2021-07-26 01:26:06 -07:00
|
|
|
|
|
|
|
pub(crate) struct Loader {
|
|
|
|
arena: Arena<String>,
|
2023-01-12 19:25:28 -08:00
|
|
|
unstable: bool,
|
2021-07-26 01:26:06 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Loader {
|
2023-01-12 19:25:28 -08:00
|
|
|
pub(crate) fn new(unstable: bool) -> Self {
|
2021-07-26 01:26:06 -07:00
|
|
|
Loader {
|
|
|
|
arena: Arena::new(),
|
2023-01-12 19:25:28 -08:00
|
|
|
unstable,
|
2021-07-26 01:26:06 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn load<'src>(&'src self, path: &Path) -> RunResult<&'src str> {
|
2023-01-12 19:25:28 -08:00
|
|
|
let src = self.load_recursive(path, HashSet::new())?;
|
|
|
|
Ok(self.arena.alloc(src))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn load_file<'a>(path: &Path) -> RunResult<'a, String> {
|
|
|
|
fs::read_to_string(path).map_err(|io_error| Error::Load {
|
2021-07-26 01:26:06 -07:00
|
|
|
path: path.to_owned(),
|
|
|
|
io_error,
|
2023-01-12 19:25:28 -08:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn load_recursive(&self, file: &Path, seen: HashSet<PathBuf>) -> RunResult<String> {
|
|
|
|
let src = Self::load_file(file)?;
|
|
|
|
|
|
|
|
let mut output = String::new();
|
|
|
|
|
|
|
|
let mut seen_content = false;
|
|
|
|
|
|
|
|
for (i, line) in LinesWithEndings::new(&src).enumerate() {
|
|
|
|
if !seen_content && line.starts_with('!') {
|
|
|
|
let include = line
|
|
|
|
.strip_prefix("!include")
|
|
|
|
.ok_or_else(|| Error::InvalidDirective { line: line.into() })?;
|
|
|
|
|
|
|
|
if !self.unstable {
|
|
|
|
return Err(Error::Unstable {
|
|
|
|
message: "The !include directive is currently unstable.".into(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
let argument = include.trim();
|
|
|
|
|
|
|
|
if argument.is_empty() {
|
|
|
|
return Err(Error::IncludeMissingPath {
|
|
|
|
file: file.to_owned(),
|
|
|
|
line: i,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
let contents = self.process_include(file, Path::new(argument), &seen)?;
|
|
|
|
|
|
|
|
output.push_str(&contents);
|
|
|
|
} else {
|
|
|
|
if !(line.trim().is_empty() || line.trim().starts_with('#')) {
|
|
|
|
seen_content = true;
|
|
|
|
}
|
|
|
|
output.push_str(line);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(output)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn process_include(
|
|
|
|
&self,
|
|
|
|
file: &Path,
|
|
|
|
include: &Path,
|
|
|
|
seen: &HashSet<PathBuf>,
|
|
|
|
) -> RunResult<String> {
|
|
|
|
let canonical_path = if include.is_relative() {
|
|
|
|
let current_dir = file.parent().ok_or(Error::Internal {
|
|
|
|
message: format!(
|
|
|
|
"Justfile path `{}` has no parent directory",
|
|
|
|
include.display()
|
|
|
|
),
|
|
|
|
})?;
|
|
|
|
current_dir.join(include)
|
|
|
|
} else {
|
|
|
|
include.to_owned()
|
|
|
|
};
|
|
|
|
|
|
|
|
let canonical_path = canonical_path.lexiclean();
|
|
|
|
|
|
|
|
if seen.contains(&canonical_path) {
|
|
|
|
return Err(Error::CircularInclude {
|
|
|
|
current: file.to_owned(),
|
|
|
|
include: canonical_path,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut seen_paths = seen.clone();
|
|
|
|
seen_paths.insert(file.lexiclean());
|
|
|
|
|
|
|
|
self.load_recursive(&canonical_path, seen_paths)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::{Error, Lexiclean, Loader};
|
|
|
|
use temptree::temptree;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn include_justfile() {
|
|
|
|
let justfile_a = r#"
|
|
|
|
# A comment at the top of the file
|
|
|
|
!include ./justfile_b
|
|
|
|
|
|
|
|
some_recipe: recipe_b
|
|
|
|
echo "some recipe"
|
|
|
|
"#;
|
|
|
|
|
|
|
|
let justfile_b = r#"!include ./subdir/justfile_c
|
|
|
|
|
|
|
|
recipe_b: recipe_c
|
|
|
|
echo "recipe b"
|
|
|
|
"#;
|
|
|
|
|
|
|
|
let justfile_c = r#"recipe_c:
|
|
|
|
echo "recipe c"
|
|
|
|
"#;
|
|
|
|
|
|
|
|
let tmp = temptree! {
|
|
|
|
justfile: justfile_a,
|
|
|
|
justfile_b: justfile_b,
|
|
|
|
subdir: {
|
|
|
|
justfile_c: justfile_c
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let full_concatenated_output = r#"
|
|
|
|
# A comment at the top of the file
|
|
|
|
recipe_c:
|
|
|
|
echo "recipe c"
|
|
|
|
|
|
|
|
recipe_b: recipe_c
|
|
|
|
echo "recipe b"
|
|
|
|
|
|
|
|
some_recipe: recipe_b
|
|
|
|
echo "some recipe"
|
|
|
|
"#;
|
|
|
|
|
|
|
|
let loader = Loader::new(true);
|
|
|
|
|
|
|
|
let justfile_a_path = tmp.path().join("justfile");
|
|
|
|
let loader_output = loader.load(&justfile_a_path).unwrap();
|
|
|
|
|
|
|
|
assert_eq!(loader_output, full_concatenated_output);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn recursive_includes_fail() {
|
|
|
|
let justfile_a = r#"
|
|
|
|
# A comment at the top of the file
|
|
|
|
!include ./subdir/justfile_b
|
|
|
|
|
|
|
|
some_recipe: recipe_b
|
|
|
|
echo "some recipe"
|
|
|
|
|
|
|
|
"#;
|
|
|
|
|
|
|
|
let justfile_b = r#"
|
|
|
|
!include ../justfile
|
|
|
|
|
|
|
|
recipe_b:
|
|
|
|
echo "recipe b"
|
|
|
|
"#;
|
|
|
|
let tmp = temptree! {
|
|
|
|
justfile: justfile_a,
|
|
|
|
subdir: {
|
|
|
|
justfile_b: justfile_b
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let loader = Loader::new(true);
|
|
|
|
|
|
|
|
let justfile_a_path = tmp.path().join("justfile");
|
|
|
|
let loader_output = loader.load(&justfile_a_path).unwrap_err();
|
|
|
|
|
|
|
|
assert_matches!(loader_output, Error::CircularInclude { current, include }
|
|
|
|
if current == tmp.path().join("subdir").join("justfile_b").lexiclean() &&
|
|
|
|
include == tmp.path().join("justfile").lexiclean()
|
|
|
|
);
|
2021-07-26 01:26:06 -07:00
|
|
|
}
|
|
|
|
}
|