diff --git a/Cargo.lock b/Cargo.lock index a2551962..92e1b29c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -238,7 +238,7 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "commons" -version = "0.1.0" +version = "1.0.0" dependencies = [ "ascii_table", "byte-unit", @@ -247,6 +247,7 @@ dependencies = [ "filetime", "fs-err", "fs_extra", + "fun_run", "glob", "indoc", "lazy_static", @@ -261,7 +262,6 @@ dependencies = [ "thiserror", "toml", "walkdir", - "which_problem", ] [[package]] @@ -455,6 +455,17 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "fun_run" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cc80678e5122e242f9b98be01db39d7fe16cbc03e3cc30ff36442a46f0cd1a" +dependencies = [ + "lazy_static", + "regex", + "which_problem", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -515,6 +526,7 @@ dependencies = [ "commons", "flate2", "fs-err", + "fun_run", "glob", "indoc", "libcnb", diff --git a/buildpacks/ruby/CHANGELOG.md b/buildpacks/ruby/CHANGELOG.md index 47a05460..298b6fb8 100644 --- a/buildpacks/ruby/CHANGELOG.md +++ b/buildpacks/ruby/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- The `fun_run` commons library was moved to it's own crate () + ## [2.1.2] - 2023-10-31 ### Fixed diff --git a/buildpacks/ruby/Cargo.toml b/buildpacks/ruby/Cargo.toml index 0e1da065..b84ab44f 100644 --- a/buildpacks/ruby/Cargo.toml +++ b/buildpacks/ruby/Cargo.toml @@ -25,6 +25,7 @@ thiserror = "1" ureq = "2" url = "2" clap = { version = "4", features = ["derive"] } +fun_run = { version = "0.1", features = ["which_problem"] } [dev-dependencies] libcnb-test = "=0.15.0" diff --git a/buildpacks/ruby/src/bin/agentmon_loop.rs b/buildpacks/ruby/src/bin/agentmon_loop.rs index b5f31b87..c951f809 100644 --- a/buildpacks/ruby/src/bin/agentmon_loop.rs +++ b/buildpacks/ruby/src/bin/agentmon_loop.rs @@ -88,7 +88,7 @@ where let mut cmd = Command::new(path); cmd.args(args); - eprintln!("Running: {}", commons::fun_run::display(&mut cmd)); + eprintln!("Running: {}", fun_run::display(&mut cmd)); cmd.status() } diff --git a/buildpacks/ruby/src/bin/launch_daemon.rs b/buildpacks/ruby/src/bin/launch_daemon.rs index 18c4dec2..bb243b86 100644 --- a/buildpacks/ruby/src/bin/launch_daemon.rs +++ b/buildpacks/ruby/src/bin/launch_daemon.rs @@ -105,7 +105,7 @@ fn main() { command.status().unwrap_or_else(|error| { eprintln!( "Command failed {}. Details: {error}", - commons::fun_run::display(&mut command) + fun_run::display(&mut command) ); exit(1) }); diff --git a/buildpacks/ruby/src/gem_list.rs b/buildpacks/ruby/src/gem_list.rs index 50a41be0..12d3248f 100644 --- a/buildpacks/ruby/src/gem_list.rs +++ b/buildpacks/ruby/src/gem_list.rs @@ -1,10 +1,10 @@ -use commons::fun_run::{CmdError, CommandWithName}; use commons::gem_version::GemVersion; use commons::output::{ fmt, section_log::{log_step_timed, SectionLogger}, }; use core::str::FromStr; +use fun_run::{CmdError, CommandWithName}; use regex::Regex; use std::collections::HashMap; use std::ffi::OsStr; diff --git a/buildpacks/ruby/src/layers/bundle_download_layer.rs b/buildpacks/ruby/src/layers/bundle_download_layer.rs index 12867a8d..e874f2a8 100644 --- a/buildpacks/ruby/src/layers/bundle_download_layer.rs +++ b/buildpacks/ruby/src/layers/bundle_download_layer.rs @@ -5,8 +5,8 @@ use commons::output::{ use crate::RubyBuildpack; use crate::RubyBuildpackError; -use commons::fun_run::{self, CommandWithName}; use commons::gemfile_lock::ResolvedBundlerVersion; +use fun_run::{self, CommandWithName}; use libcnb::build::BuildContext; use libcnb::data::layer_content_metadata::LayerTypes; use libcnb::layer::{ExistingLayerStrategy, Layer, LayerData, LayerResult, LayerResultBuilder}; diff --git a/buildpacks/ruby/src/layers/bundle_install_layer.rs b/buildpacks/ruby/src/layers/bundle_install_layer.rs index cad034f7..9c1da4fe 100644 --- a/buildpacks/ruby/src/layers/bundle_install_layer.rs +++ b/buildpacks/ruby/src/layers/bundle_install_layer.rs @@ -4,13 +4,11 @@ use commons::output::{ }; use crate::{BundleWithout, RubyBuildpack, RubyBuildpackError}; -use commons::fun_run::CommandWithName; use commons::{ - display::SentenceList, - fun_run::{self, CmdError}, - gemfile_lock::ResolvedRubyVersion, - metadata_digest::MetadataDigest, + display::SentenceList, gemfile_lock::ResolvedRubyVersion, metadata_digest::MetadataDigest, }; +use fun_run::CommandWithName; +use fun_run::{self, CmdError}; use libcnb::{ build::BuildContext, data::{buildpack::StackId, layer_content_metadata::LayerTypes}, diff --git a/buildpacks/ruby/src/main.rs b/buildpacks/ruby/src/main.rs index ee5cc48b..8b372c26 100644 --- a/buildpacks/ruby/src/main.rs +++ b/buildpacks/ruby/src/main.rs @@ -2,13 +2,13 @@ #![warn(clippy::pedantic)] #![allow(clippy::module_name_repetitions)] use commons::cache::CacheError; -use commons::fun_run::CmdError; use commons::gemfile_lock::GemfileLock; use commons::metadata_digest::MetadataDigest; use commons::output::warn_later::WarnGuard; #[allow(clippy::wildcard_imports)] use commons::output::{build_log::*, fmt}; use core::str::FromStr; +use fun_run::CmdError; use layers::{ bundle_download_layer::{BundleDownloadLayer, BundleDownloadLayerMetadata}, bundle_install_layer::{BundleInstallLayer, BundleInstallLayerMetadata}, diff --git a/buildpacks/ruby/src/rake_task_detect.rs b/buildpacks/ruby/src/rake_task_detect.rs index 1782bc58..a094e90a 100644 --- a/buildpacks/ruby/src/rake_task_detect.rs +++ b/buildpacks/ruby/src/rake_task_detect.rs @@ -3,8 +3,8 @@ use commons::output::{ section_log::{log_step_timed, SectionLogger}, }; -use commons::fun_run::{CmdError, CommandWithName}; use core::str::FromStr; +use fun_run::{CmdError, CommandWithName}; use std::{ffi::OsStr, process::Command}; /// Run `rake -P` and parse output to show what rake tasks an application has diff --git a/buildpacks/ruby/src/steps/rake_assets_install.rs b/buildpacks/ruby/src/steps/rake_assets_install.rs index 685b0950..86296979 100644 --- a/buildpacks/ruby/src/steps/rake_assets_install.rs +++ b/buildpacks/ruby/src/steps/rake_assets_install.rs @@ -2,11 +2,11 @@ use crate::rake_task_detect::RakeDetect; use crate::RubyBuildpack; use crate::RubyBuildpackError; use commons::cache::{mib, AppCacheCollection, CacheConfig, KeepPath}; -use commons::fun_run::{self, CmdError, CommandWithName}; use commons::output::{ fmt::{self, HELP}, section_log::{log_step, log_step_stream, SectionLogger}, }; +use fun_run::{self, CmdError, CommandWithName}; use libcnb::build::BuildContext; use libcnb::Env; use std::process::Command; diff --git a/buildpacks/ruby/src/user_errors.rs b/buildpacks/ruby/src/user_errors.rs index d446574d..3a8a43ee 100644 --- a/buildpacks/ruby/src/user_errors.rs +++ b/buildpacks/ruby/src/user_errors.rs @@ -7,7 +7,7 @@ use commons::output::{ }; use crate::RubyBuildpackError; -use commons::fun_run::{CmdError, CommandWithName}; +use fun_run::{CmdError, CommandWithName}; use indoc::formatdoc; pub(crate) fn on_error(err: libcnb::Error) { diff --git a/commons/CHANGELOG.md b/commons/CHANGELOG.md new file mode 100644 index 00000000..f3d244b1 --- /dev/null +++ b/commons/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog for commons features + +## 1.0.0 + +### Changed + +- Move `fun_run` commons library to it's own crate () diff --git a/commons/Cargo.toml b/commons/Cargo.toml index ef70d574..a8448548 100644 --- a/commons/Cargo.toml +++ b/commons/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "commons" -version = "0.1.0" +version = "1.0.0" edition = "2021" publish = false @@ -26,9 +26,9 @@ sha2 = "0.10" tempfile = "3" thiserror = "1" walkdir = "2" -which_problem = "0.1" ascii_table = { version = "4", features = ["color_codes"] } const_format = "0.2" +fun_run = "0.1" [dev-dependencies] indoc = "2" diff --git a/commons/bin/print_style_guide.rs b/commons/bin/print_style_guide.rs index 53ccdecc..0552fbee 100644 --- a/commons/bin/print_style_guide.rs +++ b/commons/bin/print_style_guide.rs @@ -1,10 +1,10 @@ use ascii_table::AsciiTable; -use commons::fun_run::CommandWithName; use commons::output::fmt::{self, DEBUG_INFO, HELP}; use commons::output::{ build_log::*, section_log::{log_step, log_step_stream, log_step_timed}, }; +use fun_run::CommandWithName; use indoc::formatdoc; use std::io::stdout; use std::process::Command; diff --git a/commons/src/err.rs b/commons/src/err.rs deleted file mode 100644 index 2525864a..00000000 --- a/commons/src/err.rs +++ /dev/null @@ -1,57 +0,0 @@ -/// Allows -#[derive(Debug)] -pub(crate) struct IoErrorAnnotation { - source: std::io::Error, - annotation: String, -} - -impl IoErrorAnnotation { - pub(crate) fn new(source: std::io::Error, annotation: String) -> Self { - Self { source, annotation } - } - - pub(crate) fn into_io_error(self) -> std::io::Error { - std::io::Error::new(self.source.kind(), self) - } -} - -impl std::fmt::Display for IoErrorAnnotation { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln!(f, "{}", self.source)?; - f.write_str(&self.annotation)?; - Ok(()) - } -} - -impl std::error::Error for IoErrorAnnotation { - fn cause(&self) -> Option<&dyn std::error::Error> { - self.source() - } - - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(&self.source) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test() { - let io_error = std::io::Error::new( - std::io::ErrorKind::NotFound, - "Zoinks, I couldn't find that ", - ); - let wrapped: std::io::Error = IoErrorAnnotation::new( - io_error, - String::from("Debug details: it's just a villan in a mask"), - ) - .into_io_error(); - - assert_eq!( - "Zoinks, I couldn't find that \nDebug details: it's just a villan in a mask", - &format!("{wrapped}") - ); - } -} diff --git a/commons/src/fun_run.rs b/commons/src/fun_run.rs deleted file mode 100644 index cce1bbc7..00000000 --- a/commons/src/fun_run.rs +++ /dev/null @@ -1,574 +0,0 @@ -use lazy_static::lazy_static; -use libherokubuildpack::command::CommandExt; -use std::ffi::OsString; -use std::io::Write; -use std::os::unix::process::ExitStatusExt; -use std::process::Command; -use std::process::ExitStatus; -use std::process::Output; -use which_problem::Which; - -#[cfg(test)] -use libherokubuildpack as _; - -use crate::fun_run; - -/// The `fun_run` module is designed to make running commands more fun for you -/// and your users. -/// -/// Fun runs are easy to understand when they work, and easier to debug when -/// they fail. -/// -/// Fun runs make it easy to: -/// -/// - Advertise the command being run before execution -/// - Customize how commands are displayed -/// - Return error messages with the command name. -/// - Turn non-zero status results into an error -/// - Embed stdout and stderr into errors (when not streamed) -/// - Store stdout and stderr without displaying them (when streamed) -/// -/// Even better: -/// -/// - Composable by design. Use what you want. Ignore what you don't. -/// - Plays well with standard library types by default. -/// -/// And of course: -/// -/// - Fun(ctional) -/// -/// While the pieces can be composed functionally the real magic comes when you start mixing in the helper structs `NamedCommand`, `NamedOutput` and `CmdError`. -/// Together these will return a Result type that contains the associated name of the command just called: `Result`. -/// -/// Example: -/// -/// ```no_run -/// use commons::fun_run::CommandWithName; -/// use std::process::Command; -/// use libcnb::Env; -/// -/// let env = Env::new(); -/// -/// let result = Command::new("bundle") -/// .args(["install"]) -/// .envs(&env) -/// .stream_output(std::io::stdout(), std::io::stderr()); -/// -/// match result { -/// Ok(output) => { -/// assert_eq!("bundle install", &output.name()) -/// }, -/// Err(varient) => { -/// assert_eq!("bundle install", &varient.name()) -/// } -/// } -/// ``` -/// -/// Change names as you see fit: -/// -/// ```no_run -/// use commons::fun_run::CommandWithName; -/// use std::process::Command; -/// use libcnb::Env; -/// -/// let env = Env::new(); -/// -/// let result = Command::new("gem") -/// .args(["install", "bundler", "-v", "2.4.1.7"]) -/// .envs(&env) -/// // Overwrites default command name which would include extra arguments -/// .named("gem install") -/// .stream_output(std::io::stdout(), std::io::stderr()); -/// -/// match result { -/// Ok(output) => { -/// assert_eq!("bundle install", &output.name()) -/// }, -/// Err(varient) => { -/// assert_eq!("bundle install", &varient.name()) -/// } -/// } -/// ``` -/// -/// Or include env vars: -/// -/// ```no_run -/// use commons::fun_run::{self, CommandWithName}; -/// use std::process::Command; -/// use libcnb::Env; -/// -/// let env = Env::new(); -/// -/// let result = Command::new("gem") -/// .args(["install", "bundler", "-v", "2.4.1.7"]) -/// .envs(&env) -/// // Overwrites default command name -/// .named_fn(|cmd| { -/// // Annotate command with GEM_HOME env var -/// fun_run::display_with_env_keys(cmd, &env, ["GEM_HOME"]) -/// }) -/// .stream_output(std::io::stdout(), std::io::stderr()); -/// -/// match result { -/// Ok(output) => { -/// assert_eq!("GEM_HOME=\"/usr/bin/local/.gems\" gem install bundler -v 2.4.1.7", &output.name()) -/// }, -/// Err(varient) => { -/// assert_eq!("GEM_HOME=\"/usr/bin/local/.gems\" gem install bundler -v 2.4.1.7", &varient.name()) -/// } -/// } -/// ``` - -/// Allows for a functional-style flow when running a `Command` via -/// providing `cmd_map` -pub trait CmdMapExt -where - F: Fn(&mut Command) -> O, -{ - fn cmd_map(&mut self, f: F) -> O; -} - -impl CmdMapExt for Command -where - F: Fn(&mut Command) -> O, -{ - /// Acts like `Iterator.map` on a `Command` - /// - /// Yields its self and returns whatever output the block returns. - fn cmd_map(&mut self, f: F) -> O { - f(self) - } -} - -/// Easilly convert command output into a result with names -/// -/// Associated function name is experimental and may change -pub trait ResultNameExt { - /// # Errors - /// - /// Returns a `CmdError::SystemError` if the original Result was `Err`. - fn with_name(self, name: impl AsRef) -> Result; -} - -/// Convert the value of `Command::output()` into `Result` -impl ResultNameExt for Result { - /// # Errors - /// - /// Returns a `CmdError::SystemError` if the original Result was `Err`. - fn with_name(self, name: impl AsRef) -> Result { - let name = name.as_ref(); - self.map_err(|io_error| CmdError::SystemError(name.to_string(), io_error)) - .map(|output| NamedOutput { - name: name.to_string(), - output, - }) - } -} - -pub trait CommandWithName { - fn name(&mut self) -> String; - fn mut_cmd(&mut self) -> &mut Command; - - fn named(&mut self, s: impl AsRef) -> NamedCommand<'_> { - let name = s.as_ref().to_string(); - let command = self.mut_cmd(); - NamedCommand { name, command } - } - - #[allow(clippy::needless_lifetimes)] - fn named_fn<'a>(&'a mut self, f: impl FnOnce(&mut Command) -> String) -> NamedCommand<'a> { - let cmd = self.mut_cmd(); - let name = f(cmd); - self.named(name) - } - - /// Runs the command without streaming - /// - /// # Errors - /// - /// Returns `CmdError::SystemError` if the system is unable to run the command. - /// Returns `CmdError::NonZeroExitNotStreamed` if the exit code is not zero. - fn named_output(&mut self) -> Result { - let name = self.name(); - self.mut_cmd() - .output() - .with_name(name) - .and_then(NamedOutput::nonzero_captured) - } - - /// Runs the command and streams to the given writers - /// - /// # Errors - /// - /// Returns `CmdError::SystemError` if the system is unable to run the command - /// Returns `CmdError::NonZeroExitAlreadyStreamed` if the exit code is not zero. - fn stream_output( - &mut self, - stdout_write: OW, - stderr_write: EW, - ) -> Result - where - OW: Write + Send, - EW: Write + Send, - { - let name = &self.name(); - self.mut_cmd() - .output_and_write_streams(stdout_write, stderr_write) - .with_name(name) - .and_then(NamedOutput::nonzero_streamed) - } -} - -impl CommandWithName for Command { - fn name(&mut self) -> String { - fun_run::display(self) - } - - fn mut_cmd(&mut self) -> &mut Command { - self - } -} - -/// It's a command, with a name -pub struct NamedCommand<'a> { - name: String, - command: &'a mut Command, -} - -impl CommandWithName for NamedCommand<'_> { - fn name(&mut self) -> String { - self.name.to_string() - } - - fn mut_cmd(&mut self) -> &mut Command { - self.command - } -} - -/// Holds a the `Output` of a command's execution along with it's "name" -/// -/// When paired with `CmdError` a `Result` will retain the -/// "name" of the command regardless of succss or failure. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NamedOutput { - name: String, - output: Output, -} - -impl NamedOutput { - /// # Errors - /// - /// Returns an error if the status is not zero - pub fn nonzero_captured(self) -> Result { - nonzero_captured(self.name, self.output) - } - - /// # Errors - /// - /// Returns an error if the status is not zero - pub fn nonzero_streamed(self) -> Result { - nonzero_streamed(self.name, self.output) - } - - #[must_use] - pub fn status(&self) -> &ExitStatus { - &self.output.status - } - - #[must_use] - pub fn stdout_lossy(&self) -> String { - String::from_utf8_lossy(&self.output.stdout).to_string() - } - - #[must_use] - pub fn stderr_lossy(&self) -> String { - String::from_utf8_lossy(&self.output.stderr).to_string() - } - - #[must_use] - pub fn name(&self) -> String { - self.name.clone() - } -} - -impl AsRef for NamedOutput { - fn as_ref(&self) -> &Output { - &self.output - } -} - -impl From for Output { - fn from(value: NamedOutput) -> Self { - value.output - } -} - -lazy_static! { - // https://github.com/jimmycuadra/rust-shellwords/blob/d23b853a850ceec358a4137d5e520b067ddb7abc/src/lib.rs#L23 - static ref QUOTE_ARG_RE: regex::Regex = regex::Regex::new(r"([^A-Za-z0-9_\-.,:/@\n])").expect("Internal error:"); -} - -/// Converts a command and its arguments into a user readable string -/// -/// Example -/// -/// ```rust -/// use std::process::Command; -/// use commons::fun_run; -/// -/// let name = fun_run::display(Command::new("bundle").arg("install")); -/// assert_eq!(String::from("bundle install"), name); -/// ``` -#[must_use] -pub fn display(command: &mut Command) -> String { - vec![command.get_program().to_string_lossy().to_string()] - .into_iter() - .chain( - command - .get_args() - .map(std::ffi::OsStr::to_string_lossy) - .map(|arg| { - if QUOTE_ARG_RE.is_match(&arg) { - format!("{arg:?}") - } else { - format!("{arg}") - } - }), - ) - .collect::>() - .join(" ") -} - -/// Converts a command, arguments, and specified environment variables to user readable string -/// -/// -/// Example -/// -/// ```rust -/// use std::process::Command; -/// use commons::fun_run; -/// use libcnb::Env; -/// -/// let mut env = Env::new(); -/// env.insert("RAILS_ENV", "production"); - -/// -/// let mut command = Command::new("bundle"); -/// command.arg("install").envs(&env); -/// -/// let name = fun_run::display_with_env_keys(&mut command, &env, ["RAILS_ENV"]); -/// assert_eq!(String::from(r#"RAILS_ENV="production" bundle install"#), name); -/// ``` -#[must_use] -pub fn display_with_env_keys(cmd: &mut Command, env: E, keys: I) -> String -where - E: IntoIterator, - K: Into, - V: Into, - I: IntoIterator, - O: Into, -{ - let env = env - .into_iter() - .map(|(k, v)| (k.into(), v.into())) - .collect::>(); - - keys.into_iter() - .map(|key| { - let key = key.into(); - format!( - "{}={:?}", - key.to_string_lossy(), - env.get(&key).cloned().unwrap_or_else(|| OsString::from("")) - ) - }) - .chain([display(cmd)]) - .collect::>() - .join(" ") -} - -/// Adds diagnostic information to a `CmdError` using `which_problem` if error is `std::io::Error` -/// -/// This feature is experimental -pub fn map_which_problem( - error: CmdError, - cmd: &mut Command, - path_env: Option, -) -> CmdError { - match error { - CmdError::SystemError(name, error) => { - CmdError::SystemError(name, annotate_which_problem(error, cmd, path_env)) - } - CmdError::NonZeroExitNotStreamed(_) | CmdError::NonZeroExitAlreadyStreamed(_) => error, - } -} - -/// Adds diagnostic information to an `std::io::Error` using `which_problem` -/// -/// This feature is experimental -#[must_use] -pub fn annotate_which_problem( - error: std::io::Error, - cmd: &mut Command, - path_env: Option, -) -> std::io::Error { - let program = cmd.get_program().to_os_string(); - let current_working_dir = cmd.get_current_dir().map(std::path::Path::to_path_buf); - let problem = Which { - cwd: current_working_dir, - program, - path_env, - ..Which::default() - } - .diagnose(); - - let annotation = match problem { - Ok(details) => format!("\nSystem diagnostic information:\n\n{details}"), - Err(error) => format!("\nInternal error while gathering dianostic information:\n\n{error}"), - }; - - annotate_io_error(error, annotation) -} - -/// Returns an IO error that displays the given annotation starting on -/// the next line. -/// -/// Internal API used by `annotate_which_problem` -#[must_use] -fn annotate_io_error(source: std::io::Error, annotation: String) -> std::io::Error { - crate::err::IoErrorAnnotation::new(source, annotation).into_io_error() -} - -/// Who says (`Command`) errors can't be fun? -/// -/// Fun run errors include all the info a user needs to debug, like -/// the name of the command that failed and any outputs (like error messages -/// in stderr). -/// -/// Fun run errors don't overwhelm end users, so by default if stderr is already -/// streamed the output won't be duplicated. -/// -/// Enjoy if you want, skip if you don't. Fun run errors are not mandatory. -/// -/// Error output formatting is unstable -#[derive(Debug, thiserror::Error)] -#[allow(clippy::module_name_repetitions)] -pub enum CmdError { - #[error("Could not run command `{0}`. {1}")] - SystemError(String, std::io::Error), - - #[error("Command failed: `{cmd}`\nexit status: {status}\nstdout: {stdout}\nstderr: {stderr}", cmd = .0.name, status = .0.output.status.code().unwrap_or_else(|| 1), stdout = display_out_or_empty(&.0.output.stdout), stderr = display_out_or_empty(&.0.output.stderr))] - NonZeroExitNotStreamed(NamedOutput), - - #[error("Command failed: `{cmd}`\nexit status: {status}\nstdout: \nstderr: ", cmd = .0.name, status = .0.output.status.code().unwrap_or_else(|| 1))] - NonZeroExitAlreadyStreamed(NamedOutput), -} - -impl CmdError { - /// Returns a display representation of the command that failed - /// - /// Example: - /// - /// ```no_run - /// use commons::fun_run::{self, CmdMapExt, ResultNameExt}; - /// use std::process::Command; - /// - /// let result = Command::new("cat") - /// .arg("mouse.txt") - /// .cmd_map(|cmd| cmd.output().with_name(fun_run::display(cmd))); - /// - /// match result { - /// Ok(_) => todo!(), - /// Err(error) => assert_eq!(error.name().to_string(), "cat mouse.txt") - /// } - /// ``` - #[must_use] - pub fn name(&self) -> std::borrow::Cow<'_, str> { - match self { - CmdError::SystemError(name, _) => name.into(), - CmdError::NonZeroExitNotStreamed(out) | CmdError::NonZeroExitAlreadyStreamed(out) => { - out.name.as_str().into() - } - } - } -} - -impl From for NamedOutput { - fn from(value: CmdError) -> Self { - match value { - CmdError::SystemError(name, error) => NamedOutput { - name, - output: Output { - status: ExitStatus::from_raw(error.raw_os_error().unwrap_or(-1)), - stdout: Vec::new(), - stderr: error.to_string().into_bytes(), - }, - }, - CmdError::NonZeroExitNotStreamed(named) - | CmdError::NonZeroExitAlreadyStreamed(named) => named, - } - } -} - -fn display_out_or_empty(contents: &[u8]) -> String { - let contents = String::from_utf8_lossy(contents); - if contents.trim().is_empty() { - "".to_string() - } else { - contents.to_string() - } -} - -/// Converts a `std::io::Error` into a `CmdError` which includes the formatted command name -#[must_use] -pub fn on_system_error(name: String, error: std::io::Error) -> CmdError { - CmdError::SystemError(name, error) -} - -/// Converts an `Output` into an error when status is non-zero -/// -/// When calling a `Command` and streaming the output to stdout/stderr -/// it can be jarring to have the contents emitted again in the error. When this -/// error is displayed those outputs will not be repeated. -/// -/// Use when the `Output` comes from a source that was already streamed. -/// -/// To to include the results of stdout/stderr in the display of the error -/// use `nonzero_captured` instead. -/// -/// # Errors -/// -/// Returns Err when the `Output` status is non-zero -pub fn nonzero_streamed(name: String, output: impl Into) -> Result { - let output = output.into(); - if output.status.success() { - Ok(NamedOutput { name, output }) - } else { - Err(CmdError::NonZeroExitAlreadyStreamed(NamedOutput { - name, - output, - })) - } -} - -/// Converts an `Output` into an error when status is non-zero -/// -/// Use when the `Output` comes from a source that was not streamed -/// to stdout/stderr so it will be included in the error display by default. -/// -/// To avoid double printing stdout/stderr when streaming use `nonzero_streamed` -/// -/// # Errors -/// -/// Returns Err when the `Output` status is non-zero -pub fn nonzero_captured(name: String, output: impl Into) -> Result { - let output = output.into(); - if output.status.success() { - Ok(NamedOutput { name, output }) - } else { - Err(CmdError::NonZeroExitNotStreamed(NamedOutput { - name, - output, - })) - } -} diff --git a/commons/src/lib.rs b/commons/src/lib.rs index f415d2c9..4cede6f7 100644 --- a/commons/src/lib.rs +++ b/commons/src/lib.rs @@ -3,16 +3,15 @@ // Used in both testing and printing the style guide use indoc as _; + // Used in the style guide use ascii_table as _; +use fun_run as _; pub mod cache; pub mod display; -pub mod fun_run; pub mod gem_version; pub mod gemfile_lock; pub mod layer; pub mod metadata_digest; pub mod output; - -mod err; diff --git a/commons/src/output/section_log.rs b/commons/src/output/section_log.rs index a89fcf77..29ed49eb 100644 --- a/commons/src/output/section_log.rs +++ b/commons/src/output/section_log.rs @@ -75,7 +75,7 @@ pub fn log_step_timed(s: impl AsRef, f: impl FnOnce() -> T) -> T { /// to the output. The main use case is running commands /// /// ```no_run -/// use commons::fun_run::CommandWithName; +/// use fun_run::CommandWithName; /// use commons::output::section_log::log_step_stream; /// use commons::output::fmt; ///