Skip to content

Commit 4d15abe

Browse files
Merge #806
806: improve error messages r=Alexhuszagh a=Emilgardis this makes error messages a bit better by displaying stdout/stderr if it was captured also improves command output in verbose mode or in errors new: <img width="715" alt="the new output on an error on verbose mode" src="https://user-images.githubusercontent.com/1502855/174091357-d9122066-cd1c-4a21-8392-2572ef93b734.png"> old: <img width="577" alt="the old output on an error on verbose mode, has a some unnecessary double quoutes and doesn't tell what the actual reported error was/is" src="https://user-images.githubusercontent.com/1502855/174019703-7c5e413b-9f4b-487f-88e0-66a422a99302.png"> Co-authored-by: Emil Gardström <[email protected]>
2 parents a4c444b + 9e1a13c commit 4d15abe

File tree

9 files changed

+229
-33
lines changed

9 files changed

+229
-33
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ members = ["xtask"]
1919
[dependencies]
2020
atty = "0.2"
2121
clap = { version = "3.2.2", features = ["derive", "unstable-v4"] }
22-
color-eyre = "0.6"
22+
color-eyre = { version = "0.6", default-features = false }
2323
eyre = "0.6"
2424
thiserror = "1"
2525
home = "0.5"

src/cargo.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ pub fn cargo_metadata_with_args(
111111
.ok()
112112
.unwrap_or_else(|| "cargo".to_string()),
113113
);
114-
command.arg("metadata").arg("--format-version=1");
114+
command.arg("metadata").args(&["--format-version", "1"]);
115115
if let Some(cd) = cd {
116116
command.current_dir(cd);
117117
}
@@ -154,9 +154,6 @@ pub fn run(args: &[String], verbose: bool) -> Result<ExitStatus, CommandError> {
154154
}
155155

156156
/// run cargo and get the output, does not check the exit status
157-
pub fn run_and_get_output(
158-
args: &[String],
159-
verbose: bool,
160-
) -> Result<std::process::Output, CommandError> {
157+
pub fn run_and_get_output(args: &[String], verbose: bool) -> Result<std::process::Output> {
161158
Command::new("cargo").args(args).run_and_get_output(verbose)
162159
}

src/docker/engine.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use std::process::Command;
55
use crate::errors::*;
66
use crate::extensions::CommandExt;
77

8-
const DOCKER: &str = "docker";
9-
const PODMAN: &str = "podman";
8+
pub const DOCKER: &str = "docker";
9+
pub const PODMAN: &str = "podman";
1010

1111
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1212
pub enum EngineType {

src/errors.rs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,35 @@ pub fn install_panic_hook() -> Result<()> {
1010

1111
#[derive(Debug, thiserror::Error)]
1212
pub enum CommandError {
13-
#[error("`{1}` failed with exit code: {0}")]
14-
NonZeroExitCode(std::process::ExitStatus, String),
15-
#[error("could not execute `{0}`")]
16-
CouldNotExecute(#[source] Box<dyn std::error::Error + Send + Sync>, String),
13+
#[error("`{command}` failed with {status}")]
14+
NonZeroExitCode {
15+
status: std::process::ExitStatus,
16+
command: String,
17+
stderr: Vec<u8>,
18+
stdout: Vec<u8>,
19+
},
20+
#[error("could not execute `{command}`")]
21+
CouldNotExecute {
22+
#[source]
23+
source: Box<dyn std::error::Error + Send + Sync>,
24+
command: String,
25+
},
1726
#[error("`{0:?}` output was not UTF-8")]
1827
Utf8Error(#[source] std::string::FromUtf8Error, std::process::Output),
1928
}
29+
30+
impl CommandError {
31+
/// Attach valuable information to this [`CommandError`](Self)
32+
pub fn to_section_report(self) -> eyre::Report {
33+
match &self {
34+
CommandError::NonZeroExitCode { stderr, stdout, .. } => {
35+
let stderr = String::from_utf8_lossy(stderr).trim().to_string();
36+
let stdout = String::from_utf8_lossy(stdout).trim().to_string();
37+
eyre::eyre!(self)
38+
.section(color_eyre::SectionExt::header(stderr, "Stderr:"))
39+
.section(color_eyre::SectionExt::header(stdout, "Stdout:"))
40+
}
41+
_ => eyre::eyre!(self),
42+
}
43+
}
44+
}

src/extensions.rs

Lines changed: 85 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,98 @@
11
use std::borrow::Cow;
22
use std::fmt;
3-
use std::process::{Command, ExitStatus};
3+
use std::process::{Command, ExitStatus, Output};
44

55
use crate::errors::*;
66

7+
pub const STRIPPED_BINS: &[&str] = &[crate::docker::DOCKER, crate::docker::PODMAN, "cargo"];
8+
79
pub trait CommandExt {
810
fn print_verbose(&self, verbose: bool);
9-
fn status_result(&self, status: ExitStatus) -> Result<(), CommandError>;
11+
fn status_result(
12+
&self,
13+
verbose: bool,
14+
status: ExitStatus,
15+
output: Option<&Output>,
16+
) -> Result<(), CommandError>;
1017
fn run(&mut self, verbose: bool, silence_stdout: bool) -> Result<(), CommandError>;
1118
fn run_and_get_status(
1219
&mut self,
1320
verbose: bool,
1421
silence_stdout: bool,
1522
) -> Result<ExitStatus, CommandError>;
16-
fn run_and_get_stdout(&mut self, verbose: bool) -> Result<String, CommandError>;
17-
fn run_and_get_output(&mut self, verbose: bool) -> Result<std::process::Output, CommandError>;
23+
fn run_and_get_stdout(&mut self, verbose: bool) -> Result<String>;
24+
fn run_and_get_output(&mut self, verbose: bool) -> Result<std::process::Output>;
25+
fn command_pretty(&self, verbose: bool, strip: impl for<'a> Fn(&'a str) -> bool) -> String;
1826
}
1927

2028
impl CommandExt for Command {
29+
fn command_pretty(&self, verbose: bool, strip: impl for<'a> Fn(&'a str) -> bool) -> String {
30+
// a dummy implementor of display to avoid using unwraps
31+
struct C<'c, F>(&'c Command, bool, F);
32+
impl<F> std::fmt::Display for C<'_, F>
33+
where
34+
F: for<'a> Fn(&'a str) -> bool,
35+
{
36+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37+
let cmd = self.0;
38+
write!(
39+
f,
40+
"{}",
41+
// if verbose, never strip, if not, let user choose
42+
crate::file::pretty_path(cmd.get_program(), move |c| if self.1 {
43+
false
44+
} else {
45+
self.2(c)
46+
})
47+
)?;
48+
let args = cmd.get_args();
49+
if args.len() > 1 {
50+
write!(
51+
f,
52+
" {}",
53+
shell_words::join(args.map(|o| o.to_string_lossy()))
54+
)?;
55+
}
56+
Ok(())
57+
}
58+
}
59+
format!("{}", C(self, verbose, strip))
60+
}
61+
2162
fn print_verbose(&self, verbose: bool) {
63+
// TODO: introduce verbosity levels, v = 1, strip cmd, v > 1, don't strip cmd
2264
if verbose {
2365
if let Some(cwd) = self.get_current_dir() {
24-
println!("+ {:?} {:?}", cwd, self);
66+
println!("+ {:?} {}", cwd, self.command_pretty(true, |_| false));
2567
} else {
26-
println!("+ {:?}", self);
68+
println!("+ {}", self.command_pretty(true, |_| false));
2769
}
2870
}
2971
}
3072

31-
fn status_result(&self, status: ExitStatus) -> Result<(), CommandError> {
73+
fn status_result(
74+
&self,
75+
verbose: bool,
76+
status: ExitStatus,
77+
output: Option<&Output>,
78+
) -> Result<(), CommandError> {
3279
if status.success() {
3380
Ok(())
3481
} else {
35-
Err(CommandError::NonZeroExitCode(status, format!("{self:?}")))
82+
Err(CommandError::NonZeroExitCode {
83+
status,
84+
command: self
85+
.command_pretty(verbose, |ref cmd| STRIPPED_BINS.iter().any(|f| f == cmd)),
86+
stderr: output.map(|out| out.stderr.clone()).unwrap_or_default(),
87+
stdout: output.map(|out| out.stdout.clone()).unwrap_or_default(),
88+
})
3689
}
3790
}
3891

3992
/// Runs the command to completion
4093
fn run(&mut self, verbose: bool, silence_stdout: bool) -> Result<(), CommandError> {
4194
let status = self.run_and_get_status(verbose, silence_stdout)?;
42-
self.status_result(status)
95+
self.status_result(verbose, status, None)
4396
}
4497

4598
/// Runs the command to completion
@@ -53,37 +106,53 @@ impl CommandExt for Command {
53106
self.stdout(std::process::Stdio::null());
54107
}
55108
self.status()
56-
.map_err(|e| CommandError::CouldNotExecute(Box::new(e), format!("{self:?}")))
109+
.map_err(|e| CommandError::CouldNotExecute {
110+
source: Box::new(e),
111+
command: self
112+
.command_pretty(verbose, |ref cmd| STRIPPED_BINS.iter().any(|f| f == cmd)),
113+
})
57114
.map_err(Into::into)
58115
}
59116

60117
/// Runs the command to completion and returns its stdout
61-
fn run_and_get_stdout(&mut self, verbose: bool) -> Result<String, CommandError> {
118+
fn run_and_get_stdout(&mut self, verbose: bool) -> Result<String> {
62119
let out = self.run_and_get_output(verbose)?;
63-
self.status_result(out.status)?;
64-
out.stdout()
120+
self.status_result(verbose, out.status, Some(&out))
121+
.map_err(CommandError::to_section_report)?;
122+
out.stdout().map_err(Into::into)
65123
}
66124

67125
/// Runs the command to completion and returns the status and its [output](std::process::Output).
68126
///
69127
/// # Notes
70128
///
71129
/// This command does not check the status.
72-
fn run_and_get_output(&mut self, verbose: bool) -> Result<std::process::Output, CommandError> {
130+
fn run_and_get_output(&mut self, verbose: bool) -> Result<std::process::Output> {
73131
self.print_verbose(verbose);
74-
self.output()
75-
.map_err(|e| CommandError::CouldNotExecute(Box::new(e), format!("{self:?}")))
132+
self.output().map_err(|e| {
133+
CommandError::CouldNotExecute {
134+
source: Box::new(e),
135+
command: self
136+
.command_pretty(verbose, |ref cmd| STRIPPED_BINS.iter().any(|f| f == cmd)),
137+
}
138+
.to_section_report()
139+
})
76140
}
77141
}
78142

79143
pub trait OutputExt {
80144
fn stdout(&self) -> Result<String, CommandError>;
145+
fn stderr(&self) -> Result<String, CommandError>;
81146
}
82147

83148
impl OutputExt for std::process::Output {
84149
fn stdout(&self) -> Result<String, CommandError> {
85150
String::from_utf8(self.stdout.clone()).map_err(|e| CommandError::Utf8Error(e, self.clone()))
86151
}
152+
153+
fn stderr(&self) -> Result<String, CommandError> {
154+
String::from_utf8(self.stderr.clone()).map_err(|e| CommandError::Utf8Error(e, self.clone()))
155+
}
87156
}
88157

89158
pub struct SafeCommand {

src/file.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::borrow::Cow;
2+
use std::ffi::OsStr;
13
use std::fs::{self, File};
24
use std::io::Read;
35
use std::path::{Path, PathBuf};
@@ -36,6 +38,55 @@ fn _canonicalize(path: &Path) -> Result<PathBuf> {
3638
}
3739
}
3840

41+
/// Pretty format a file path. Also removes the path prefix from a command if wanted
42+
pub fn pretty_path(path: impl AsRef<Path>, strip: impl for<'a> Fn(&'a str) -> bool) -> String {
43+
let path = path.as_ref();
44+
// TODO: Use Path::file_prefix
45+
let file_stem = path.file_stem();
46+
let file_name = path.file_name();
47+
let path = if let (Some(file_stem), Some(file_name)) = (file_stem, file_name) {
48+
if let Some(file_name) = file_name.to_str() {
49+
if strip(file_name) {
50+
Cow::Borrowed(file_stem)
51+
} else {
52+
Cow::Borrowed(path.as_os_str())
53+
}
54+
} else {
55+
Cow::Borrowed(path.as_os_str())
56+
}
57+
} else {
58+
maybe_canonicalize(path)
59+
};
60+
61+
if let Some(path) = path.to_str() {
62+
shell_escape(path).to_string()
63+
} else {
64+
format!("{path:?}")
65+
}
66+
}
67+
68+
pub fn shell_escape(string: &str) -> Cow<'_, str> {
69+
let escape: &[char] = if cfg!(target_os = "windows") {
70+
&['%', '$', '`', '!', '"']
71+
} else {
72+
&['$', '\'', '\\', '!', '"']
73+
};
74+
75+
if string.contains(escape) {
76+
Cow::Owned(format!("{string:?}"))
77+
} else if string.contains(' ') {
78+
Cow::Owned(format!("\"{string}\""))
79+
} else {
80+
Cow::Borrowed(string)
81+
}
82+
}
83+
84+
pub fn maybe_canonicalize(path: &Path) -> Cow<'_, OsStr> {
85+
canonicalize(path)
86+
.map(|p| Cow::Owned(p.as_os_str().to_owned()))
87+
.unwrap_or_else(|_| path.as_os_str().into())
88+
}
89+
3990
pub fn write_file(path: impl AsRef<Path>, overwrite: bool) -> Result<File> {
4091
let path = path.as_ref();
4192
fs::create_dir_all(
@@ -57,3 +108,55 @@ pub fn write_file(path: impl AsRef<Path>, overwrite: bool) -> Result<File> {
57108
open.open(&path)
58109
.wrap_err(format!("couldn't write to file `{}`", path.display()))
59110
}
111+
112+
#[cfg(test)]
113+
mod tests {
114+
use super::*;
115+
116+
#[test]
117+
#[cfg(target_family = "windows")]
118+
fn pretty_path_windows() {
119+
assert_eq!(
120+
pretty_path("C:\\path\\bin\\cargo.exe", |f| f.contains("cargo")),
121+
"cargo".to_string()
122+
);
123+
assert_eq!(
124+
pretty_path("C:\\Program Files\\Docker\\bin\\docker.exe", |_| false),
125+
"\"C:\\Program Files\\Docker\\bin\\docker.exe\"".to_string()
126+
);
127+
assert_eq!(
128+
pretty_path("C:\\Program Files\\single'quote\\cargo.exe", |c| c
129+
.contains("cargo")),
130+
"cargo".to_string()
131+
);
132+
assert_eq!(
133+
pretty_path("C:\\Program Files\\single'quote\\cargo.exe", |_| false),
134+
"\"C:\\Program Files\\single'quote\\cargo.exe\"".to_string()
135+
);
136+
assert_eq!(
137+
pretty_path("C:\\Program Files\\%not_var%\\cargo.exe", |_| false),
138+
"\"C:\\\\Program Files\\\\%not_var%\\\\cargo.exe\"".to_string()
139+
);
140+
}
141+
142+
#[test]
143+
#[cfg(target_family = "unix")]
144+
fn pretty_path_linux() {
145+
assert_eq!(
146+
pretty_path("/usr/bin/cargo", |f| f.contains("cargo")),
147+
"cargo".to_string()
148+
);
149+
assert_eq!(
150+
pretty_path("/home/user/my rust/bin/cargo", |_| false),
151+
"\"/home/user/my rust/bin/cargo\"".to_string(),
152+
);
153+
assert_eq!(
154+
pretty_path("/home/user/single'quote/cargo", |c| c.contains("cargo")),
155+
"cargo".to_string()
156+
);
157+
assert_eq!(
158+
pretty_path("/home/user/single'quote/cargo", |_| false),
159+
"\"/home/user/single'quote/cargo\"".to_string()
160+
);
161+
}
162+
}

src/rustc.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ pub fn target_list(verbose: bool) -> Result<TargetList> {
4040
.map(|s| TargetList {
4141
triples: s.lines().map(|l| l.to_owned()).collect(),
4242
})
43-
.map_err(Into::into)
4443
}
4544

4645
pub fn sysroot(host: &Host, target: &Target, verbose: bool) -> Result<PathBuf> {

src/rustup.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,18 @@ pub fn installed_toolchains(verbose: bool) -> Result<Vec<String>> {
4545
pub fn available_targets(toolchain: &str, verbose: bool) -> Result<AvailableTargets> {
4646
let mut cmd = Command::new("rustup");
4747
cmd.args(&["target", "list", "--toolchain", toolchain]);
48-
let output = cmd.run_and_get_output(verbose)?;
48+
let output = cmd
49+
.run_and_get_output(verbose)
50+
.suggestion("is rustup installed?")?;
4951

5052
if !output.status.success() {
5153
if String::from_utf8_lossy(&output.stderr).contains("is a custom toolchain") {
5254
eyre::bail!("{toolchain} is a custom toolchain. To use it, you'll need to set the environment variable `CROSS_CUSTOM_TOOLCHAIN=1`")
5355
}
54-
return Err(cmd.status_result(output.status).unwrap_err().into());
56+
return Err(cmd
57+
.status_result(verbose, output.status, Some(&output))
58+
.unwrap_err()
59+
.to_section_report());
5560
}
5661
let out = output.stdout()?;
5762
let mut default = String::new();

0 commit comments

Comments
 (0)