Replaced anyhow and AppError with zutil_app_error.

This commit is contained in:
Filipe Rodrigues 2025-01-30 04:48:04 +00:00
parent eedd24d9b5
commit 5217f091b6
Signed by: zenithsiz
SSH Key Fingerprint: SHA256:Mb5ppb3Sh7IarBO/sBTXLHbYEOz37hJAlslLQPPAPaU
21 changed files with 231 additions and 1070 deletions

View File

@ -20,7 +20,8 @@
"tempdir",
"thiserror",
"yeet",
"Zbuild"
"Zbuild",
"zutil"
],
"rust-analyzer.cargo.features": "all"
}

10
Cargo.lock generated
View File

@ -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",
]

View File

@ -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]

View File

@ -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)} )*

View File

@ -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}]")
}

View File

@ -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(()),
})
}

View File

@ -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)
}

View File

@ -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<_>>()
)
})
}
}

View File

@ -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)
},
}

View File

@ -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,
{

View File

@ -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

View File

@ -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),

View File

@ -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.

View File

@ -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),

View File

@ -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

View File

@ -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,

View File

@ -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"))?;
}
}

View File

@ -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

View File

@ -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

View File

@ -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"
);

View File

@ -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]>,
{