Dependency file parser now supports backslashes at the end of the line to continue the line, as well as multiple dependencies in the same file

This commit is contained in:
Filipe Rodrigues 2024-09-01 23:30:06 +01:00
parent aefeb9db4c
commit 0058abb8c0
Signed by: zenithsiz
SSH Key Fingerprint: SHA256:Mb5ppb3Sh7IarBO/sBTXLHbYEOz37hJAlslLQPPAPaU
3 changed files with 124 additions and 33 deletions

View File

@ -24,7 +24,12 @@ use {
futures::{stream::FuturesUnordered, StreamExt},
indexmap::IndexMap,
itertools::Itertools,
std::{collections::BTreeMap, future::Future, sync::Arc, time::SystemTime},
std::{
collections::{BTreeMap, HashMap},
future::Future,
sync::Arc,
time::SystemTime,
},
tokio::{fs, process, sync::Semaphore, task},
};
@ -635,39 +640,68 @@ impl Builder {
reason: &BuildReason,
) -> Result<Vec<(Target<ArcStr>, BuildResult, Option<BuildLockDepGuard>)>, AppError> {
tracing::trace!(target=?parent_target, ?rule.name, ?deps_file, "Building dependencies of target rule dependency-file");
let (output, deps) = self::parse_deps_file(deps_file).await?;
let mut deps = self::parse_deps_file(deps_file).await?;
tracing::trace!(target=?parent_target, ?rule.name, ?deps_file, ?deps, "Found dependencies of target rule dependency-file");
match rule.output.is_empty() {
// Ensure that at least one dependency output matches the rule
let matches_rule = |output: &str| match rule.output.is_empty() {
// If there were no outputs, make sure it matches the rule name
// TODO: Seems kinda weird for it to match the rule name, but not sure how else to check this here
true =>
if output != *rule.name {
return Err(AppError::DepFileMissingRuleName {
deps_file_path: deps_file.into(),
rule_name: rule.name.to_string(),
dep_output: output,
});
},
true => (output == &*rule.name)
.then_some(())
.ok_or_else(|| AppError::DepFileMissingRuleName {
deps_file_path: deps_file.into(),
rule_name: rule.name.to_string(),
dep_output: output.to_owned(),
}),
// If there were any output, make sure the dependency file applies to one of them
false => {
let any_matches = rule.output.iter().any(|out| match out {
OutItem::File { file, .. } => **file == output,
false => rule
.output
.iter()
.any(|out| match out {
OutItem::File { file, .. } => &**file == output,
})
.then_some(())
.ok_or_else(|| AppError::DepFileMissingOutputs {
deps_file_path: deps_file.into(),
rule_outputs: rule.output.iter().map(OutItem::to_string).collect(),
dep_output: output.to_owned(),
}),
};
// Find all outputs that match and those that don't match
let (oks, errs) = deps
.keys()
.map(|output| matches_rule(output).map_err(|err| (output.clone(), err)))
.partition_result::<Vec<_>, Vec<_>, _, _>();
match oks.is_empty() {
true => {
// If we had no matching outputs, try to return all errors
errs.into_iter()
.map(|(_, err)| Err(err))
.collect::<ResultMultiple<()>>()?;
// If no errors existed, return an error for that
return Err(AppError::DepFileEmpty {
deps_file_path: deps_file.into(),
});
if !any_matches {
return Err(AppError::DepFileMissingOutputs {
deps_file_path: deps_file.into(),
rule_outputs: rule.output.iter().map(OutItem::to_string).collect(),
dep_output: output,
});
}
},
// Otherwise, just log and remove all errors
false =>
for (output, err) in errs {
let _: Vec<_> = deps.remove(&output).expect("Dependency should exist");
tracing::warn!(target=%parent_target, ?rule.name, err=%err.pretty(), "Ignoring unknown output in dependency file");
},
}
// Build all dependencies
// Note: We don't want to fail-early, see the note on `deps` in `build_unchecked`
let deps_res = deps
.into_iter()
.flat_map(|(_, deps)| deps)
.map(|dep| {
let dep = ArcStr::from(util::normalize_path(&dep));
tracing::trace!(?rule.name, ?dep, "Found rule dependency");
@ -757,19 +791,35 @@ impl Builder {
}
/// Parses a dependencies file
// TODO: Support multiple dependencies in each file
async fn parse_deps_file(file: &str) -> Result<(String, Vec<String>), AppError> {
async fn parse_deps_file(file: &str) -> Result<HashMap<ArcStr, Vec<ArcStr>>, AppError> {
// Read it
let contents = fs::read_to_string(file).await.map_err(AppError::read_file(file))?;
let mut contents = fs::read_to_string(file).await.map_err(AppError::read_file(file))?;
// Parse it
let (output, deps) = contents.split_once(':').ok_or_else(|| AppError::DepFileMissingColon {
deps_file_path: file.into(),
})?;
let output = output.trim().to_owned();
let deps = deps.split_whitespace().map(str::to_owned).collect();
// Replace all backslashes at the end of a line with spaces
// Note: Although it'd be fine to replace it with a single space, by replacing it
// with two, we avoid having to move the string contents
util::string_replace_in_place_with(&mut contents, "\\\n", " ");
Ok((output, deps))
// Now go through all non-empty lines and parse them
let deps = contents
.lines()
.filter_map(|line| {
let line = line.trim();
(!line.is_empty()).then_some(line)
})
.map(|line| {
// Parse it
let (output, deps) = line.split_once(':').ok_or_else(|| AppError::DepFileMissingColon {
deps_file_path: file.into(),
})?;
let output = ArcStr::from(output.trim());
let deps = deps.split_whitespace().map(ArcStr::from).collect();
Ok((output, deps))
})
.collect::<ResultMultiple<_>>()?;
Ok(deps)
}
/// Returns the last build time of a rule.

View File

@ -528,7 +528,7 @@ decl_error! {
/// Dependencies file missing rule name
#[source(None)]
#[fmt("Dependencies file {deps_file_path:?} is missing the rule name {rule_name}, found {dep_output}")]
#[fmt("Dependencies file {deps_file_path:?} is missing the rule name {rule_name:?}, found {dep_output:?}")]
DepFileMissingRuleName {
/// Dep file path
deps_file_path: PathBuf,
@ -542,7 +542,7 @@ decl_error! {
/// Dependencies file missing rule name
#[source(None)]
#[fmt("Dependencies file {deps_file_path:?} is missing any output of {rule_outputs:?}, found {dep_output}")]
#[fmt("Dependencies file {deps_file_path:?} is missing any output of {rule_outputs:?}, found {dep_output:?}")]
DepFileMissingOutputs {
/// Dep file path
deps_file_path: PathBuf,
@ -554,6 +554,14 @@ decl_error! {
dep_output: String,
},
/// Dependencies file empty
#[source(None)]
#[fmt("Dependencies file {deps_file_path:?} had no dependencies")]
DepFileEmpty {
/// Dep file path
deps_file_path: PathBuf,
},
/// Rule executable was empty
#[source(None)]
#[fmt("Rule {rule_name} executable as empty")]

View File

@ -12,8 +12,10 @@ use {
pin_project::pin_project,
std::{
io,
mem,
path::{self, Path, PathBuf},
pin::Pin,
str::pattern::Pattern,
task,
time::{Duration, Instant},
},
@ -116,6 +118,37 @@ pub fn normalize_path(path: &str) -> String {
path
}
/// In-place replaces matching parts of a string.
#[expect(
clippy::needless_pass_by_value,
reason = "It's more ergonomic to pass patterns by value"
)]
pub fn string_replace_in_place_with<P>(s: &mut String, pat: P, replace_with: &str)
where
P: Pattern + Clone,
{
let mut cur_idx = 0;
let mut matches = s.match_indices(pat.clone());
// Find all matches, replacing the range as we go.
#[expect(
clippy::string_slice,
reason = "The index will always be valid, as it's the end of the string returned by `match_indices`, which \
must return substrings of the string"
)]
while let Some((pos, part)) = matches.next() {
// Replace the range
mem::drop(matches);
s.replace_range(cur_idx + pos..cur_idx + pos + part.len(), replace_with);
// After replacing `...ABC` with `...DEF`, put ourselves as the end of
// the string we just replaced, to avoid recursively replacing the same
// pattern over and over.
cur_idx += pos + replace_with.len();
matches = s[cur_idx..].match_indices(pat.clone());
}
}
#[cfg(test)]
mod tests {
#[test]