diff --git a/src/check_release.rs b/src/check_release.rs index c0c137230..c6a322e7b 100644 --- a/src/check_release.rs +++ b/src/check_release.rs @@ -1,6 +1,6 @@ +use std::io::Write as _; use std::{collections::BTreeMap, sync::Arc, time::Instant}; -use anstream::{eprintln, println}; use anstyle::{AnsiColor, Color, Reset, Style}; use anyhow::Context; @@ -158,13 +158,14 @@ pub(super) fn run_check_release( let mut results_with_errors = vec![]; for (semver_query, time_to_decide, results) in all_results { config - .log_verbose(|_| { + .log_verbose(|config| { let category = match semver_query.required_update { RequiredSemverUpdate::Major => "major", RequiredSemverUpdate::Minor => "minor", }; if results.is_empty() { - eprintln!( + writeln!( + config.stderr(), "{}{:>12}{} [{:8.3}s] {:^18} {}", Style::new() .fg_color(Some(Color::Ansi(AnsiColor::Green))) @@ -174,9 +175,10 @@ pub(super) fn run_check_release( time_to_decide.as_secs_f32(), category, semver_query.id - ); + )?; } else { - eprintln!( + writeln!( + config.stderr(), "{}{:>12}{} [{:>8.3}s] {:^18} {}", Style::new() .fg_color(Some(Color::Ansi(AnsiColor::Red))) @@ -186,7 +188,7 @@ pub(super) fn run_check_release( time_to_decide.as_secs_f32(), category, semver_query.id - ); + )?; } Ok(()) }) @@ -218,18 +220,20 @@ pub(super) fn run_check_release( for (semver_query, results) in results_with_errors { required_versions.push(semver_query.required_update); config - .log_info(|_| { - println!( + .log_info(|config| { + writeln!( + config.stdout(), "\n--- failure {}: {} ---\n", - &semver_query.id, &semver_query.human_readable_name - ); + &semver_query.id, + &semver_query.human_readable_name + )?; Ok(()) }) .expect("print failed"); if let Some(ref_link) = semver_query.reference_link.as_deref() { - config.log_info(|_| { - println!("{}Description:{}\n{}\n{:>12} {}\n{:>12} {}\n", + config.log_info(|config| { + writeln!(config.stdout(), "{}Description:{}\n{}\n{:>12} {}\n{:>12} {}\n", Style::new().bold(), Reset, &semver_query.error_message, "ref:", @@ -240,13 +244,14 @@ pub(super) fn run_check_release( crate_version!(), semver_query.id, ) - ); + )?; Ok(()) }) .expect("print failed"); } else { - config.log_info(|_| { - println!( + config.log_info(|config| { + writeln!( + config.stdout(), "{}Description:{}\n{}\n{:>12} {}\n", Style::new().bold(), Reset, @@ -257,15 +262,20 @@ pub(super) fn run_check_release( crate_version!(), semver_query.id, ) - ); + )?; Ok(()) }) .expect("print failed"); } config - .log_info(|_| { - println!("{}Failed in:{}", Style::new().bold(), Reset); + .log_info(|config| { + writeln!( + config.stdout(), + "{}Failed in:{}", + Style::new().bold(), + Reset + )?; Ok(()) }) .expect("print failed"); @@ -283,28 +293,36 @@ pub(super) fn run_check_release( .context("Error instantiating semver query template.") .expect("could not materialize template"); config - .log_info(|_| { - println!(" {}", message); + .log_info(|config| { + writeln!(config.stdout(), " {}", message)?; Ok(()) }) .expect("print failed"); config - .log_extra_verbose(|_| { + .log_extra_verbose(|config| { let serde_pretty = serde_json::to_string_pretty(&pretty_result).expect("serde failed"); let indented_serde = serde_pretty .split('\n') .map(|line| format!(" {line}")) .join("\n"); - println!("\tlint rule output values:\n{}", indented_serde); + writeln!( + config.stdout(), + "\tlint rule output values:\n{}", + indented_serde + )?; Ok(()) }) .expect("print failed"); } else { config - .log_info(|_| { - println!("{}\n", serde_json::to_string_pretty(&pretty_result)?); + .log_info(|config| { + writeln!( + config.stdout(), + "{}\n", + serde_json::to_string_pretty(&pretty_result)? + )?; Ok(()) }) .expect("print failed"); diff --git a/src/config.rs b/src/config.rs index d80466583..0ac905629 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,13 @@ -use anstream::{eprint, eprintln}; +use anstream::AutoStream; use anstyle::{AnsiColor, Color, Reset, Style}; +use std::io::Write; use crate::templating::make_handlebars_registry; +// re-export this so users don't have to add the `anstream` crate directly +// just to set color choice +pub use anstream::ColorChoice; + #[allow(dead_code)] pub struct GlobalConfig { level: Option, @@ -11,6 +16,8 @@ pub struct GlobalConfig { /// /// This will be used to print an error if the user's rustc version is not high enough. minimum_rustc_version: semver::Version, + stdout: AutoStream>, + stderr: AutoStream>, } impl Default for GlobalConfig { @@ -21,10 +28,15 @@ impl Default for GlobalConfig { impl GlobalConfig { pub fn new() -> Self { + let stdout_choice = anstream::stdout().current_choice(); + let stderr_choice = anstream::stdout().current_choice(); + Self { level: None, handlebars: make_handlebars_registry(), minimum_rustc_version: semver::Version::new(1, 74, 0), + stdout: AutoStream::new(Box::new(std::io::stdout()), stdout_choice), + stderr: AutoStream::new(Box::new(std::io::stderr()), stderr_choice), } } @@ -106,14 +118,14 @@ impl GlobalConfig { justified: bool, ) -> anyhow::Result<()> { if self.is_info() { - eprint!("{}", Style::new().fg_color(Some(color)).bold()); + write!(self.stderr, "{}", Style::new().fg_color(Some(color)).bold())?; if justified { - eprint!("{status:>12}"); + write!(self.stderr, "{status:>12}")?; } else { - eprint!("{status}{}{}:", Reset, Style::new().bold()); + write!(self.stderr, "{status}{}{}:", Reset, Style::new().bold())?; } - eprintln!("{} {message}", Reset); + writeln!(self.stderr, "{Reset} {message}")?; } Ok(()) @@ -135,6 +147,91 @@ impl GlobalConfig { pub fn shell_warn(&mut self, message: impl std::fmt::Display) -> anyhow::Result<()> { self.shell_print("warning", message, Color::Ansi(AnsiColor::Yellow), false) } + + /// Gets the color-supporting `stdout` that the crate will use. + /// + /// See [`GlobalConfig::set_stdout`] and [`GlobalConfig::set_out_color_choice`] to + /// configure this stream + #[must_use] + #[inline] + pub fn stdout(&mut self) -> impl Write + '_ { + &mut self.stdout + } + + /// Gets the color-supporting `stderr` that the crate will use. + /// + /// See [`GlobalConfig::set_stderr`] and [`GlobalConfig::set_err_color_choice`] to + /// configure this stream + #[must_use] + #[inline] + pub fn stderr(&mut self) -> impl Write + '_ { + &mut self.stderr + } + + /// Sets the stderr output stream + /// + /// Defaults to the global color choice setting in [`ColorChoice::global`]. + /// Call [`GlobalConfig::set_err_color_choice`] to customize the color choice + pub fn set_stderr(&mut self, err: Box) { + self.stderr = AutoStream::new(err, ColorChoice::global()); + } + + /// Sets the stderr output stream + /// + /// Defaults to the global color choice setting in [`ColorChoice::global`]. + /// Call [`GlobalConfig::set_err_color_choice`] to customize the color choice + pub fn set_stdout(&mut self, out: Box) { + self.stdout = AutoStream::new(out, ColorChoice::global()); + } + + /// Individually set the color choice setting for [`GlobalConfig::stderr`] + /// + /// Defaults to the global color choice in [`ColorChoice::global`], which can be set + /// in [`ColorChoice::write_global`] if you are using the `anstream` crate. + /// + /// See also [`GlobalConfig::set_out_color_choice`] and [`GlobalConfig::set_color_choice`] + pub fn set_err_color_choice(&mut self, choice: ColorChoice) { + // TODO - `anstream` doesn't have a good mechanism to set color choice (on one stream) + // without making a new object, so we have to make a new autostream, but since we need + // to move the `RawStream` inner, we temporarily replace it with /dev/null + let stderr = std::mem::replace( + &mut self.stderr, + AutoStream::never(Box::new(std::io::sink())), + ); + self.stderr = AutoStream::new(stderr.into_inner(), choice); + } + + /// Individually set the color choice setting for [`GlobalConfig::stdout`] + /// + /// Defaults to the global color choice in [`ColorChoice::global`], which can be set + /// in [`ColorChoice::write_global`] if you are using the `anstream` crate. + /// + /// See also [`GlobalConfig::set_err_color_choice`] and [`GlobalConfig::set_color_choice`] + pub fn set_out_color_choice(&mut self, choice: ColorChoice) { + // TODO - `anstream` doesn't have a good mechanism to set color choice (on one stream) + // without making a new object, so we have to make a new autostream, but since we need + // to move the `RawStream` inner, we temporarily replace it with /dev/null + let stdout = std::mem::replace( + &mut self.stdout, + AutoStream::never(Box::new(std::io::sink())), + ); + self.stdout = AutoStream::new(stdout.into_inner(), choice); + } + + /// Sets the color choice for both [`GlobalConfig::stderr`] and [`GlobalConfig::stdout`] + /// + /// Defaults to the global color choice in [`ColorChoice::global`], which can be set + /// in [`ColorChoice::write_global`] if you are using the `anstream` crate. + /// + /// Prefer to use [`ColorChoice::write_global`] to avoid creating new stream objects if you + /// don't need to configure `cargo-semver-checks` colors differently than other crates + /// that use `anstream` for outputting colors. + /// + /// See also [`GlobalConfig::set_err_color_choice`] and [`GlobalConfig::set_out_color_choice`] + pub fn set_color_choice(&mut self, choice: ColorChoice) { + self.set_err_color_choice(choice); + self.set_out_color_choice(choice); + } } #[cfg(test)] diff --git a/src/main.rs b/src/main.rs index f48089bd0..44f7f9e2c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ fn main() { let Cargo::SemverChecks(args) = Cargo::parse(); - args.check_release.color_choice.write_global(); + args.color_choice.write_global(); if args.bugreport { use bugreport::{bugreport, collector::*, format::Markdown}; @@ -134,6 +134,10 @@ struct SemverChecks { #[command(subcommand)] command: Option, + + // docstring for help is on the `colorchoice_clap::Color` struct itself + #[command(flatten)] + color_choice: colorchoice_clap::Color, } /// Check your crate for semver violations. @@ -284,16 +288,9 @@ struct CheckRelease { #[arg(long = "target")] build_target: Option, + // docstring for help is on the `clap_verbosity_flag::Verbosity` struct itself #[command(flatten)] verbosity: clap_verbosity_flag::Verbosity, - - /// Whether to print colors to the terminal: - /// always, always-ansi (always use only ANSI color codes), - /// auto (based on whether output is a tty), and never - /// - /// Default is auto (use colors if output is a TTY, otherwise don't use colors) - #[command(flatten)] - color_choice: colorchoice_clap::Color, } impl From for cargo_semver_checks::Check { diff --git a/src/rustdoc_cmd.rs b/src/rustdoc_cmd.rs index 1af3a4741..ea5d7a1c3 100644 --- a/src/rustdoc_cmd.rs +++ b/src/rustdoc_cmd.rs @@ -1,3 +1,4 @@ +use std::io::Write as _; use std::path::{Path, PathBuf}; use anstream::ColorChoice; @@ -145,32 +146,47 @@ impl RustdocCommand { let output = cmd.output()?; if !output.status.success() { if self.silence { - config.log_error(|_| { + config.log_error(|config| { let delimiter = "-----"; - eprintln!("error: running cargo-doc on crate {crate_name} failed with output:"); - eprintln!( + writeln!( + config.stderr(), + "error: running cargo-doc on crate {crate_name} failed with output:" + )?; + writeln!( + config.stderr(), "{delimiter}\n{}\n{delimiter}\n", String::from_utf8_lossy(&output.stderr) - ); - eprintln!("error: failed to build rustdoc for crate {crate_name} v{version}"); + )?; + writeln!( + config.stderr(), + "error: failed to build rustdoc for crate {crate_name} v{version}" + )?; Ok(()) })?; } else { - config.log_error(|_| { - eprintln!( + config.log_error(|config| { + writeln!( + config.stderr(), "error: running cargo-doc on crate {crate_name} v{version} failed, see stderr output above" - ); + )?; Ok(()) })?; } config.log_error(|config| { let features = crate_source.feature_list_from_config(config, crate_data.feature_config); - eprintln!("note: this is usually due to a compilation error in the crate,"); - eprintln!(" and is unlikely to be a bug in cargo-semver-checks"); - eprintln!( + writeln!( + config.stderr(), + "note: this is usually due to a compilation error in the crate," + )?; + writeln!( + config.stderr(), + " and is unlikely to be a bug in cargo-semver-checks" + )?; + writeln!( + config.stderr(), "note: the following command can be used to reproduce the compilation error:" - ); + )?; let selector = match crate_source { CrateSource::Registry { version, .. } => format!("{crate_name}@={version}"), CrateSource::ManifestPath { manifest } => format!( @@ -188,14 +204,15 @@ impl RustdocCommand { } else { format!("--features {} ", features.into_iter().join(",")) }; - eprintln!( + writeln!( + config.stderr(), " \ cargo new --lib example && cd example && echo '[workspace]' >> Cargo.toml && cargo add {selector} --no-default-features {feature_list}&& cargo check\n" - ); + )?; Ok(()) })?; anyhow::bail!( @@ -231,23 +248,25 @@ cargo new --lib example && { None } else { - config.log_error(|_| { + config.log_error(|config| { let delimiter = "-----"; - eprintln!( + writeln!( + config.stderr(), "error: running cargo-config on crate {crate_name} failed with output:" - ); - eprintln!( + )?; + writeln!( + config.stderr(), "{delimiter}\n{}\n{delimiter}\n", String::from_utf8_lossy(&output.stderr) - ); + )?; - eprintln!("error: unexpected cargo config output for crate {crate_name} v{version}\n"); - eprintln!("note: this may be a bug in cargo, or a bug in cargo-semver-checks;"); - eprintln!(" if unsure, feel free to open a GitHub issue on cargo-semver-checks"); - eprintln!("note: running the following command on the crate should reproduce the error:"); - eprintln!( + writeln!(config.stderr(), "error: unexpected cargo config output for crate {crate_name} v{version}\n")?; + writeln!(config.stderr(), "note: this may be a bug in cargo, or a bug in cargo-semver-checks;")?; + writeln!(config.stderr(), " if unsure, feel free to open a GitHub issue on cargo-semver-checks")?; + writeln!(config.stderr(), "note: running the following command on the crate should reproduce the error:")?; + writeln!(config.stderr(), " cargo config -Zunstable-options get --format=json-value build.target\n", - ); + )?; Ok(()) })?; anyhow::bail!( @@ -395,14 +414,15 @@ fn create_placeholder_rustdoc_manifest( ..DependencyDetail::default() }, }; - config.log_verbose(|_| { + config.log_verbose(|config| { if project_with_features.features.is_empty() { return Ok(()); } - eprintln!( + writeln!( + config.stderr(), " Features: {}", project_with_features.features.join(","), - ); + )?; Ok(()) })?; let mut deps = DepsSet::new();