mirror of
https://github.com/Zenithsiz/zbuild.git
synced 2026-02-03 14:10:02 +00:00
Replaced anyhow and AppError with zutil_app_error.
This commit is contained in:
parent
eedd24d9b5
commit
5217f091b6
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -20,7 +20,8 @@
|
||||
"tempdir",
|
||||
"thiserror",
|
||||
"yeet",
|
||||
"Zbuild"
|
||||
"Zbuild",
|
||||
"zutil"
|
||||
],
|
||||
"rust-analyzer.cargo.features": "all"
|
||||
}
|
||||
|
||||
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -1799,7 +1799,6 @@ dependencies = [
|
||||
name = "zbuild"
|
||||
version = "0.1.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
"clap",
|
||||
"console-subscriber",
|
||||
@ -1818,6 +1817,7 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
"tracing-test",
|
||||
"unicode-ident",
|
||||
"zutil-app-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1840,3 +1840,11 @@ dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zutil-app-error"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/Zenithsiz/zutil?rev=5363bba6ced162185a1eb5a132cce499bfc5d818#5363bba6ced162185a1eb5a132cce499bfc5d818"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
]
|
||||
|
||||
@ -10,7 +10,6 @@ publish = ["filipejr"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
anyhow = "1.0.95"
|
||||
async-broadcast = "0.7.2"
|
||||
clap = { version = "4.5.27", features = ["derive"] }
|
||||
console-subscriber = { version = "0.4.1", optional = true }
|
||||
@ -27,6 +26,7 @@ tokio-stream = "0.1.17"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
unicode-ident = "1.0.16"
|
||||
zutil-app-error = { git = "https://github.com/Zenithsiz/zutil", rev = "5363bba6ced162185a1eb5a132cce499bfc5d818" }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
|
||||
52
src/ast.rs
52
src/ast.rs
@ -7,7 +7,7 @@
|
||||
)]
|
||||
|
||||
// Imports
|
||||
use {anyhow::Context, std::str::pattern::Pattern};
|
||||
use {crate::AppError, std::str::pattern::Pattern, zutil_app_error::Context};
|
||||
|
||||
/// Zbuild ast
|
||||
#[derive(Clone, Debug)]
|
||||
@ -27,7 +27,7 @@ pub struct Ast<'a> {
|
||||
|
||||
impl<'a> Ast<'a> {
|
||||
/// Parses a full ast from `input`.
|
||||
pub fn parse_full(input: &'a str) -> Result<Self, anyhow::Error> {
|
||||
pub fn parse_full(input: &'a str) -> Result<Self, AppError> {
|
||||
let mut parser = Parser::new(input);
|
||||
let ast = Self::parse_from(&mut parser).with_context(|| {
|
||||
let remaining = parser.remaining();
|
||||
@ -36,14 +36,14 @@ impl<'a> Ast<'a> {
|
||||
false => format!("Error at:\n'''\n{remaining}\n'''"),
|
||||
}
|
||||
})?;
|
||||
anyhow::ensure!(parser.is_finished()?, "Unexpected tokens at the end");
|
||||
zutil_app_error::ensure!(parser.is_finished()?, "Unexpected tokens at the end");
|
||||
|
||||
Ok(ast)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Parsable<'a> for Ast<'a> {
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, anyhow::Error> {
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, AppError> {
|
||||
let mut aliases = vec![];
|
||||
let mut pats = vec![];
|
||||
let mut defaults = vec![];
|
||||
@ -81,7 +81,7 @@ pub struct AliasStmt<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Parsable<'a> for AliasStmt<'a> {
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, anyhow::Error> {
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, AppError> {
|
||||
parser.parse::<TokenAlias<'a>>()?;
|
||||
let name = parser.parse::<Ident<'a>>().context("Expected alias name")?;
|
||||
parser.parse::<TokenEq<'a>>()?;
|
||||
@ -103,7 +103,7 @@ pub struct PatStmt<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Parsable<'a> for PatStmt<'a> {
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, anyhow::Error> {
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, AppError> {
|
||||
parser.parse::<TokenPat<'a>>()?;
|
||||
|
||||
let non_empty = parser.try_parse::<TokenNonEmpty<'a>>().is_ok();
|
||||
@ -124,7 +124,7 @@ pub struct DefaultStmt<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Parsable<'a> for DefaultStmt<'a> {
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, anyhow::Error> {
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, AppError> {
|
||||
parser.parse::<TokenDefault<'a>>()?;
|
||||
let default = parser.parse::<Expr<'a>>().context("Expected default expression")?;
|
||||
parser.parse::<TokenSemi<'a>>()?;
|
||||
@ -156,7 +156,7 @@ pub struct RuleStmt<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Parsable<'a> for RuleStmt<'a> {
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, anyhow::Error> {
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, AppError> {
|
||||
parser.parse::<TokenRule<'a>>()?;
|
||||
let name = parser.parse::<Ident<'a>>().context("Expected rule name")?;
|
||||
parser.parse::<TokenBracesOpen<'a>>()?;
|
||||
@ -211,7 +211,7 @@ pub struct Command<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Parsable<'a> for Command<'a> {
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, anyhow::Error> {
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, AppError> {
|
||||
let mut cwd = None;
|
||||
let mut args = None;
|
||||
|
||||
@ -227,7 +227,7 @@ impl<'a> Parsable<'a> for Command<'a> {
|
||||
match key.0 {
|
||||
"cwd" => cwd = Some(parser.parse::<Expr<'a>>()?),
|
||||
"args" => args = Some(parser.parse::<Array<Expr<'a>>>()?),
|
||||
key => anyhow::bail!("Unknown key: `{key:?}`"),
|
||||
key => zutil_app_error::bail!("Unknown key: `{key:?}`"),
|
||||
}
|
||||
|
||||
match parser.parse::<AnyOf2<TokenComma<'a>, TokenBracketClose<'a>>>()? {
|
||||
@ -261,7 +261,7 @@ impl<'a, T> Parsable<'a> for Array<T>
|
||||
where
|
||||
T: Parsable<'a>,
|
||||
{
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, anyhow::Error> {
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, AppError> {
|
||||
let mut values = vec![];
|
||||
|
||||
match parser.try_parse::<TokenBracketOpen<'a>>() {
|
||||
@ -313,7 +313,7 @@ pub struct Expr<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Parsable<'a> for Expr<'a> {
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, anyhow::Error> {
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, AppError> {
|
||||
let mut is_deps_file = false;
|
||||
let mut is_static = false;
|
||||
let mut is_opt = false;
|
||||
@ -352,7 +352,7 @@ pub enum ExprCmpt<'a> {
|
||||
|
||||
impl<'a> ExprCmpt<'a> {
|
||||
/// Parses a list of expression components from a parser
|
||||
pub fn parse_many(parser: &mut Parser<'a>, cmpts: &mut Vec<Self>) -> Result<(), anyhow::Error> {
|
||||
pub fn parse_many(parser: &mut Parser<'a>, cmpts: &mut Vec<Self>) -> Result<(), AppError> {
|
||||
match parser
|
||||
.parse::<AnyOf2<Ident<'a>, TokenDoubleQuote<'a>>>()
|
||||
.context("Expected an identifier, or literal")?
|
||||
@ -365,7 +365,7 @@ impl<'a> ExprCmpt<'a> {
|
||||
let op = parser.parse::<Ident<'a>>().context("Expected identifier after `.`")?;
|
||||
match op.0 {
|
||||
"dir_name" => ops.push(ExprOp::DirName),
|
||||
op => anyhow::bail!("Unknown expression operator: {op:?}"),
|
||||
op => zutil_app_error::bail!("Unknown expression operator: {op:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -375,7 +375,7 @@ impl<'a> ExprCmpt<'a> {
|
||||
AnyOf2::T1(_) => {
|
||||
while parser.try_parse::<TokenDoubleQuote<'a>>().is_err() {
|
||||
let Some(end_idx) = parser.remaining().find(['{', '"']) else {
|
||||
anyhow::bail!("Expected closing `\"` after `\"`");
|
||||
zutil_app_error::bail!("Expected closing `\"` after `\"`");
|
||||
};
|
||||
let prefix = parser.advance_by(end_idx);
|
||||
if !prefix.is_empty() {
|
||||
@ -413,7 +413,7 @@ pub enum ExprOp {
|
||||
pub struct Ident<'a>(pub &'a str);
|
||||
|
||||
impl<'a> Parsable<'a> for Ident<'a> {
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, anyhow::Error> {
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, AppError> {
|
||||
let orig_input = parser.remaining();
|
||||
parser
|
||||
.strip_prefix(unicode_ident::is_xid_start)
|
||||
@ -431,10 +431,10 @@ pub macro decl_tokens($($TokenName:ident = $Token:expr;)*) {
|
||||
pub struct $TokenName<'a>(pub &'a str);
|
||||
|
||||
impl<'a> Parsable<'a> for $TokenName<'a> {
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, anyhow::Error> {
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, AppError> {
|
||||
match parser.strip_prefix($Token) {
|
||||
Some(value) => Ok(Self(value)),
|
||||
None => anyhow::bail!("Expected {:?}", $Token),
|
||||
None => zutil_app_error::bail!("Expected {:?}", $Token),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -469,7 +469,7 @@ decl_tokens! {
|
||||
|
||||
pub trait Parsable<'a>: Sized {
|
||||
/// Parses this type from `input`, mutating it in-place.
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, anyhow::Error>;
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, AppError>;
|
||||
}
|
||||
|
||||
/// Parser
|
||||
@ -494,12 +494,12 @@ impl<'a> Parser<'a> {
|
||||
///
|
||||
/// Panics if `idx` isn't a utf-8 codepoint boundary.
|
||||
/// Panics if `idx` is out of bounds.
|
||||
pub fn ch_at(&self, idx: usize) -> char {
|
||||
pub fn _ch_at(&self, idx: usize) -> char {
|
||||
self.input[idx..].chars().next().expect("Index was out of bounds")
|
||||
}
|
||||
|
||||
/// Returns if the parser is finished
|
||||
pub fn is_finished(&mut self) -> Result<bool, anyhow::Error> {
|
||||
pub fn is_finished(&mut self) -> Result<bool, AppError> {
|
||||
self.trim()?;
|
||||
|
||||
Ok(self.input.is_empty())
|
||||
@ -518,7 +518,7 @@ impl<'a> Parser<'a> {
|
||||
///
|
||||
/// - Whitespace
|
||||
/// - Comments
|
||||
pub fn trim(&mut self) -> Result<(), anyhow::Error> {
|
||||
pub fn trim(&mut self) -> Result<(), AppError> {
|
||||
while self
|
||||
.input
|
||||
.starts_with(|ch: char| ch.is_whitespace() || matches!(ch, '#'))
|
||||
@ -553,7 +553,7 @@ impl<'a> Parser<'a> {
|
||||
}
|
||||
|
||||
/// Parses `T` from this parser
|
||||
pub fn parse<T: Parsable<'a>>(&mut self) -> Result<T, anyhow::Error> {
|
||||
pub fn parse<T: Parsable<'a>>(&mut self) -> Result<T, AppError> {
|
||||
self.trim()?;
|
||||
T::parse_from(self)
|
||||
}
|
||||
@ -561,7 +561,7 @@ impl<'a> Parser<'a> {
|
||||
/// Tries to parses `T` from this parser.
|
||||
///
|
||||
/// On error, nothing is modified.
|
||||
pub fn try_parse<T: Parsable<'a>>(&mut self) -> Result<T, anyhow::Error> {
|
||||
pub fn try_parse<T: Parsable<'a>>(&mut self) -> Result<T, AppError> {
|
||||
let mut parser = self.clone();
|
||||
let value = parser.parse::<T>()?;
|
||||
|
||||
@ -581,7 +581,7 @@ macro decl_any_of($Name:ident, $($T:ident),* $(,)?) {
|
||||
$T: Parsable<'a>,
|
||||
)*
|
||||
{
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, anyhow::Error> {
|
||||
fn parse_from(parser: &mut Parser<'a>) -> Result<Self, AppError> {
|
||||
#![expect(non_snake_case, reason = "Macro generated")]
|
||||
|
||||
$(
|
||||
@ -599,7 +599,7 @@ macro decl_any_of($Name:ident, $($T:ident),* $(,)?) {
|
||||
let ${concat(at_, $T)} = self::at_most(${concat(parser_, $T)}.remaining(), 50);
|
||||
)*
|
||||
|
||||
anyhow::bail!(
|
||||
zutil_app_error::bail!(
|
||||
concat!(
|
||||
"Expected one of the following matches:",
|
||||
$( "\n{} at {:?}", ${ignore($T)} )*
|
||||
|
||||
139
src/build.rs
139
src/build.rs
@ -11,7 +11,7 @@ pub use self::{lock::BuildResult, reason::BuildReason};
|
||||
use {
|
||||
self::lock::{BuildLock, BuildLockDepGuard},
|
||||
crate::{
|
||||
error::ResultMultiple,
|
||||
error::{self, AppErrorData},
|
||||
expand,
|
||||
rules::{Command, DepItem, Expr, ExprTree, OutItem, Rule, Target},
|
||||
util::{self, ArcStr},
|
||||
@ -19,18 +19,19 @@ use {
|
||||
Expander,
|
||||
Rules,
|
||||
},
|
||||
anyhow::Context,
|
||||
dashmap::DashMap,
|
||||
futures::{stream::FuturesUnordered, StreamExt, TryStreamExt},
|
||||
indexmap::IndexMap,
|
||||
itertools::Itertools,
|
||||
std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
fmt,
|
||||
future::Future,
|
||||
sync::Arc,
|
||||
time::SystemTime,
|
||||
},
|
||||
tokio::{fs, process, sync::Semaphore, task},
|
||||
zutil_app_error::{app_error, AllErrs, Context},
|
||||
};
|
||||
|
||||
/// Event
|
||||
@ -122,13 +123,12 @@ impl Builder {
|
||||
// Then try to insert it
|
||||
if let Some(prev_rule_name) = rule_output_tree
|
||||
.insert(&output_file, rule_name.clone(), &[&rule.pats, &rules.pats])
|
||||
.context("Unable to add rule output to tree")
|
||||
.map_err(AppError::Other)?
|
||||
.context("Unable to add rule output to tree")?
|
||||
{
|
||||
return Err(AppError::Other(anyhow::anyhow!(
|
||||
return Err(app_error!(
|
||||
"Multiple rules match the same output file: {output_file}\n first rule: {prev_rule_name}\n \
|
||||
second rule: {rule_name}"
|
||||
)));
|
||||
));
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -203,9 +203,7 @@ impl Builder {
|
||||
.rules
|
||||
.rules
|
||||
.get(&*target_rule.name)
|
||||
.ok_or_else(|| AppError::UnknownRule {
|
||||
rule_name: (*target_rule.name).to_owned(),
|
||||
})?;
|
||||
.with_context(|| format!("Unknown rule {:?}", target_rule.name))?;
|
||||
let expand_visitor =
|
||||
expand::Visitor::new([&rule.aliases, &self.rules.aliases], [&rule.pats, &self.rules.pats], [
|
||||
&target_rule.pats,
|
||||
@ -213,7 +211,7 @@ impl Builder {
|
||||
let rule = self
|
||||
.expander
|
||||
.expand_rule(rule, &expand_visitor)
|
||||
.map_err(AppError::expand_rule(&*rule.name))?;
|
||||
.with_context(|| format!("Unable to expand rule {:?}", rule.name))?;
|
||||
|
||||
Ok(Some((rule, target_rule)))
|
||||
}
|
||||
@ -251,7 +249,7 @@ impl Builder {
|
||||
let target = self
|
||||
.expander
|
||||
.expand_target(target, &expand_visitor)
|
||||
.map_err(AppError::expand_target(target))?;
|
||||
.with_context(|| format!("Unable to expand target {target}"))?;
|
||||
|
||||
// Then build
|
||||
self.build(&target, ignore_missing, reason).await
|
||||
@ -278,7 +276,9 @@ impl Builder {
|
||||
match *target {
|
||||
Target::File { ref file, .. } => match fs::symlink_metadata(&**file).await {
|
||||
Ok(metadata) => {
|
||||
let build_time = metadata.modified().map_err(AppError::get_file_modified_time(&**file))?;
|
||||
let build_time = metadata
|
||||
.modified()
|
||||
.with_context(|| format!("Unable to get file modified time: {file:?}"))?;
|
||||
tracing::trace!(%target, ?build_time, "Found target file");
|
||||
return Ok((
|
||||
BuildResult {
|
||||
@ -301,10 +301,8 @@ impl Builder {
|
||||
));
|
||||
},
|
||||
Err(err) =>
|
||||
return Err(AppError::MissingFile {
|
||||
file_path: (**file).into(),
|
||||
source: err,
|
||||
}),
|
||||
do yeet AppError::new(&err)
|
||||
.context(format!("Missing file {file:?} and no rule to build it found")),
|
||||
},
|
||||
// Note: If `target_rule` returns `Err` if this was a rule, so we can never reach here
|
||||
Target::Rule { .. } => unreachable!(),
|
||||
@ -323,12 +321,9 @@ impl Builder {
|
||||
// First check if we're done with a dependency lock
|
||||
let build_guard = build_lock.lock_dep().await;
|
||||
if let Some(res) = build_guard.res() {
|
||||
return res
|
||||
.map(|res| (res, Some(build_guard)))
|
||||
.map_err(|()| AppError::BuildTarget {
|
||||
source: None,
|
||||
target: target.to_string(),
|
||||
});
|
||||
return res.map(|res| (res, Some(build_guard))).map_err(|()| {
|
||||
AppError::msg_with_data("Unable to build target {target}", AppErrorData { should_ignore: true })
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise, try to upgrade to a build lock
|
||||
@ -368,8 +363,7 @@ impl Builder {
|
||||
build_inner(this, target, rule, ignore_missing, reason)
|
||||
})
|
||||
.await
|
||||
.context("Unable to join task")
|
||||
.map_err(AppError::Other)?;
|
||||
.context("Unable to join task")?;
|
||||
|
||||
match res {
|
||||
Ok(res) => {
|
||||
@ -380,10 +374,10 @@ impl Builder {
|
||||
Err(err) => {
|
||||
// If we should, close the exec semaphore to ensure we exit as early as possible
|
||||
// Note: This check is racy, but it's fine to print this warning multiple times. We just don't want
|
||||
// to spam the user, since all further errors will likely caused by `AppError::ExecSemaphoreClosed`,
|
||||
// to spam the user, since all further errors will likely caused by the semaphore closing,
|
||||
// while the first few are the useful ones with the reason why the execution semaphore is being closed.
|
||||
if self.stop_builds_on_first_err && !self.exec_semaphore.is_closed() {
|
||||
tracing::debug!(err=%err.pretty(), "Stopping all future builds due to failure of target {target}");
|
||||
tracing::debug!(err=%error::pretty(&err), "Stopping all future builds due to failure of target {target}");
|
||||
self.exec_semaphore.close();
|
||||
}
|
||||
|
||||
@ -433,7 +427,7 @@ impl Builder {
|
||||
tracing::trace!(%target, ?rule.name, ?deps_last_build_time, ?rule_last_build_time, "Rebuilding target rule");
|
||||
self.rebuild_rule(rule)
|
||||
.await
|
||||
.map_err(AppError::build_rule(&*rule.name))?;
|
||||
.with_context(|| format!("Unable to build rule {:?}", rule.name))?;
|
||||
}
|
||||
|
||||
// Then get the build time
|
||||
@ -489,12 +483,12 @@ impl Builder {
|
||||
is_optional,
|
||||
exists: util::fs_try_exists_symlink(&**file)
|
||||
.await
|
||||
.map_err(AppError::check_file_exists(&**file))?,
|
||||
.with_context(|| format!("Unable to check if file exists {file:?}"))?,
|
||||
}),
|
||||
}
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<ResultMultiple<Vec<_>>>()
|
||||
.collect::<AllErrs<Vec<_>, _>>()
|
||||
.await?;
|
||||
|
||||
// And all output dependencies
|
||||
@ -518,14 +512,14 @@ impl Builder {
|
||||
is_optional: false,
|
||||
exists: util::fs_try_exists_symlink(&**file)
|
||||
.await
|
||||
.map_err(AppError::check_file_exists(&**file))?,
|
||||
.with_context(|| format!("Unable to check if file exists {file:?}"))?,
|
||||
})),
|
||||
_ => Ok(None),
|
||||
}
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.filter_map(move |res| async move { res.transpose() })
|
||||
.collect::<ResultMultiple<Vec<_>>>()
|
||||
.collect::<AllErrs<Vec<_>, _>>()
|
||||
.await?;
|
||||
|
||||
// Then build all dependencies, as well as any dependency files
|
||||
@ -561,7 +555,7 @@ impl Builder {
|
||||
reason.with_target(target.clone()),
|
||||
)
|
||||
.await
|
||||
.map_err(AppError::build_target(&dep_target))?;
|
||||
.with_context(|| format!("Unable to build target {dep_target}"))?;
|
||||
tracing::trace!(%target, ?rule.name, ?dep, ?res, "Built target rule dependency");
|
||||
|
||||
self.send_event(|| Event::TargetDepBuilt {
|
||||
@ -604,21 +598,21 @@ impl Builder {
|
||||
} => self
|
||||
.build_deps_file(target, file, rule, ignore_missing, reason)
|
||||
.await
|
||||
.map_err(AppError::build_deps_file(&**file))?,
|
||||
.with_context(|| format!("Unable to build dependencies file {file:?}"))?,
|
||||
Dep::File { .. } => vec![],
|
||||
};
|
||||
tracing::trace!(%target, ?rule.name, ?dep, ?dep_res, ?dep_deps, "Built target rule dependency dependencies");
|
||||
|
||||
let deps = util::chain!(dep_res, dep_deps.into_iter());
|
||||
|
||||
Ok(deps)
|
||||
Ok::<_, AppError>(deps)
|
||||
}
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.map_ok(|deps| deps.map(Ok))
|
||||
.map_ok(futures::stream::iter)
|
||||
.try_flatten()
|
||||
.collect::<ResultMultiple<Vec<_>>>()
|
||||
.collect::<AllErrs<Vec<_>, _>>()
|
||||
.await?;
|
||||
|
||||
Ok(deps)
|
||||
@ -643,13 +637,12 @@ impl Builder {
|
||||
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 => (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(),
|
||||
}),
|
||||
true => (output == &*rule.name).then_some(()).ok_or_else(|| {
|
||||
app_error!(
|
||||
"Dependencies file {deps_file:?} is missing the rule name {:?}, found {output:?}",
|
||||
rule.name
|
||||
)
|
||||
}),
|
||||
|
||||
// If there were any output, make sure the dependency file applies to one of them
|
||||
false => rule
|
||||
@ -659,10 +652,11 @@ impl Builder {
|
||||
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(),
|
||||
.ok_or_else(|| {
|
||||
app_error!(
|
||||
"Dependencies file {deps_file:?} is missing any output of {:?}, found {output:?}",
|
||||
rule.output.iter().map(OutItem::to_string).collect::<Vec<_>>()
|
||||
)
|
||||
}),
|
||||
};
|
||||
|
||||
@ -674,14 +668,10 @@ impl Builder {
|
||||
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<()>>()?;
|
||||
errs.into_iter().map(|(_, err)| Err(err)).collect::<AllErrs<(), _>>()?;
|
||||
|
||||
// If no errors existed, return an error for that
|
||||
return Err(AppError::DepFileEmpty {
|
||||
deps_file_path: deps_file.into(),
|
||||
});
|
||||
zutil_app_error::bail!("Dependencies file {deps_file:?} had no dependencies");
|
||||
},
|
||||
|
||||
// Otherwise, just log and remove all errors
|
||||
@ -689,7 +679,7 @@ impl Builder {
|
||||
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");
|
||||
tracing::warn!(target=%parent_target, ?rule.name, err=%error::pretty(&err), "Ignoring unknown output in dependency file");
|
||||
},
|
||||
}
|
||||
|
||||
@ -709,7 +699,7 @@ impl Builder {
|
||||
let (res, dep_guard) = self
|
||||
.build(&dep_target, ignore_missing, reason.with_target(parent_target.clone()))
|
||||
.await
|
||||
.map_err(AppError::build_target(&dep_target))?;
|
||||
.with_context(|| format!("Unable to build target {dep_target}"))?;
|
||||
|
||||
self.send_event(|| Event::TargetDepBuilt {
|
||||
target: parent_target.clone(),
|
||||
@ -721,7 +711,7 @@ impl Builder {
|
||||
}
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<ResultMultiple<_>>()
|
||||
.collect::<AllErrs<_, _>>()
|
||||
.await?;
|
||||
|
||||
Ok(deps_res)
|
||||
@ -738,7 +728,7 @@ impl Builder {
|
||||
// be in a "bad state", and since all the modification dates
|
||||
// match it wouldn't be rebuilt.
|
||||
let Ok(_permit) = self.exec_semaphore.acquire().await else {
|
||||
do yeet AppError::ExecSemaphoreClosed {};
|
||||
do yeet AppError::msg_with_data("Execution semaphore was closed", AppErrorData { should_ignore: true });
|
||||
};
|
||||
|
||||
for cmd in &rule.exec.cmds {
|
||||
@ -752,9 +742,10 @@ impl Builder {
|
||||
#[expect(unused_results, reason = "Due to the builder pattern of `Command`")]
|
||||
async fn exec_cmd(&self, rule: &Rule<ArcStr>, cmd: &Command<ArcStr>) -> Result<(), AppError> {
|
||||
// Get the program name
|
||||
let (program, args) = cmd.args.split_first().ok_or_else(|| AppError::RuleExecEmpty {
|
||||
rule_name: rule.name.to_string(),
|
||||
})?;
|
||||
let (program, args) = cmd
|
||||
.args
|
||||
.split_first()
|
||||
.ok_or_else(|| app_error!("Rule {:?} executable was empty", rule.name))?;
|
||||
|
||||
// Create the command and feed in all the arguments
|
||||
let mut os_cmd = process::Command::new(&**program);
|
||||
@ -773,9 +764,9 @@ impl Builder {
|
||||
os_cmd
|
||||
.status()
|
||||
.await
|
||||
.map_err(AppError::spawn_command(cmd))?
|
||||
.with_context(|| format!("Unable to spawn {}", self::cmd_to_string(cmd)))?
|
||||
.exit_ok()
|
||||
.map_err(AppError::command_failed(cmd))
|
||||
.with_context(|| format!("Command failed {}", self::cmd_to_string(cmd)))
|
||||
})
|
||||
.await?;
|
||||
tracing::trace!(target: "zbuild_exec", rule_name=?rule.name, ?program, ?args, ?duration, "Execution duration");
|
||||
@ -787,7 +778,9 @@ impl Builder {
|
||||
/// Parses a dependencies file
|
||||
async fn parse_deps_file(file: &str) -> Result<HashMap<ArcStr, Vec<ArcStr>>, AppError> {
|
||||
// Read it
|
||||
let mut contents = fs::read_to_string(file).await.map_err(AppError::read_file(file))?;
|
||||
let mut contents = fs::read_to_string(file)
|
||||
.await
|
||||
.with_context(|| format!("Unable to read file {file:?}"))?;
|
||||
|
||||
// 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
|
||||
@ -803,15 +796,15 @@ async fn parse_deps_file(file: &str) -> Result<HashMap<ArcStr, Vec<ArcStr>>, App
|
||||
})
|
||||
.map(|line| {
|
||||
// Parse it
|
||||
let (output, deps) = line.split_once(':').ok_or_else(|| AppError::DepFileMissingColon {
|
||||
deps_file_path: file.into(),
|
||||
})?;
|
||||
let (output, deps) = line
|
||||
.split_once(':')
|
||||
.ok_or_else(|| app_error!("Dependencies file {file:?} was missing a `:`"))?;
|
||||
let output = ArcStr::from(output.trim());
|
||||
let deps = deps.split_whitespace().map(ArcStr::from).collect();
|
||||
|
||||
Ok((output, deps))
|
||||
})
|
||||
.collect::<ResultMultiple<_>>()?;
|
||||
.collect::<AllErrs<_, _>>()?;
|
||||
|
||||
Ok(deps)
|
||||
}
|
||||
@ -832,15 +825,23 @@ async fn rule_last_build_time(rule: &Rule<ArcStr>) -> Result<Option<SystemTime>,
|
||||
};
|
||||
let metadata = fs::symlink_metadata(&**file)
|
||||
.await
|
||||
.map_err(AppError::read_file_metadata(&**file))?;
|
||||
let modified_time = metadata.modified().map_err(AppError::get_file_modified_time(&**file))?;
|
||||
.with_context(|| format!("Unable to read file metadata (not following symlinks) of {file:?}"))?;
|
||||
let modified_time = metadata
|
||||
.modified()
|
||||
.with_context(|| format!("Unable to get file modified time of {file:?}"))?;
|
||||
|
||||
Ok(modified_time)
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<ResultMultiple<Vec<_>>>()
|
||||
.collect::<AllErrs<Vec<_>, _>>()
|
||||
.await?
|
||||
.into_iter()
|
||||
.min();
|
||||
Ok(built_time)
|
||||
}
|
||||
|
||||
/// Helper function to format a `Command` for errors
|
||||
fn cmd_to_string<T: fmt::Display>(cmd: &Command<T>) -> String {
|
||||
let inner = cmd.args.iter().map(|arg| format!("\"{arg}\"")).join(" ");
|
||||
format!("[{inner}]")
|
||||
}
|
||||
|
||||
@ -3,7 +3,9 @@
|
||||
// Imports
|
||||
use {
|
||||
crate::{rules::Target, util::ArcStr, AppError},
|
||||
itertools::Itertools,
|
||||
std::{ops::Try, sync::Arc},
|
||||
zutil_app_error::app_error,
|
||||
};
|
||||
|
||||
/// Inner type for [`BuildReason`].
|
||||
@ -131,10 +133,10 @@ impl BuildReason {
|
||||
/// otherwise returns `Ok`.
|
||||
pub fn check_recursively(&self, target: &Target<ArcStr>) -> Result<(), AppError> {
|
||||
self.for_each(|parent_target| match target == parent_target {
|
||||
true => Err(AppError::FoundRecursiveRule {
|
||||
target: target.to_string(),
|
||||
parent_targets: self.collect_all().iter().map(Target::to_string).collect(),
|
||||
}),
|
||||
true => Err(app_error!(
|
||||
"Found recursive rule: {target} (Parent rules: {})",
|
||||
self.collect_all().iter().map(Target::to_string).join(", ")
|
||||
)),
|
||||
false => Ok(()),
|
||||
})
|
||||
}
|
||||
|
||||
896
src/error.rs
896
src/error.rs
@ -1,677 +1,26 @@
|
||||
//! Errors
|
||||
|
||||
// Imports
|
||||
use {
|
||||
crate::rules::{Command, Expr, ExprOp, Target},
|
||||
itertools::{Itertools, Position as ItertoolsPos},
|
||||
std::{
|
||||
convert::Infallible,
|
||||
env,
|
||||
error::Error as StdError,
|
||||
fmt,
|
||||
io,
|
||||
ops::{ControlFlow, FromResidual, Try},
|
||||
path::PathBuf,
|
||||
process::{self, ExitStatusError, Termination},
|
||||
string::FromUtf8Error,
|
||||
vec,
|
||||
},
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
ops::FromResidual,
|
||||
process::{self, Termination},
|
||||
};
|
||||
|
||||
/// Generates the error enum
|
||||
macro_rules! decl_error {
|
||||
(
|
||||
$(#[$meta:meta])*
|
||||
$Name:ident;
|
||||
$Multiple:ident($MultipleTy:ty);
|
||||
$Other:ident($OtherTy:ty);
|
||||
/// App error
|
||||
pub type AppError = zutil_app_error::AppError<AppErrorData>;
|
||||
|
||||
$(
|
||||
$( #[doc = $variant_doc:expr] )*
|
||||
$(
|
||||
#[from_fn(
|
||||
// Function definition
|
||||
$(#[$variant_fn_meta:meta])*
|
||||
fn $variant_fn:ident
|
||||
|
||||
// Generics
|
||||
$( <
|
||||
$( $VariantLifetimes:lifetime, )*
|
||||
$( $VariantGenerics:ident $(: $VariantBound:path )? ),* $(,)?
|
||||
> )?
|
||||
|
||||
// Error
|
||||
(
|
||||
$variant_fn_err:ident: $VariantFnErr:ty $( => $variant_fn_err_expr:expr )?
|
||||
)
|
||||
|
||||
// Args
|
||||
(
|
||||
$(
|
||||
$variant_fn_arg:ident: $VariantFnArg:ty $( => $variant_fn_arg_expr:expr )?
|
||||
),*
|
||||
$(,)?
|
||||
)
|
||||
|
||||
// Return type lifetimes
|
||||
$(
|
||||
+ $VariantFnLifetime:lifetime
|
||||
)?
|
||||
)]
|
||||
)?
|
||||
#[source($variant_source:expr)]
|
||||
#[fmt($($variant_fmt:tt)*)]
|
||||
$Variant:ident {
|
||||
$(
|
||||
$( #[$variant_field_meta:meta] )*
|
||||
$variant_field:ident: $VariantField:ty
|
||||
),*
|
||||
$(,)?
|
||||
},
|
||||
)*
|
||||
) => {
|
||||
$( #[ $meta ] )*
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum $Name {
|
||||
/// Multiple
|
||||
$Multiple($MultipleTy),
|
||||
|
||||
/// Other
|
||||
// TODO: Removes usages of this, it's for quick prototyping
|
||||
$Other($OtherTy),
|
||||
|
||||
$(
|
||||
$( #[doc = $variant_doc] )*
|
||||
$Variant {
|
||||
$(
|
||||
$( #[$variant_field_meta] )*
|
||||
$variant_field: $VariantField,
|
||||
)*
|
||||
},
|
||||
)*
|
||||
}
|
||||
|
||||
impl $Name {
|
||||
$(
|
||||
$(
|
||||
#[doc = concat!("Returns a function to create a [`Self::", stringify!($Variant) ,"`] error from it's inner error.")]
|
||||
$( #[$variant_fn_meta] )*
|
||||
pub fn $variant_fn
|
||||
|
||||
// Generics
|
||||
$( <
|
||||
$( $VariantLifetimes, )*
|
||||
$( $VariantGenerics $(: $VariantBound )?, )*
|
||||
> )?
|
||||
|
||||
// Arguments
|
||||
( $(
|
||||
$variant_fn_arg: $VariantFnArg,
|
||||
)* )
|
||||
|
||||
// Return type
|
||||
-> impl FnOnce($VariantFnErr) -> Self $( + $VariantFnLifetime )?
|
||||
|
||||
{
|
||||
move |$variant_fn_err| Self::$Variant {
|
||||
$variant_fn_err $(: $variant_fn_err_expr )?,
|
||||
$(
|
||||
$variant_fn_arg $(: $variant_fn_arg_expr )?,
|
||||
)*
|
||||
}
|
||||
}
|
||||
)?
|
||||
)*
|
||||
|
||||
/// Returns an object that can be used for a pretty display of this error
|
||||
#[must_use] pub fn pretty(&self) -> PrettyDisplay<'_> {
|
||||
PrettyDisplay::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for AppError {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match self {
|
||||
// Note: We don't return any of the errors here, so that we can format
|
||||
// it properly without duplicating errors.
|
||||
Self::$Multiple(_) => None,
|
||||
Self::$Other(source) => AsRef::<dyn StdError>::as_ref(source).source(),
|
||||
$(
|
||||
#[expect(clippy::allow_attributes, reason = "Auto-generated code")]
|
||||
#[allow(unused_variables, reason = "Auto-generated code")]
|
||||
Self::$Variant { $( $variant_field ),* } => $variant_source,
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AppError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Display the main message
|
||||
match self {
|
||||
Self::$Multiple(errs) => write!(f, "Multiple errors ({})", errs.len()),
|
||||
Self::$Other(source) => source.fmt(f),
|
||||
$(
|
||||
#[expect(clippy::allow_attributes, reason = "Auto-generated code")]
|
||||
#[allow(unused_variables, reason = "Auto-generated code")]
|
||||
Self::$Variant { $( $variant_field ),* } => write!(f, $($variant_fmt)*),
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// App error data
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct AppErrorData {
|
||||
/// Whether this error should be ignored when printing
|
||||
pub should_ignore: bool,
|
||||
}
|
||||
|
||||
|
||||
decl_error! {
|
||||
/// Test
|
||||
AppError;
|
||||
Multiple(Vec<Self>);
|
||||
Other(anyhow::Error);
|
||||
|
||||
/// Get current directory
|
||||
#[from_fn( fn get_current_dir(source: io::Error)() )]
|
||||
#[source(Some(source))]
|
||||
#[fmt("Unable to get current directory")]
|
||||
GetCurrentDir {
|
||||
/// Underlying error
|
||||
source: io::Error
|
||||
},
|
||||
|
||||
/// Set current directory
|
||||
#[from_fn(
|
||||
fn set_current_dir<P: Into<PathBuf>>(source: io::Error)(
|
||||
dir: P => dir.into()
|
||||
)
|
||||
)]
|
||||
#[source(Some(source))]
|
||||
#[fmt("Unable to set current directory to {dir:?}")]
|
||||
SetCurrentDir {
|
||||
/// Underlying error
|
||||
source: io::Error,
|
||||
|
||||
/// Path we tried to set as current directory
|
||||
dir: PathBuf
|
||||
},
|
||||
|
||||
/// Read file
|
||||
#[from_fn(
|
||||
fn read_file<P: Into<PathBuf>>(source: io::Error)(
|
||||
file_path: P => file_path.into()
|
||||
)
|
||||
)]
|
||||
#[source(Some(source))]
|
||||
#[fmt("Unable to read file {file_path:?}")]
|
||||
ReadFile {
|
||||
/// Underlying error
|
||||
source: io::Error,
|
||||
|
||||
/// File we failed to read
|
||||
file_path: PathBuf,
|
||||
},
|
||||
|
||||
/// Read file metadata
|
||||
#[from_fn(
|
||||
fn read_file_metadata<P: Into<PathBuf>>(source: io::Error)(
|
||||
file_path: P => file_path.into()
|
||||
)
|
||||
)]
|
||||
#[source(Some(source))]
|
||||
#[fmt("Unable to read file metadata (not following symlinks) {file_path:?}")]
|
||||
ReadFileMetadata {
|
||||
/// Underlying error
|
||||
source: io::Error,
|
||||
|
||||
/// File we failed to read metadata of
|
||||
file_path: PathBuf,
|
||||
},
|
||||
|
||||
/// Get file modified time
|
||||
#[from_fn(
|
||||
fn get_file_modified_time<P: Into<PathBuf>>(source: io::Error)(
|
||||
file_path: P => file_path.into()
|
||||
)
|
||||
)]
|
||||
#[source(Some(source))]
|
||||
#[fmt("Unable to get file modified time {file_path:?}")]
|
||||
GetFileModifiedTime {
|
||||
/// Underlying error
|
||||
source: io::Error,
|
||||
|
||||
/// File we failed to get the modified time of
|
||||
file_path: PathBuf,
|
||||
},
|
||||
|
||||
/// Check if file exists
|
||||
#[from_fn(
|
||||
fn check_file_exists<P: Into<PathBuf>>(source: io::Error)(
|
||||
file_path: P => file_path.into()
|
||||
)
|
||||
)]
|
||||
#[source(Some(source))]
|
||||
#[fmt("Unable to check if file exists {file_path:?}")]
|
||||
CheckFileExists {
|
||||
/// Underlying error
|
||||
source: io::Error,
|
||||
|
||||
/// File we failed to check
|
||||
file_path: PathBuf,
|
||||
},
|
||||
|
||||
/// Missing file
|
||||
#[from_fn(
|
||||
// TODO: For some reason, rustc thinks the following lint is
|
||||
// unfulfilled, check why.
|
||||
//#[expect(dead_code, reason = "Not used yet")]
|
||||
fn missing_file<P: Into<PathBuf>>(source: io::Error)(
|
||||
file_path: P => file_path.into()
|
||||
)
|
||||
)]
|
||||
#[source(Some(source))]
|
||||
#[fmt("Missing file {file_path:?} and no rule to build it found")]
|
||||
MissingFile {
|
||||
/// Underlying error
|
||||
source: io::Error,
|
||||
|
||||
/// File that is missing
|
||||
file_path: PathBuf,
|
||||
},
|
||||
|
||||
/// Spawn command
|
||||
#[from_fn(
|
||||
fn spawn_command<T: fmt::Display>(source: io::Error)(
|
||||
cmd: &Command<T> => self::cmd_to_string(cmd)
|
||||
) + '_
|
||||
)]
|
||||
#[source(Some(source))]
|
||||
#[fmt("Unable to spawn {cmd}")]
|
||||
SpawnCommand {
|
||||
/// Underlying error
|
||||
source: io::Error,
|
||||
|
||||
/// Command formatted
|
||||
cmd: String,
|
||||
},
|
||||
|
||||
/// Command failed
|
||||
#[from_fn(
|
||||
fn command_failed<T: fmt::Display>(source: ExitStatusError)(
|
||||
cmd: &Command<T> => self::cmd_to_string(cmd)
|
||||
) + '_
|
||||
)]
|
||||
#[source(Some(source))]
|
||||
#[fmt("Command failed {cmd}")]
|
||||
CommandFailed {
|
||||
/// Underlying error
|
||||
source: ExitStatusError,
|
||||
|
||||
/// Command formatted
|
||||
cmd: String,
|
||||
},
|
||||
|
||||
/// Command output was non-utf8
|
||||
#[from_fn(
|
||||
fn command_output_non_utf8<T: fmt::Display>(source: FromUtf8Error)(
|
||||
cmd: &Command<T> => self::cmd_to_string(cmd)
|
||||
) + '_
|
||||
)]
|
||||
#[source(Some(source))]
|
||||
#[fmt("Command output was non-utf8 {cmd}")]
|
||||
CommandOutputNonUtf8 {
|
||||
/// Underlying error
|
||||
source: FromUtf8Error,
|
||||
|
||||
/// Command formatted
|
||||
cmd: String,
|
||||
},
|
||||
|
||||
/// Get default jobs
|
||||
#[from_fn( fn get_default_jobs(source: io::Error)() )]
|
||||
#[source(Some(source))]
|
||||
#[fmt("Unable to query system for available parallelism for default number of jobs")]
|
||||
GetDefaultJobs {
|
||||
/// Underlying error
|
||||
source: io::Error
|
||||
},
|
||||
|
||||
/// Zbuild not found
|
||||
#[source(None)]
|
||||
#[fmt("No `zbuild.zb` file found in current or parent directories.\nYou can use `--path {{zbuild-path}}` in order to specify the manifest's path")]
|
||||
ZBuildNotFound {},
|
||||
|
||||
/// Path had no parent
|
||||
#[source(None)]
|
||||
#[fmt("Path had no parent directory {path:?}")]
|
||||
PathParent {
|
||||
/// Path that had no parent
|
||||
path: PathBuf,
|
||||
},
|
||||
|
||||
/// Build target
|
||||
#[from_fn(
|
||||
fn build_target<'target, T: fmt::Display>(source: Self => Some(Box::new(source)))(
|
||||
target: &'target Target<T> => target.to_string()
|
||||
) + 'target
|
||||
)]
|
||||
#[source(source.as_deref().map(|err: &AppError| <&dyn StdError>::from(err)))]
|
||||
#[fmt("Unable to build target {target}")]
|
||||
BuildTarget {
|
||||
/// Underlying error
|
||||
source: Option<Box<Self>>,
|
||||
|
||||
/// Formatted target
|
||||
target: String,
|
||||
},
|
||||
|
||||
/// Build rule
|
||||
#[from_fn(
|
||||
fn build_rule<S: Into<String>>(source: Self => Box::new(source))(
|
||||
rule_name: S => rule_name.into()
|
||||
)
|
||||
)]
|
||||
#[source(Some(&**source))]
|
||||
#[fmt("Unable to build rule {rule_name}")]
|
||||
BuildRule {
|
||||
/// Underlying error
|
||||
source: Box<Self>,
|
||||
|
||||
/// Rule name
|
||||
rule_name: String,
|
||||
},
|
||||
|
||||
/// Build dependencies file
|
||||
#[from_fn(
|
||||
fn build_deps_file<P: Into<PathBuf>>(source: Self => Box::new(source))(
|
||||
deps_file: P => deps_file.into()
|
||||
)
|
||||
)]
|
||||
#[source(Some(&**source))]
|
||||
#[fmt("Unable to build dependencies file {deps_file:?}")]
|
||||
BuildDepFile {
|
||||
/// Underlying error
|
||||
source: Box<Self>,
|
||||
|
||||
/// Dependencies file
|
||||
deps_file: PathBuf,
|
||||
},
|
||||
|
||||
/// Expand rule
|
||||
#[from_fn(
|
||||
fn expand_rule<T: Into<String>>(source: Self => Box::new(source))(
|
||||
rule_name: T => rule_name.into()
|
||||
)
|
||||
)]
|
||||
#[source(Some(&**source))]
|
||||
#[fmt("Unable to expand rule {rule_name}")]
|
||||
ExpandRule {
|
||||
/// Underlying error
|
||||
source: Box<Self>,
|
||||
|
||||
/// Rule name
|
||||
rule_name: String,
|
||||
},
|
||||
|
||||
/// Expand target
|
||||
#[from_fn(
|
||||
fn expand_target<'target, T: fmt::Display>(source: Self => Box::new(source))(
|
||||
target: &'target Target<T> => target.to_string()
|
||||
) + 'target
|
||||
)]
|
||||
#[source(Some(&**source))]
|
||||
#[fmt("Unable to expand target {target}")]
|
||||
ExpandTarget {
|
||||
/// Underlying error
|
||||
source: Box<Self>,
|
||||
|
||||
/// Formatted target
|
||||
target: String,
|
||||
},
|
||||
|
||||
/// Expand expression
|
||||
#[from_fn(
|
||||
fn expand_expr<'expr,>(source: Self => Box::new(source))(
|
||||
expr: &'expr Expr => expr.to_string()
|
||||
) + 'expr
|
||||
)]
|
||||
#[source(Some(&**source))]
|
||||
#[fmt("Unable to expand expression {expr}")]
|
||||
ExpandExpr {
|
||||
/// Underlying error
|
||||
source: Box<Self>,
|
||||
|
||||
/// Formatted expression
|
||||
expr: String,
|
||||
},
|
||||
|
||||
/// Unknown rule
|
||||
#[source(None)]
|
||||
#[fmt("Unknown rule {rule_name:?}")]
|
||||
UnknownRule {
|
||||
/// Rule name
|
||||
rule_name: String,
|
||||
},
|
||||
|
||||
/// Unknown expression
|
||||
#[source(None)]
|
||||
#[fmt("Unknown expression {expr_ident:?}")]
|
||||
UnknownExpr {
|
||||
/// Expression identifier
|
||||
expr_ident: String,
|
||||
},
|
||||
|
||||
/// Unknown pattern
|
||||
#[source(None)]
|
||||
#[fmt("Unknown pattern {pattern_name:?}")]
|
||||
UnknownPattern {
|
||||
/// Pattern name
|
||||
pattern_name: String,
|
||||
},
|
||||
|
||||
/// Unresolved aliases or patterns
|
||||
#[source(None)]
|
||||
#[fmt("Expression had unresolved aliases or patterns: {expr} ({expr_cmpts:?})")]
|
||||
UnresolvedAliasesOrPats {
|
||||
/// Formatted expression
|
||||
expr: String,
|
||||
|
||||
/// Components
|
||||
expr_cmpts: Vec<String>,
|
||||
},
|
||||
|
||||
/// Match expression had 2 or moore patterns
|
||||
#[source(None)]
|
||||
#[fmt("Match expression had 2 or more patterns: {expr} ({expr_cmpts:?})")]
|
||||
MatchExprTooManyPats {
|
||||
/// Formatted expression
|
||||
expr: String,
|
||||
|
||||
/// Components
|
||||
expr_cmpts: Vec<String>,
|
||||
},
|
||||
|
||||
/// Expr operation
|
||||
#[from_fn( fn expr_op(source: Self => Box::new(source))(op: ExprOp) )]
|
||||
#[source(Some(&**source))]
|
||||
#[fmt("Unable to apply expr operation `{op}`")]
|
||||
ExprOp {
|
||||
/// Underlying error
|
||||
source: Box<Self>,
|
||||
|
||||
/// Operation
|
||||
op: ExprOp,
|
||||
},
|
||||
|
||||
/// Dependencies file missing `:`
|
||||
#[source(None)]
|
||||
#[fmt("Dependencies file {deps_file_path:?} was missing a `:`")]
|
||||
DepFileMissingColon {
|
||||
/// Dep file path
|
||||
deps_file_path: PathBuf,
|
||||
},
|
||||
|
||||
/// Dependencies file missing rule name
|
||||
#[source(None)]
|
||||
#[fmt("Dependencies file {deps_file_path:?} is missing the rule name {rule_name:?}, found {dep_output:?}")]
|
||||
DepFileMissingRuleName {
|
||||
/// Dep file path
|
||||
deps_file_path: PathBuf,
|
||||
|
||||
/// Rule name
|
||||
rule_name: String,
|
||||
|
||||
/// Dependencies file output
|
||||
dep_output: String,
|
||||
},
|
||||
|
||||
/// Dependencies file missing rule name
|
||||
#[source(None)]
|
||||
#[fmt("Dependencies file {deps_file_path:?} is missing any output of {rule_outputs:?}, found {dep_output:?}")]
|
||||
DepFileMissingOutputs {
|
||||
/// Dep file path
|
||||
deps_file_path: PathBuf,
|
||||
|
||||
/// Rule outputs
|
||||
rule_outputs: Vec<String>,
|
||||
|
||||
/// Dependency
|
||||
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")]
|
||||
RuleExecEmpty {
|
||||
/// Rule name
|
||||
rule_name: String,
|
||||
},
|
||||
|
||||
/// Exit due to failed builds
|
||||
#[source(None)]
|
||||
#[fmt("Exiting with non-0 due to failed builds")]
|
||||
ExitDueToFailedBuilds {},
|
||||
|
||||
/// Execution semaphore was closed
|
||||
#[source(None)]
|
||||
#[fmt("Execution semaphore was closed")]
|
||||
ExecSemaphoreClosed {},
|
||||
|
||||
/// Found recursive rule
|
||||
#[source(None)]
|
||||
#[fmt("Found recursive rule: {target} (Parent rules: {})", parent_targets.iter().join(", "))]
|
||||
FoundRecursiveRule {
|
||||
/// Formatted recursive target
|
||||
target: String,
|
||||
|
||||
/// Formatted parent targets
|
||||
parent_targets: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Helper function to format a `Command` for errors
|
||||
fn cmd_to_string<T: fmt::Display>(cmd: &Command<T>) -> String {
|
||||
let inner = cmd.args.iter().map(|arg| format!("\"{arg}\"")).join(" ");
|
||||
format!("[{inner}]")
|
||||
}
|
||||
|
||||
/// Helper type to collect a `IntoIter<Item = Result<T, AppError>>`
|
||||
/// into a `Result<C, AppError::Multiple>`.
|
||||
#[derive(Debug)]
|
||||
pub enum ResultMultiple<C> {
|
||||
Ok(C),
|
||||
Err(Vec<AppError>),
|
||||
}
|
||||
|
||||
impl<C> Default for ResultMultiple<C>
|
||||
where
|
||||
C: Default,
|
||||
{
|
||||
#[expect(clippy::derivable_impls, reason = "We want to be explicit")]
|
||||
impl Default for AppErrorData {
|
||||
fn default() -> Self {
|
||||
Self::Ok(C::default())
|
||||
}
|
||||
}
|
||||
impl<C, U> Extend<Result<U, AppError>> for ResultMultiple<C>
|
||||
where
|
||||
C: Extend<U>,
|
||||
{
|
||||
fn extend<T: IntoIterator<Item = Result<U, AppError>>>(&mut self, iter: T) {
|
||||
// TODO: Do this more efficiently?
|
||||
for res in iter {
|
||||
match (&mut *self, res) {
|
||||
// If we have a collection, and we get an item, extend it
|
||||
(Self::Ok(collection), Ok(item)) => collection.extend_one(item),
|
||||
// If we have a collection, but find an error, switch to errors
|
||||
(Self::Ok(_), Err(err)) => *self = Self::Err(vec![err]),
|
||||
// If we have errors and got an item, ignore it
|
||||
(Self::Err(_), Ok(_)) => (),
|
||||
// If we have errors and got an error, extend it
|
||||
(Self::Err(errs), Err(err)) => errs.push(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, T> FromIterator<Result<T, AppError>> for ResultMultiple<C>
|
||||
where
|
||||
C: Default + Extend<T>,
|
||||
{
|
||||
fn from_iter<I>(iter: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = Result<T, AppError>>,
|
||||
{
|
||||
// TODO: If we get any errors, don't allocate memory for the rest of the values?
|
||||
let (values, errs) = iter.into_iter().partition_result::<C, Vec<_>, _, _>();
|
||||
match errs.is_empty() {
|
||||
true => Self::Ok(values),
|
||||
false => Self::Err(errs),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ResultMultipleResidue(Vec<AppError>);
|
||||
|
||||
impl<C> Try for ResultMultiple<C> {
|
||||
type Output = C;
|
||||
type Residual = ResultMultipleResidue;
|
||||
|
||||
fn from_output(output: Self::Output) -> Self {
|
||||
Self::Ok(output)
|
||||
}
|
||||
|
||||
fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
|
||||
match self {
|
||||
Self::Ok(values) => ControlFlow::Continue(values),
|
||||
Self::Err(errs) => ControlFlow::Break(ResultMultipleResidue(errs)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FromResidual<ResultMultipleResidue> for ResultMultiple<T> {
|
||||
fn from_residual(residual: ResultMultipleResidue) -> Self {
|
||||
Self::Err(residual.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FromResidual<ResultMultipleResidue> for Result<T, AppError> {
|
||||
fn from_residual(residual: ResultMultipleResidue) -> Self {
|
||||
let err = match <[_; 1]>::try_from(residual.0) {
|
||||
Ok([err]) => err,
|
||||
Err(errs) => {
|
||||
assert!(!errs.is_empty(), "`ResultMultipleResidue` should hold at least 1 error");
|
||||
AppError::Multiple(errs)
|
||||
},
|
||||
};
|
||||
|
||||
Err(err)
|
||||
Self { should_ignore: false }
|
||||
}
|
||||
}
|
||||
|
||||
@ -687,7 +36,7 @@ impl Termination for ExitResult {
|
||||
match self {
|
||||
Self::Ok => process::ExitCode::SUCCESS,
|
||||
Self::Err(err) => {
|
||||
eprintln!("Error: {}", err.pretty());
|
||||
eprintln!("Error: {}", self::pretty(&err));
|
||||
process::ExitCode::FAILURE
|
||||
},
|
||||
}
|
||||
@ -703,216 +52,7 @@ impl FromResidual<Result<Infallible, AppError>> for ExitResult {
|
||||
}
|
||||
}
|
||||
|
||||
// Note: This impl is provided for tests, so they can be quick and dirty with
|
||||
// errors.
|
||||
impl FromResidual<Result<Infallible, anyhow::Error>> for ExitResult {
|
||||
fn from_residual(residual: Result<Infallible, anyhow::Error>) -> Self {
|
||||
Self::from_residual(residual.map_err(AppError::Other))
|
||||
}
|
||||
}
|
||||
|
||||
/// Pretty display for [`AppError`]
|
||||
#[derive(Debug)]
|
||||
pub struct PrettyDisplay<'a> {
|
||||
/// Root error
|
||||
root: &'a AppError,
|
||||
|
||||
/// Whether we should show irrelevant errors.
|
||||
show_irrelevant: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Copy, Debug)]
|
||||
enum Column {
|
||||
Line,
|
||||
Empty,
|
||||
}
|
||||
|
||||
impl Column {
|
||||
/// Returns the string for this column
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Line => "│ ",
|
||||
Self::Empty => " ",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PrettyDisplay<'a> {
|
||||
/// Creates a new pretty display
|
||||
pub(crate) fn new(root: &'a AppError) -> Self {
|
||||
// Get whether to show irrelevant errors from the environment
|
||||
let var = env::var("ZBUILD_SHOW_IRRELEVANT_ERRS");
|
||||
let show_irrelevant = match var {
|
||||
Ok(mut s) => {
|
||||
s.make_ascii_lowercase();
|
||||
matches!(s.as_str(), "1" | "y" | "yes" | "true")
|
||||
},
|
||||
Err(_) => false,
|
||||
};
|
||||
|
||||
Self { root, show_irrelevant }
|
||||
}
|
||||
|
||||
/// Sets if we should show irrelevant errors during formatting
|
||||
pub fn with_show_irrelevant(&mut self, show_irrelevant: bool) -> &mut Self {
|
||||
self.show_irrelevant = show_irrelevant;
|
||||
self
|
||||
}
|
||||
|
||||
/// Formats a single error
|
||||
// Note: Always prints, even if irrelevant. Only `fmt_multiple` actually filters
|
||||
// any of it's entries for being irrelevant.
|
||||
fn fmt_single(
|
||||
&self,
|
||||
f: &mut fmt::Formatter<'_>,
|
||||
err: &AppError,
|
||||
columns: &mut Vec<Column>,
|
||||
total_ignored_errs: &mut usize,
|
||||
) -> fmt::Result {
|
||||
// If it's multiple, display it as multiple
|
||||
if let AppError::Multiple(errs) = err {
|
||||
return self.fmt_multiple(f, errs, columns, total_ignored_errs);
|
||||
}
|
||||
|
||||
// Else write the top-level error
|
||||
write!(f, "{err}")?;
|
||||
|
||||
// Then, if there's a cause, write the rest
|
||||
if let Some(mut cur_source) = err.source() {
|
||||
let starting_columns = columns.len();
|
||||
loop {
|
||||
// Print the pre-amble
|
||||
f.pad("\n")?;
|
||||
for c in &*columns {
|
||||
f.pad(c.as_str())?;
|
||||
}
|
||||
f.pad("└─")?;
|
||||
columns.push(Column::Empty);
|
||||
|
||||
// Then check if we got to a multiple.
|
||||
match cur_source.downcast_ref::<AppError>() {
|
||||
Some(AppError::Multiple(errs)) => {
|
||||
self.fmt_multiple(f, errs, columns, total_ignored_errs)?;
|
||||
break;
|
||||
},
|
||||
_ => write!(f, "{cur_source}",)?,
|
||||
}
|
||||
|
||||
// And descend
|
||||
cur_source = match cur_source.source() {
|
||||
Some(source) => source,
|
||||
_ => break,
|
||||
};
|
||||
}
|
||||
let _: vec::Drain<'_, _> = columns.drain(starting_columns..);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Formats multiple errors
|
||||
fn fmt_multiple(
|
||||
&self,
|
||||
f: &mut fmt::Formatter<'_>,
|
||||
errs: &[AppError],
|
||||
columns: &mut Vec<Column>,
|
||||
total_ignored_errs: &mut usize,
|
||||
) -> fmt::Result {
|
||||
// Write the top-level error
|
||||
write!(f, "Multiple errors:")?;
|
||||
|
||||
// For each error, write it
|
||||
let mut ignored_errs = 0;
|
||||
for (pos, err) in errs.iter().with_position() {
|
||||
// If this error is irrelevant, continue
|
||||
if !self.show_irrelevant && !self::err_contains_relevant(err) {
|
||||
ignored_errs += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
f.pad("\n")?;
|
||||
for c in &*columns {
|
||||
f.pad(c.as_str())?;
|
||||
}
|
||||
|
||||
// Note: We'll only print `└─` if we have no ignored errors, since if we do,
|
||||
// we need that to print the final line showcasing how many we ignored
|
||||
match ignored_errs == 0 && matches!(pos, ItertoolsPos::Last | ItertoolsPos::Only) {
|
||||
true => {
|
||||
f.pad("└─")?;
|
||||
columns.push(Column::Empty);
|
||||
},
|
||||
false => {
|
||||
f.pad("├─")?;
|
||||
columns.push(Column::Line);
|
||||
},
|
||||
}
|
||||
|
||||
self.fmt_single(f, err, columns, total_ignored_errs)?;
|
||||
let _: Option<_> = columns.pop();
|
||||
}
|
||||
|
||||
if ignored_errs != 0 {
|
||||
*total_ignored_errs += ignored_errs;
|
||||
|
||||
f.pad("\n")?;
|
||||
for c in &*columns {
|
||||
f.pad(c.as_str())?;
|
||||
}
|
||||
f.pad("└─")?;
|
||||
write!(f, "({ignored_errs} irrelevant errors)")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PrettyDisplay<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut columns = vec![];
|
||||
let mut total_ignored_errs = 0;
|
||||
self.fmt_single(f, self.root, &mut columns, &mut total_ignored_errs)?;
|
||||
assert_eq!(columns.len(), 0, "There should be no columns after formatting");
|
||||
|
||||
if total_ignored_errs != 0 {
|
||||
f.pad("\n")?;
|
||||
write!(
|
||||
f,
|
||||
"Note: {total_ignored_errs} irrelevant errors were hidden, set `ZBUILD_SHOW_IRRELEVANT_ERRS=1` to \
|
||||
show them"
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns if this error contains any "relevant" errors.
|
||||
///
|
||||
/// In our case, the following cases are considered *irrelevant*:
|
||||
/// - Is an [`AppError::BuildTarget`] with no source.
|
||||
/// - Is an [`AppError::ExecSemaphoreClosed`].
|
||||
fn err_contains_relevant(err: &AppError) -> bool {
|
||||
// If we're multiple errors, return if any of them are relevant
|
||||
if let AppError::Multiple(errs) = err {
|
||||
return errs.iter().any(self::err_contains_relevant);
|
||||
}
|
||||
|
||||
// Else if the error itself is irrelevant, return
|
||||
if matches!(
|
||||
err,
|
||||
AppError::BuildTarget { source: None, .. } | AppError::ExecSemaphoreClosed {}
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Else check the inner error, if any
|
||||
if let Some(source) = err.source() &&
|
||||
let Some(err) = source.downcast_ref::<AppError>()
|
||||
{
|
||||
return self::err_contains_relevant(err);
|
||||
}
|
||||
|
||||
// Else, we're relevant
|
||||
true
|
||||
/// Function to setup pretty printing
|
||||
pub fn pretty(err: &AppError) -> zutil_app_error::PrettyDisplay<'_, AppErrorData> {
|
||||
err.pretty().with_ignore_err(|_, data| data.should_ignore)
|
||||
}
|
||||
|
||||
@ -3,13 +3,14 @@
|
||||
// Imports
|
||||
use {
|
||||
crate::{
|
||||
error::{AppError, ResultMultiple},
|
||||
rules::{Command, DepItem, Exec, Expr, ExprCmpt, ExprOp, OutItem, Pattern, Rule, Target},
|
||||
util::ArcStr,
|
||||
AppError,
|
||||
},
|
||||
indexmap::IndexMap,
|
||||
smallvec::SmallVec,
|
||||
std::{collections::BTreeMap, marker::PhantomData, mem, path::PathBuf, sync::Arc},
|
||||
zutil_app_error::{app_error, AllErrs, Context},
|
||||
};
|
||||
|
||||
/// Expander
|
||||
@ -59,9 +60,9 @@ impl Expander {
|
||||
let value = ops.iter().try_fold(value, |mut value, &op| {
|
||||
value
|
||||
.with_mut(|s| self.expand_expr_op(op, s))
|
||||
.map_err(AppError::expr_op(op))?;
|
||||
.with_context(|| format!("Unable to apply expression operator `{op}`"))?;
|
||||
|
||||
Ok(value)
|
||||
Ok::<_, AppError>(value)
|
||||
})?;
|
||||
|
||||
expr.push_str(&value);
|
||||
@ -70,14 +71,11 @@ impl Expander {
|
||||
|
||||
// Else keep on Keep and error on Error
|
||||
FlowControl::Keep => expr.push(cmpt),
|
||||
FlowControl::Error =>
|
||||
return Err(AppError::UnknownExpr {
|
||||
expr_ident: name.to_string(),
|
||||
}),
|
||||
FlowControl::Error => zutil_app_error::bail!("Unknown expression {name:?}"),
|
||||
},
|
||||
};
|
||||
|
||||
Ok(expr)
|
||||
Ok::<_, AppError>(expr)
|
||||
})?;
|
||||
|
||||
// Then try to parse from the expression
|
||||
@ -90,9 +88,7 @@ impl Expander {
|
||||
ExprOp::DirName => {
|
||||
// Get the path and try to pop the last segment
|
||||
let mut path = PathBuf::from(mem::take(value));
|
||||
if !path.pop() {
|
||||
return Err(AppError::PathParent { path });
|
||||
}
|
||||
zutil_app_error::ensure!(path.pop(), "Path had no parent directory {path:?}");
|
||||
|
||||
// Then convert it back to a string
|
||||
// Note: This should technically never fail, since the path was originally
|
||||
@ -116,7 +112,7 @@ impl Expander {
|
||||
.aliases
|
||||
.iter()
|
||||
.map(|(name, expr)| Ok((name.clone(), self.expand_expr(expr, visitor)?)))
|
||||
.collect::<ResultMultiple<_>>()?;
|
||||
.collect::<AllErrs<_, _>>()?;
|
||||
|
||||
let output = rule
|
||||
.output
|
||||
@ -127,7 +123,7 @@ impl Expander {
|
||||
is_deps_file,
|
||||
}),
|
||||
})
|
||||
.collect::<ResultMultiple<_>>()?;
|
||||
.collect::<AllErrs<_, _>>()?;
|
||||
|
||||
let deps = rule
|
||||
.deps
|
||||
@ -145,7 +141,7 @@ impl Expander {
|
||||
is_deps_file,
|
||||
}),
|
||||
})
|
||||
.collect::<ResultMultiple<_>>()?;
|
||||
.collect::<AllErrs<_, _>>()?;
|
||||
|
||||
let exec = Exec {
|
||||
cmds: rule
|
||||
@ -153,7 +149,7 @@ impl Expander {
|
||||
.cmds
|
||||
.iter()
|
||||
.map(|cmd| self.expand_cmd(cmd, visitor))
|
||||
.collect::<ResultMultiple<_>>()?,
|
||||
.collect::<AllErrs<_, _>>()?,
|
||||
};
|
||||
|
||||
Ok(Rule {
|
||||
@ -177,7 +173,7 @@ impl Expander {
|
||||
.args
|
||||
.iter()
|
||||
.map(|arg| self.expand_expr(arg, visitor))
|
||||
.collect::<ResultMultiple<_>>()?,
|
||||
.collect::<AllErrs<_, _>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
@ -188,7 +184,9 @@ impl Expander {
|
||||
{
|
||||
let target = match *target {
|
||||
Target::File { ref file, is_static } => Target::File {
|
||||
file: self.expand_expr(file, visitor).map_err(AppError::expand_expr(file))?,
|
||||
file: self
|
||||
.expand_expr(file, visitor)
|
||||
.with_context(|| format!("Unable to expand expression {file}"))?,
|
||||
is_static,
|
||||
},
|
||||
|
||||
@ -198,12 +196,15 @@ impl Expander {
|
||||
.map(|(pat, expr)| {
|
||||
Ok((
|
||||
pat.clone(),
|
||||
self.expand_expr(expr, visitor).map_err(AppError::expand_expr(expr))?,
|
||||
self.expand_expr(expr, visitor)
|
||||
.with_context(|| format!("Unable to expand expression {expr}"))?,
|
||||
))
|
||||
})
|
||||
.collect::<ResultMultiple<_>>()?;
|
||||
.collect::<AllErrs<_, _>>()?;
|
||||
Target::Rule {
|
||||
rule: self.expand_expr(rule, visitor).map_err(AppError::expand_expr(rule))?,
|
||||
rule: self
|
||||
.expand_expr(rule, visitor)
|
||||
.with_context(|| format!("Unable to expand expression {rule}"))?,
|
||||
pats: Arc::new(pats),
|
||||
}
|
||||
},
|
||||
@ -250,11 +251,12 @@ impl TryFromExpr for Expr {
|
||||
|
||||
impl TryFromExpr for ArcStr {
|
||||
fn try_from_expr(expr: Expr) -> Result<Self, AppError> {
|
||||
expr.try_into_string()
|
||||
.map_err(|expr| AppError::UnresolvedAliasesOrPats {
|
||||
expr: expr.to_string(),
|
||||
expr_cmpts: expr.cmpts.into_iter().map(|cmpt| cmpt.to_string()).collect(),
|
||||
})
|
||||
expr.try_into_string().map_err(|expr| {
|
||||
app_error!(
|
||||
"Expression had unresolved aliases or patterns: {expr} ({:?})",
|
||||
expr.cmpts.iter().map(ExprCmpt::to_string).collect::<Vec<_>>()
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
40
src/lib.rs
40
src/lib.rs
@ -51,7 +51,6 @@ use {
|
||||
expand::Expander,
|
||||
rules::Rules,
|
||||
},
|
||||
anyhow::Context,
|
||||
futures::{stream::FuturesUnordered, StreamExt, TryFutureExt},
|
||||
std::{
|
||||
collections::BTreeMap,
|
||||
@ -65,6 +64,7 @@ use {
|
||||
},
|
||||
util::ArcStr,
|
||||
watcher::Watcher,
|
||||
zutil_app_error::Context,
|
||||
};
|
||||
|
||||
#[expect(clippy::too_many_lines, reason = "TODO: Split it up more")]
|
||||
@ -72,10 +72,7 @@ pub async fn run(args: Args) -> Result<(), AppError> {
|
||||
// Find the zbuild location and change the current directory to it
|
||||
// TODO: Not adjust the zbuild path and read it before?
|
||||
let zbuild_path = match args.zbuild_path {
|
||||
Some(path) => path
|
||||
.canonicalize()
|
||||
.context("Unable to canonicalize zbuild path")
|
||||
.map_err(AppError::Other)?,
|
||||
Some(path) => path.canonicalize().context("Unable to canonicalize zbuild path")?,
|
||||
None => self::find_zbuild().await?,
|
||||
};
|
||||
tracing::debug!(?zbuild_path, "Found zbuild path");
|
||||
@ -83,24 +80,21 @@ pub async fn run(args: Args) -> Result<(), AppError> {
|
||||
let zbuild_path = zbuild_path.file_name().expect("Zbuild path had no file name");
|
||||
let zbuild_path = Path::new(zbuild_path);
|
||||
tracing::debug!(?zbuild_dir, "Moving to zbuild directory");
|
||||
env::set_current_dir(zbuild_dir).map_err(AppError::set_current_dir(zbuild_dir))?;
|
||||
env::set_current_dir(zbuild_dir).with_context(|| format!("Unable to set current directory to {zbuild_dir:?}"))?;
|
||||
|
||||
// Parse the ast
|
||||
let zbuild_file = fs::read_to_string(zbuild_path).map_err(AppError::read_file(&zbuild_path))?;
|
||||
let zbuild_file =
|
||||
fs::read_to_string(zbuild_path).with_context(|| format!("Unable to read zbuild file {zbuild_path:?}"))?;
|
||||
let zbuild_file = ArcStr::from(zbuild_file);
|
||||
tracing::trace!(?zbuild_file, "Read zbuild.zb");
|
||||
let ast = Ast::parse_full(&zbuild_file)
|
||||
.context("Unable to parse zbuild file")
|
||||
.map_err(AppError::Other)?;
|
||||
let ast = Ast::parse_full(&zbuild_file).context("Unable to parse zbuild file")?;
|
||||
tracing::trace!(?ast, "Parsed ast");
|
||||
|
||||
// Create the expander
|
||||
let expander = Expander::new();
|
||||
|
||||
// Build the rules
|
||||
let rules = Rules::from_ast(&zbuild_file, ast)
|
||||
.context("Unable to build rules")
|
||||
.map_err(AppError::Other)?;
|
||||
let rules = Rules::from_ast(&zbuild_file, ast).context("Unable to build rules")?;
|
||||
tracing::trace!(?rules, "Built rules");
|
||||
|
||||
// Get the max number of jobs we can execute at once
|
||||
@ -111,7 +105,7 @@ pub async fn run(args: Args) -> Result<(), AppError> {
|
||||
},
|
||||
Some(jobs) => jobs,
|
||||
None => thread::available_parallelism()
|
||||
.map_err(AppError::get_default_jobs())?
|
||||
.context("Unable to query system for available parallelism for default number of jobs")?
|
||||
.into(),
|
||||
};
|
||||
tracing::debug!(?jobs, "Concurrent jobs");
|
||||
@ -162,8 +156,7 @@ pub async fn run(args: Args) -> Result<(), AppError> {
|
||||
!args.watch && !args.keep_going,
|
||||
args.always_build,
|
||||
)
|
||||
.context("Unable to create builder")
|
||||
.map_err(AppError::Other)?;
|
||||
.context("Unable to create builder")?;
|
||||
let builder = Arc::new(builder);
|
||||
|
||||
// Then create the watcher, if we're watching
|
||||
@ -217,29 +210,32 @@ pub async fn run(args: Args) -> Result<(), AppError> {
|
||||
false => {
|
||||
tracing::error!("One or more builds failed:");
|
||||
for (target, err) in failed_targets {
|
||||
tracing::error!(err=%err.pretty(), "Failed to build target {target}");
|
||||
tracing::error!(err=%error::pretty(&err), "Failed to build target {target}");
|
||||
}
|
||||
|
||||
Err(AppError::ExitDueToFailedBuilds {})
|
||||
Err(AppError::msg("Exiting with non-0 due to failed builds"))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the nearest zbuild file
|
||||
async fn find_zbuild() -> Result<PathBuf, AppError> {
|
||||
let cur_path = env::current_dir().map_err(AppError::get_current_dir())?;
|
||||
let cur_path = env::current_dir().context("Unable to get current directory")?;
|
||||
let mut cur_path = cur_path.as_path();
|
||||
|
||||
loop {
|
||||
let zbuild_path = cur_path.join("zbuild.zb");
|
||||
match util::fs_try_exists_symlink(&zbuild_path)
|
||||
.await
|
||||
.map_err(AppError::check_file_exists(&zbuild_path))?
|
||||
.with_context(|| format!("Unable to check if file exists {zbuild_path:?}"))?
|
||||
{
|
||||
true => return Ok(zbuild_path),
|
||||
false => match cur_path.parent() {
|
||||
Some(parent) => cur_path = parent,
|
||||
None => return Err(AppError::ZBuildNotFound {}),
|
||||
None => zutil_app_error::bail!(
|
||||
"No `zbuild.zb` file found in current or parent directories.\nYou can use `--path \
|
||||
{{zbuild-path}}` in order to specify the manifest's path"
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -273,7 +269,7 @@ async fn build_target<T: BuildableTargetInner + fmt::Display + fmt::Debug>(
|
||||
Ok(())
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::error!(%target, err=%err.pretty(), "Unable to build target");
|
||||
tracing::error!(%target, err=%error::pretty(&err), "Unable to build target");
|
||||
Err(err)
|
||||
},
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ mod pre_init;
|
||||
|
||||
// Imports
|
||||
use {
|
||||
anyhow::Context,
|
||||
crate::AppError,
|
||||
std::{
|
||||
env::{self, VarError},
|
||||
fs,
|
||||
@ -17,6 +17,7 @@ use {
|
||||
},
|
||||
tracing::metadata::LevelFilter,
|
||||
tracing_subscriber::{prelude::*, EnvFilter, Registry},
|
||||
zutil_app_error::Context,
|
||||
};
|
||||
|
||||
/// Initializes the logger
|
||||
@ -96,7 +97,7 @@ where
|
||||
}
|
||||
|
||||
/// Creates the file layer
|
||||
fn file_layer<S>(log_path: &Path) -> Result<impl tracing_subscriber::Layer<S>, anyhow::Error>
|
||||
fn file_layer<S>(log_path: &Path) -> Result<impl tracing_subscriber::Layer<S>, AppError>
|
||||
where
|
||||
S: tracing::Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span> + 'static,
|
||||
{
|
||||
|
||||
@ -28,7 +28,6 @@ mod logger;
|
||||
|
||||
// Imports
|
||||
use {
|
||||
anyhow::Context,
|
||||
clap::Parser,
|
||||
std::{
|
||||
env,
|
||||
@ -36,6 +35,7 @@ use {
|
||||
},
|
||||
tokio::runtime,
|
||||
zbuild::{AppError, Args, ExitResult},
|
||||
zutil_app_error::Context,
|
||||
};
|
||||
|
||||
#[expect(
|
||||
@ -76,10 +76,7 @@ fn main() -> ExitResult {
|
||||
}
|
||||
}
|
||||
|
||||
let runtime = runtime_builder
|
||||
.build()
|
||||
.context("Failed building the Runtime")
|
||||
.map_err(AppError::Other)?;
|
||||
let runtime = runtime_builder.build().context("Failed building the Runtime")?;
|
||||
|
||||
runtime.block_on(zbuild::run(args))?;
|
||||
ExitResult::Ok
|
||||
|
||||
@ -18,7 +18,7 @@ pub use {
|
||||
|
||||
// Imports
|
||||
use {
|
||||
crate::{util::ArcStr, Ast},
|
||||
crate::{util::ArcStr, AppError, Ast},
|
||||
indexmap::IndexMap,
|
||||
std::sync::Arc,
|
||||
};
|
||||
@ -50,7 +50,7 @@ pub struct Rules {
|
||||
|
||||
impl Rules {
|
||||
/// Creates all rules from the ast
|
||||
pub fn from_ast(zbuild_file: &ArcStr, ast: Ast<'_>) -> Result<Self, anyhow::Error> {
|
||||
pub fn from_ast(zbuild_file: &ArcStr, ast: Ast<'_>) -> Result<Self, AppError> {
|
||||
let aliases = ast
|
||||
.aliases
|
||||
.into_iter()
|
||||
@ -84,7 +84,7 @@ impl Rules {
|
||||
let name = zbuild_file.slice_from_str(rule.name.0);
|
||||
(name, Rule::from_ast(zbuild_file, rule)?)
|
||||
})
|
||||
.collect::<Result<_, anyhow::Error>>()?;
|
||||
.collect::<Result<_, AppError>>()?;
|
||||
|
||||
Ok(Self {
|
||||
aliases: Arc::new(aliases),
|
||||
|
||||
@ -2,10 +2,11 @@
|
||||
|
||||
use {
|
||||
super::Expr,
|
||||
crate::{error::AppError, rules::pattern::Pattern, util::ArcStr},
|
||||
crate::{rules::pattern::Pattern, util::ArcStr},
|
||||
indexmap::IndexMap,
|
||||
itertools::{Itertools, PeekingNext},
|
||||
std::collections::BTreeMap,
|
||||
crate::AppError,
|
||||
};
|
||||
|
||||
/// An expression tree.
|
||||
@ -74,9 +75,7 @@ impl<K> ExprTree<K> {
|
||||
|
||||
// After this the expression should be empty
|
||||
if let Some(cmpt) = cmpts.next() {
|
||||
return Err(AppError::Other(anyhow::anyhow!(
|
||||
"Unexpected component in expression {expr}: {cmpt}"
|
||||
)));
|
||||
zutil_app_error::bail!("Unexpected component in expression {expr}: {cmpt}");
|
||||
}
|
||||
|
||||
// Finally try to insert and retrieve the old key, if any.
|
||||
|
||||
@ -5,6 +5,7 @@ use {
|
||||
super::Expr,
|
||||
crate::{ast, util::ArcStr},
|
||||
std::fmt,
|
||||
crate::AppError,
|
||||
};
|
||||
|
||||
|
||||
@ -23,10 +24,10 @@ pub enum OutItem<T> {
|
||||
|
||||
impl OutItem<Expr> {
|
||||
/// Creates a new item from it's `ast`.
|
||||
pub fn from_ast(zbuild_file: &ArcStr, item: ast::Expr<'_>) -> Result<Self, anyhow::Error> {
|
||||
pub fn from_ast(zbuild_file: &ArcStr, item: ast::Expr<'_>) -> Result<Self, AppError> {
|
||||
let is_deps_file = item.is_deps_file;
|
||||
anyhow::ensure!(!item.is_opt, "Output items cannot be optional");
|
||||
anyhow::ensure!(!item.is_static, "Output items cannot be static");
|
||||
zutil_app_error::ensure!(!item.is_opt, "Output items cannot be optional");
|
||||
zutil_app_error::ensure!(!item.is_static, "Output items cannot be static");
|
||||
|
||||
Ok(Self::File {
|
||||
file: Expr::from_ast(zbuild_file, item),
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
// Imports
|
||||
use {
|
||||
super::{pattern::Pattern, DepItem, Expr, OutItem},
|
||||
crate::{ast, util::ArcStr},
|
||||
crate::{ast, util::ArcStr, AppError},
|
||||
indexmap::IndexMap,
|
||||
std::sync::Arc,
|
||||
};
|
||||
@ -32,7 +32,7 @@ pub struct Rule<T> {
|
||||
|
||||
impl Rule<Expr> {
|
||||
/// Creates a new rule from it's ast
|
||||
pub fn from_ast(zbuild_file: &ArcStr, rule: ast::RuleStmt<'_>) -> Result<Self, anyhow::Error> {
|
||||
pub fn from_ast(zbuild_file: &ArcStr, rule: ast::RuleStmt<'_>) -> Result<Self, AppError> {
|
||||
let aliases = rule
|
||||
.aliases
|
||||
.into_iter()
|
||||
@ -56,7 +56,7 @@ impl Rule<Expr> {
|
||||
.0
|
||||
.into_iter()
|
||||
.map(|out| OutItem::from_ast(zbuild_file, out))
|
||||
.collect::<Result<_, anyhow::Error>>()?;
|
||||
.collect::<Result<_, AppError>>()?;
|
||||
let deps = rule
|
||||
.deps
|
||||
.0
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
// Imports
|
||||
use {
|
||||
crate::{build, rules::Target, util::ArcStr, AppError, Builder},
|
||||
anyhow::Context,
|
||||
dashmap::{DashMap, DashSet},
|
||||
futures::{stream::FuturesUnordered, StreamExt},
|
||||
notify_debouncer_full::Debouncer,
|
||||
@ -21,6 +20,7 @@ use {
|
||||
},
|
||||
tokio::sync::mpsc,
|
||||
tokio_stream::wrappers::ReceiverStream,
|
||||
zutil_app_error::Context,
|
||||
};
|
||||
|
||||
/// A reverse dependency
|
||||
@ -69,12 +69,11 @@ impl Watcher {
|
||||
let _: Result<(), _> = fs_event_tx.blocking_send(fs_event);
|
||||
},
|
||||
Err(errs) =>
|
||||
for err in errs {
|
||||
tracing::warn!(err=?anyhow::Error::from(err), "Error while watching");
|
||||
for err in &errs {
|
||||
tracing::warn!(err=?AppError::from(err), "Error while watching");
|
||||
},
|
||||
})
|
||||
.context("Unable to create file watcher")
|
||||
.map_err(AppError::Other)?;
|
||||
.context("Unable to create file watcher")?;
|
||||
|
||||
Ok(Self {
|
||||
watcher,
|
||||
|
||||
@ -7,7 +7,10 @@
|
||||
mod util;
|
||||
|
||||
// Imports
|
||||
use {anyhow::Context, zbuild::ExitResult};
|
||||
use {
|
||||
zbuild::ExitResult,
|
||||
zutil_app_error::{app_error, Context},
|
||||
};
|
||||
|
||||
/// Single rule with multiple outputs
|
||||
#[tokio::test]
|
||||
@ -32,7 +35,7 @@ rule create_file {
|
||||
let file2_out = temp_dir.path().join("file2.out");
|
||||
for file_out in [file1_out, file2_out] {
|
||||
if !file_out.try_exists().context("Unable to check if output file exists")? {
|
||||
Err(anyhow::anyhow!("Output file {file_out:?} was missing"))?;
|
||||
Err(app_error!("Output file {file_out:?} was missing"))?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,10 @@
|
||||
mod util;
|
||||
|
||||
// Imports
|
||||
use {anyhow::Context, zbuild::ExitResult};
|
||||
use {
|
||||
zbuild::ExitResult,
|
||||
zutil_app_error::{app_error, Context},
|
||||
};
|
||||
|
||||
/// Single rule and target
|
||||
#[tokio::test]
|
||||
@ -28,7 +31,7 @@ rule create_file {
|
||||
// Note: We're making sure it *doesn't* exist, since we didn't want to build it.
|
||||
let file_out = temp_dir.path().join("file.out");
|
||||
if file_out.try_exists().context("Unable to check if output file exists")? {
|
||||
Err(anyhow::anyhow!("Output file {file_out:?} was present"))?;
|
||||
Err(app_error!("Output file {file_out:?} was present"))?;
|
||||
}
|
||||
|
||||
ExitResult::Ok
|
||||
|
||||
@ -7,7 +7,10 @@
|
||||
mod util;
|
||||
|
||||
// Imports
|
||||
use {anyhow::Context, zbuild::ExitResult};
|
||||
use {
|
||||
zbuild::ExitResult,
|
||||
zutil_app_error::{app_error, Context},
|
||||
};
|
||||
|
||||
/// Single rule and target
|
||||
#[tokio::test]
|
||||
@ -26,7 +29,7 @@ rule create_file {
|
||||
|
||||
let file_out = temp_dir.path().join("file.out");
|
||||
if !file_out.try_exists().context("Unable to check if output file exists")? {
|
||||
Err(anyhow::anyhow!("Output file {file_out:?} was missing"))?;
|
||||
Err(app_error!("Output file {file_out:?} was missing"))?;
|
||||
}
|
||||
|
||||
ExitResult::Ok
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// Features
|
||||
#![feature(must_not_suspend)]
|
||||
#![feature(must_not_suspend, yeet_expr)]
|
||||
// Lints
|
||||
#![expect(clippy::tests_outside_test_module, reason = "We're an integration test")]
|
||||
|
||||
@ -8,10 +8,10 @@ mod util;
|
||||
|
||||
// Imports
|
||||
use {
|
||||
anyhow::Context,
|
||||
std::fs,
|
||||
tempfile::TempDir,
|
||||
zbuild::{Args, ExitResult},
|
||||
zbuild::{AppError, Args, ExitResult},
|
||||
zutil_app_error::Context,
|
||||
};
|
||||
|
||||
/// Test for `--keep-going`
|
||||
@ -46,7 +46,7 @@ async fn keep_going() -> ExitResult {
|
||||
///
|
||||
/// When testing with `keep_going = false`, we ensure that `C1` is not built,
|
||||
/// since `C2` exits after `B` errors, so nothing else should be built.
|
||||
async fn inner(keep_going: bool) -> Result<(), anyhow::Error> {
|
||||
async fn inner(keep_going: bool) -> Result<(), AppError> {
|
||||
let temp_dir = TempDir::with_prefix("zbuild").context("Unable to create temporary directory")?;
|
||||
let zbuild_zb = temp_dir.path().join("zbuild.zb");
|
||||
|
||||
@ -95,34 +95,34 @@ rule c2 {
|
||||
};
|
||||
tracing::info!(?args, "Arguments");
|
||||
let res = zbuild::run(args).await;
|
||||
anyhow::ensure!(res.is_err(), "Expected zbuild error");
|
||||
zutil_app_error::ensure!(res.is_err(), "Expected zbuild error");
|
||||
|
||||
|
||||
let a = temp_dir.path().join("a");
|
||||
let b = temp_dir.path().join("b");
|
||||
let c1 = temp_dir.path().join("c1");
|
||||
let c2 = temp_dir.path().join("c2");
|
||||
anyhow::ensure!(
|
||||
zutil_app_error::ensure!(
|
||||
!a.try_exists().context("Unable to check if output file exists")?,
|
||||
"Output file {a:?} was created"
|
||||
);
|
||||
anyhow::ensure!(
|
||||
zutil_app_error::ensure!(
|
||||
!b.try_exists().context("Unable to check if output file exists")?,
|
||||
"Output file {b:?} was created"
|
||||
);
|
||||
|
||||
match keep_going {
|
||||
true => anyhow::ensure!(
|
||||
true => zutil_app_error::ensure!(
|
||||
c1.try_exists().context("Unable to check if output file exists")?,
|
||||
"Output file {c1:?} was missing"
|
||||
),
|
||||
false => anyhow::ensure!(
|
||||
false => zutil_app_error::ensure!(
|
||||
!c1.try_exists().context("Unable to check if output file exists")?,
|
||||
"Output file {c1:?} was created"
|
||||
),
|
||||
}
|
||||
|
||||
anyhow::ensure!(
|
||||
zutil_app_error::ensure!(
|
||||
c2.try_exists().context("Unable to check if output file exists")?,
|
||||
"Output file {c2:?} was missing"
|
||||
);
|
||||
|
||||
@ -7,10 +7,15 @@
|
||||
)]
|
||||
|
||||
// Imports
|
||||
use {anyhow::Context, std::fs, tempfile::TempDir, zbuild::Args};
|
||||
use {
|
||||
std::fs,
|
||||
tempfile::TempDir,
|
||||
zbuild::{AppError, Args},
|
||||
zutil_app_error::Context,
|
||||
};
|
||||
|
||||
/// Creates a directory with a zbuild manifest, then runs it, and returns the directory
|
||||
pub async fn with_zbuild<'a, T>(zbuild_manifest: &str, targets: T) -> Result<TempDir, anyhow::Error>
|
||||
pub async fn with_zbuild<'a, T>(zbuild_manifest: &str, targets: T) -> Result<TempDir, AppError>
|
||||
where
|
||||
T: AsRef<[&'a str]>,
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user