From 525865c0acabfcee241cc5b11aa273fc13793898 Mon Sep 17 00:00:00 2001 From: Richard Schneeman Date: Tue, 14 Jan 2025 11:02:12 -0600 Subject: [PATCH] Remove deprecated interfaces (#385) * Remove deprecated interfaces * Force stdout and stderr to sync This output came from CI: ``` ** Invoke assets:precompile (first_time) START RAKE TEST OUTPUT $ echo $PATH ** Execute assets:precompile /layers/heroku_ruby/gems/ruby/3.2.0/bin:/workspace/bin:/layers/heroku_ruby/gems/bin:/layers/heroku_ruby/bundler/bin:/layers/heroku_ruby/binruby/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin $ which -a rake /layers/heroku_ruby/gems/ruby/3.2.0/bin/rake /workspace/bin/rake /layers/heroku_ruby/gems/bin/rake /layers/heroku_ruby/binruby/bin/rake /usr/bin/rake /bin/rake $ which -a ruby /layers/heroku_ruby/binruby/bin/ruby /usr/bin/ruby /bin/ruby END RAKE TEST OUTPUT ``` Which is ordered incorrectly. This forces stdout and stderr to emit immediately instead of buffering. --- Cargo.lock | 33 - buildpacks/ruby/tests/integration_test.rs | 3 + commons/CHANGELOG.md | 6 + commons/Cargo.toml | 7 - commons/src/layer.rs | 8 - commons/src/layer/configure_env_layer.rs | 128 ---- commons/src/layer/default_env_layer.rs | 87 --- commons/src/lib.rs | 1 - commons/src/output/background_timer.rs | 159 ----- commons/src/output/build_log.rs | 707 ---------------------- commons/src/output/fmt.rs | 342 ----------- commons/src/output/interface.rs | 54 -- commons/src/output/mod.rs | 13 - commons/src/output/section_log.rs | 116 ---- commons/src/output/util.rs | 186 ------ commons/src/output/warn_later.rs | 351 ----------- 16 files changed, 9 insertions(+), 2192 deletions(-) delete mode 100644 commons/src/layer/configure_env_layer.rs delete mode 100644 commons/src/layer/default_env_layer.rs delete mode 100644 commons/src/output/background_timer.rs delete mode 100644 commons/src/output/build_log.rs delete mode 100644 commons/src/output/fmt.rs delete mode 100644 commons/src/output/interface.rs delete mode 100644 commons/src/output/mod.rs delete mode 100644 commons/src/output/section_log.rs delete mode 100644 commons/src/output/util.rs delete mode 100644 commons/src/output/warn_later.rs diff --git a/Cargo.lock b/Cargo.lock index 609c89da..43f0510b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -293,19 +293,13 @@ dependencies = [ "bullet_stream", "byte-unit", "cache_diff", - "const_format", "fancy-regex", "filetime", "fs-err", "fs_extra", "glob", - "indoc", - "lazy_static", "libcnb", - "libcnb-test", - "libherokubuildpack", "magic_migrate", - "pretty_assertions", "regex", "serde", "sha2", @@ -315,26 +309,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "const_format" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" -dependencies = [ - "const_format_proc_macros", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - [[package]] name = "cpufeatures" version = "0.2.12" @@ -926,7 +900,6 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a8dd4cecc7b0cf175eb115cf112e7e6b6ff900be9308b83ba3d5f3c14e59bb8" dependencies = [ - "crossbeam-utils", "sha2", ] @@ -1651,12 +1624,6 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - [[package]] name = "untrusted" version = "0.9.0" diff --git a/buildpacks/ruby/tests/integration_test.rs b/buildpacks/ruby/tests/integration_test.rs index d4b95219..e6e2ce10 100644 --- a/buildpacks/ruby/tests/integration_test.rs +++ b/buildpacks/ruby/tests/integration_test.rs @@ -163,6 +163,9 @@ fn test_default_app_ubuntu20() { chmod_plus_x(&app_dir.join("bin").join("rake")).unwrap(); fs_err::write(app_dir.join("Rakefile"), r#" + STDOUT.sync = true + STDERR.sync = true + task "assets:precompile" do puts "START RAKE TEST OUTPUT" run!("echo $PATH") diff --git a/commons/CHANGELOG.md b/commons/CHANGELOG.md index c7e17924..42494214 100644 --- a/commons/CHANGELOG.md +++ b/commons/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog for commons features +## 2024-01-14 + +### Changed + +- Remove deprecated interfaces: `layer::ConfigureEnvLayer` and `layer::DefaultEnvLayer` and the `output` module (https://github.com/heroku/buildpacks-ruby/pull/385) + ## 2024-01-13 ### Added diff --git a/commons/Cargo.toml b/commons/Cargo.toml index bbfb0d44..cae74a13 100644 --- a/commons/Cargo.toml +++ b/commons/Cargo.toml @@ -8,18 +8,14 @@ workspace = true [dependencies] byte-unit = "5" -const_format = "0.2" # TODO: Consolidate on either the regex crate or the fancy-regex crate, since this repo currently uses both. fancy-regex = "0.14" fs_extra = "1" fs-err = "3" glob = "0.3" -indoc = "2" -lazy_static = "1" # libcnb has a much bigger impact on buildpack behaviour than any other dependencies, # so it's pinned to an exact version to isolate it from lockfile refreshes. libcnb = "=0.26.1" -libherokubuildpack = { version = "=0.26.1", default-features = false, features = ["command"] } regex = "1" serde = "1" sha2 = "0.10" @@ -33,8 +29,5 @@ cache_diff = "1.0" [dev-dependencies] filetime = "0.2" -indoc = "2" -libcnb-test = "=0.26.1" -pretty_assertions = "1" toml = "0.8" bullet_stream = "0.3.0" diff --git a/commons/src/layer.rs b/commons/src/layer.rs index 4ec3cade..ef802c98 100644 --- a/commons/src/layer.rs +++ b/commons/src/layer.rs @@ -1,9 +1 @@ -mod configure_env_layer; -mod default_env_layer; pub mod diff_migrate; - -#[deprecated(note = "Use the struct layer API in the latest libcnb.rs instead")] -pub use self::configure_env_layer::ConfigureEnvLayer; - -#[deprecated(note = "Use the struct layer API in the latest libcnb.rs instead")] -pub use self::default_env_layer::DefaultEnvLayer; diff --git a/commons/src/layer/configure_env_layer.rs b/commons/src/layer/configure_env_layer.rs deleted file mode 100644 index 9b293cca..00000000 --- a/commons/src/layer/configure_env_layer.rs +++ /dev/null @@ -1,128 +0,0 @@ -use libcnb::build::BuildContext; -use libcnb::data::layer_content_metadata::LayerTypes; -use libcnb::generic::GenericMetadata; -#[allow(deprecated)] -use libcnb::layer::{Layer, LayerResult, LayerResultBuilder}; -use libcnb::layer_env::LayerEnv; -use std::marker::PhantomData; -use std::path::Path; - -/// Set environment variables -/// -/// If you want to set many default environment variables you can use -/// `DefaultEnvLayer`. If you need to set different types of environment -/// variables you can use this struct `ConfigureEnvLayer` -/// -/// Example: -/// -/// ```rust -///# use libcnb::build::{BuildContext, BuildResult, BuildResultBuilder}; -///# use libcnb::data::launch::{LaunchBuilder, ProcessBuilder}; -///# use libcnb::data::process_type; -///# use libcnb::detect::{DetectContext, DetectResult, DetectResultBuilder}; -///# use libcnb::generic::{GenericError, GenericMetadata, GenericPlatform}; -///# use libcnb::{buildpack_main, Buildpack}; -///# use libcnb::data::layer::LayerName; -/// -///# pub(crate) struct HelloWorldBuildpack; -/// -/// use libcnb::Env; -/// use libcnb::data::layer_name; -/// use libcnb::layer_env::{LayerEnv, ModificationBehavior, Scope}; -/// use commons::layer::ConfigureEnvLayer; -/// -///# impl Buildpack for HelloWorldBuildpack { -///# type Platform = GenericPlatform; -///# type Metadata = GenericMetadata; -///# type Error = GenericError; -/// -///# fn detect(&self, _context: DetectContext) -> libcnb::Result { -///# todo!() -///# } -/// -///# fn build(&self, context: BuildContext) -> libcnb::Result { -/// let env = Env::from_current(); -/// // Don't forget to apply context.platform.env() too; -/// -/// let layer = context // -/// .handle_layer( -/// layer_name!("configure_env"), -/// ConfigureEnvLayer::new( -/// LayerEnv::new() -/// .chainable_insert( -/// Scope::All, -/// ModificationBehavior::Override, -/// "BUNDLE_GEMFILE", // Tells bundler where to find the `Gemfile` -/// context.app_dir.join("Gemfile"), -/// ) -/// .chainable_insert( -/// Scope::All, -/// ModificationBehavior::Override, -/// "BUNDLE_CLEAN", // After successful `bundle install` bundler will automatically run `bundle clean` -/// "1", -/// ) -/// .chainable_insert( -/// Scope::All, -/// ModificationBehavior::Override, -/// "BUNDLE_DEPLOYMENT", // Requires the `Gemfile.lock` to be in sync with the current `Gemfile`. -/// "1", -/// ) -/// .chainable_insert( -/// Scope::All, -/// ModificationBehavior::Default, -/// "MY_ENV_VAR", -/// "Whatever I want" -/// ) -/// ), -/// )?; -/// let env = layer.env.apply(Scope::Build, &env); -/// -///# todo!() -///# } -///# } -/// -/// ``` -pub struct ConfigureEnvLayer { - pub(crate) data: LayerEnv, - pub(crate) _buildpack: std::marker::PhantomData, -} - -impl ConfigureEnvLayer -where - B: libcnb::Buildpack, -{ - #[must_use] - pub fn new(env: LayerEnv) -> Self { - ConfigureEnvLayer { - data: env, - _buildpack: PhantomData, - } - } -} - -#[allow(deprecated)] -impl Layer for ConfigureEnvLayer -where - B: libcnb::Buildpack, -{ - type Buildpack = B; - type Metadata = GenericMetadata; - - fn types(&self) -> LayerTypes { - LayerTypes { - build: true, - launch: true, - cache: false, - } - } - - fn create( - &mut self, - _context: &BuildContext, - _layer_path: &Path, - ) -> Result, B::Error> { - LayerResultBuilder::new(GenericMetadata::default()) - .env(self.data.clone()) - .build() - } -} diff --git a/commons/src/layer/default_env_layer.rs b/commons/src/layer/default_env_layer.rs deleted file mode 100644 index fd3a5081..00000000 --- a/commons/src/layer/default_env_layer.rs +++ /dev/null @@ -1,87 +0,0 @@ -use libcnb::layer_env::{LayerEnv, ModificationBehavior, Scope}; -use std::ffi::OsString; -use std::marker::PhantomData; - -use super::ConfigureEnvLayer; - -/// Set default environment variables -/// -/// If all you need to do is set default environment values, you can use -/// the `DefaultEnvLayer::new` function to set those values without having -/// to create a struct from scratch. -/// -/// ```rust -///# use libcnb::build::{BuildContext, BuildResult, BuildResultBuilder}; -///# use libcnb::data::launch::{LaunchBuilder, ProcessBuilder}; -///# use libcnb::data::process_type; -///# use libcnb::detect::{DetectContext, DetectResult, DetectResultBuilder}; -///# use libcnb::generic::{GenericError, GenericMetadata, GenericPlatform}; -///# use libcnb::{buildpack_main, Buildpack}; -///# use libcnb::data::layer::LayerName; -/// -///# pub(crate) struct HelloWorldBuildpack; -/// -/// use libcnb::Env; -/// use libcnb::data::layer_name; -/// use libcnb::layer_env::Scope; -/// use commons::layer::DefaultEnvLayer; -/// -///# impl Buildpack for HelloWorldBuildpack { -///# type Platform = GenericPlatform; -///# type Metadata = GenericMetadata; -///# type Error = GenericError; -/// -///# fn detect(&self, _context: DetectContext) -> libcnb::Result { -///# todo!() -///# } -/// -///# fn build(&self, context: BuildContext) -> libcnb::Result { -/// let env = Env::from_current(); -/// // Don't forget to apply context.platform.env() too; -/// -/// let layer = context // -/// .handle_layer( -/// layer_name!("default_env"), -/// DefaultEnvLayer::new( -/// [ -/// ("JRUBY_OPTS", "-Xcompile.invokedynamic=false"), -/// ("RACK_ENV", "production"), -/// ("RAILS_ENV", "production"), -/// ("RAILS_SERVE_STATIC_FILES", "enabled"), -/// ("RAILS_LOG_TO_STDOUT", "enabled"), -/// ("MALLOC_ARENA_MAX", "2"), -/// ("DISABLE_SPRING", "1"), -/// ] -/// .into_iter(), -/// ), -/// )?; -/// let env = layer.env.apply(Scope::Build, &env); -/// -///# todo!() -///# } -///# } -/// -/// ``` -pub struct DefaultEnvLayer; - -impl DefaultEnvLayer { - #[allow(clippy::new_ret_no_self)] - pub fn new(env: E) -> ConfigureEnvLayer - where - E: IntoIterator + Clone, - K: Into, - V: Into, - B: libcnb::Buildpack, - { - let mut layer_env = LayerEnv::new(); - for (key, value) in env { - layer_env = - layer_env.chainable_insert(Scope::All, ModificationBehavior::Default, key, value); - } - - ConfigureEnvLayer { - data: layer_env, - _buildpack: PhantomData, - } - } -} diff --git a/commons/src/lib.rs b/commons/src/lib.rs index 27dc68ce..d603afc1 100644 --- a/commons/src/lib.rs +++ b/commons/src/lib.rs @@ -4,4 +4,3 @@ pub mod gem_version; pub mod gemfile_lock; pub mod layer; pub mod metadata_digest; -pub mod output; diff --git a/commons/src/output/background_timer.rs b/commons/src/output/background_timer.rs deleted file mode 100644 index f768644d..00000000 --- a/commons/src/output/background_timer.rs +++ /dev/null @@ -1,159 +0,0 @@ -//! This module is responsible for the logic involved in the printing to output while -//! other work is being performed. -//! -use std::io::Write; -use std::sync::mpsc::Sender; -use std::sync::{mpsc, Arc, Mutex}; -use std::thread::JoinHandle; -use std::time::{Duration, Instant}; - -/// Prints a start, then a tick every second, and an end to the given `Write` value. -/// -/// Returns a struct that allows for manually stopping the timer or will automatically stop -/// the timer if the guard is dropped. This functionality allows for errors that trigger -/// an exit of the function to not accidentally have a timer printing in the background -/// forever. -pub(crate) fn start_timer( - arc_io: &Arc>, - tick_duration: Duration, - start: impl AsRef, - tick: impl AsRef, - end: impl AsRef, -) -> StopJoinGuard -where - // The 'static lifetime means as long as something holds a reference to it, nothing it references - // will go away. - // - // From https://users.rust-lang.org/t/why-does-thread-spawn-need-static-lifetime-for-generic-bounds/4541 - // - // [lifetimes] refer to the minimum possible lifetime of any borrowed references that the object contains. - T: Write + Send + Sync + 'static, -{ - let instant = Instant::now(); - let (sender, receiver) = mpsc::channel::<()>(); - let start = start.as_ref().to_string(); - let tick = tick.as_ref().to_string(); - let end = end.as_ref().to_string(); - - let arc_io = arc_io.clone(); - let handle = std::thread::spawn(move || { - // TODO: Remove usage of unwrap(): https://github.com/heroku/buildpacks-ruby/issues/238 - #[allow(clippy::unwrap_used)] - let mut io = arc_io.lock().unwrap(); - write!(&mut io, "{start}").expect("Internal error"); - io.flush().expect("Internal error"); - loop { - write!(&mut io, "{tick}").expect("Internal error"); - io.flush().expect("Internal error"); - - if receiver.recv_timeout(tick_duration).is_ok() { - write!(&mut io, "{end}").expect("Internal error"); - io.flush().expect("Internal error"); - break; - } - } - }); - - StopJoinGuard { - inner: Some(StopTimer { - handle: Some(handle), - sender: Some(sender), - instant, - }), - } -} - -/// Responsible for stopping a running timer thread -#[derive(Debug)] -pub(crate) struct StopTimer { - instant: Instant, - handle: Option>, - sender: Option>, -} - -impl StopTimer { - pub(crate) fn elapsed(&self) -> Duration { - self.instant.elapsed() - } -} - -pub(crate) trait StopJoin: std::fmt::Debug { - fn stop_join(self) -> Self; -} - -impl StopJoin for StopTimer { - fn stop_join(mut self) -> Self { - if let Some(inner) = self.sender.take() { - inner.send(()).expect("Internal error"); - } - - if let Some(inner) = self.handle.take() { - inner.join().expect("Internal error"); - } - - self - } -} - -// Guarantees that stop is called on the inner -#[derive(Debug)] -pub(crate) struct StopJoinGuard { - inner: Option, -} - -impl StopJoinGuard { - /// Since this consumes self and `stop_join` consumes - /// the inner, the option will never be empty unless - /// it was created with a None inner. - /// - /// Since inner is private we guarantee it's always Some - /// until this struct is consumed. - pub(crate) fn stop(mut self) -> T { - self.inner - .take() - .map(StopJoin::stop_join) - .expect("Internal error: Should never panic, codepath tested") - } -} - -impl Drop for StopJoinGuard { - fn drop(&mut self) { - if let Some(inner) = self.inner.take() { - inner.stop_join(); - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::output::util::ReadYourWrite; - use libcnb_test::assert_contains; - use std::thread::sleep; - - #[test] - fn does_stop_does_not_panic() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - let done = start_timer(&writer.arc_io(), Duration::from_millis(1), " .", ".", ". "); - - let _ = done.stop(); - - assert_contains!(String::from_utf8_lossy(&reader.lock().unwrap()), " ... "); - } - - #[test] - fn test_drop_stops_timer() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - let done = start_timer(&writer.arc_io(), Duration::from_millis(1), " .", ".", ". "); - - drop(done); - sleep(Duration::from_millis(2)); - - let before = String::from_utf8_lossy(&reader.lock().unwrap()).to_string(); - sleep(Duration::from_millis(100)); - let after = String::from_utf8_lossy(&reader.lock().unwrap()).to_string(); - assert_eq!(before, after); - } -} diff --git a/commons/src/output/build_log.rs b/commons/src/output/build_log.rs deleted file mode 100644 index 888d0920..00000000 --- a/commons/src/output/build_log.rs +++ /dev/null @@ -1,707 +0,0 @@ -use crate::output::background_timer::{start_timer, StopJoinGuard, StopTimer}; -#[allow(deprecated)] -use crate::output::fmt; -#[allow(clippy::wildcard_imports)] -#[allow(deprecated)] -pub use crate::output::interface::*; -use std::fmt::Debug; -use std::io::Write; -use std::marker::PhantomData; -use std::sync::{Arc, Mutex}; -use std::time::{Duration, Instant}; - -/// # Build output logging -/// -/// Use the `BuildLog` to output structured text as a buildpack is executing -/// -/// ``` -/// use commons::output::build_log::*; -/// -/// let mut logger = BuildLog::new(std::io::stdout()) -/// .buildpack_name("Heroku Ruby Buildpack"); -/// -/// logger = logger -/// .section("Ruby version") -/// .step_timed("Installing") -/// .finish_timed_step() -/// .end_section(); -/// -/// logger.finish_logging(); -/// ``` -/// -/// To log inside of a layer see [`section_log`]. - -#[derive(Debug)] -#[deprecated(note = "Use `bullet_stream` instead")] -pub struct BuildLog { - pub(crate) io: W, - pub(crate) data: BuildData, - pub(crate) state: PhantomData, -} - -/// A bag of data passed throughout the lifecycle of a `BuildLog` -#[derive(Debug)] -pub(crate) struct BuildData { - pub(crate) started: Instant, -} - -impl Default for BuildData { - fn default() -> Self { - Self { - started: Instant::now(), - } - } -} - -/// Various states for `BuildLog` to contain -/// -/// The `BuildLog` struct acts as a logging state machine. These structs -/// are meant to represent those states -pub(crate) mod state { - #[derive(Debug)] - pub struct NotStarted; - - #[derive(Debug)] - pub struct Started; - - #[derive(Debug)] - pub struct InSection; -} - -impl BuildLog -where - W: Write + Debug, -{ - pub fn new(io: W) -> Self { - Self { - io, - state: PhantomData::, - data: BuildData::default(), - } - } -} - -impl Logger for BuildLog -where - W: Write + Send + Sync + Debug + 'static, -{ - fn buildpack_name(mut self, buildpack_name: &str) -> Box { - write_now(&mut self.io, format!("{}\n\n", fmt::header(buildpack_name))); - - Box::new(BuildLog { - io: self.io, - data: self.data, - state: PhantomData::, - }) - } - - fn without_buildpack_name(self) -> Box { - Box::new(BuildLog { - io: self.io, - data: self.data, - state: PhantomData::, - }) - } -} - -impl StartedLogger for BuildLog -where - W: Write + Send + Sync + Debug + 'static, -{ - fn section(mut self: Box, s: &str) -> Box { - writeln_now(&mut self.io, fmt::section(s)); - - Box::new(BuildLog { - io: self.io, - data: self.data, - state: PhantomData::, - }) - } - - fn finish_logging(mut self: Box) { - let elapsed = fmt::time::human(&self.data.started.elapsed()); - let details = fmt::details(format!("finished in {elapsed}")); - - writeln_now(&mut self.io, fmt::section(format!("Done {details}"))); - } - - fn announce(self: Box) -> Box>> { - Box::new(AnnounceBuildLog { - io: self.io, - data: self.data, - state: PhantomData::, - leader: Some("\n".to_string()), - }) - } -} -impl SectionLogger for BuildLog -where - W: Write + Send + Sync + Debug + 'static, -{ - fn mut_step(&mut self, s: &str) { - writeln_now(&mut self.io, fmt::step(s)); - } - - fn step(mut self: Box, s: &str) -> Box { - self.mut_step(s); - - Box::new(BuildLog { - io: self.io, - state: PhantomData::, - data: self.data, - }) - } - - fn step_timed(self: Box, s: &str) -> Box { - let start = fmt::step(format!("{s}{}", fmt::background_timer_start())); - let tick = fmt::background_timer_tick(); - let end = fmt::background_timer_end(); - - let arc_io = Arc::new(Mutex::new(self.io)); - let background = start_timer(&arc_io, Duration::from_secs(1), start, tick, end); - - Box::new(FinishTimedStep { - arc_io, - background, - data: self.data, - }) - } - - fn step_timed_stream(mut self: Box, s: &str) -> Box { - self.mut_step(s); - - let started = Instant::now(); - let arc_io = Arc::new(Mutex::new(self.io)); - let mut stream = StreamTimed { - arc_io, - data: self.data, - started, - }; - stream.start(); - - Box::new(stream) - } - - fn end_section(self: Box) -> Box { - Box::new(BuildLog { - io: self.io, - data: self.data, - state: PhantomData::, - }) - } - - fn announce(self: Box) -> Box>> { - Box::new(AnnounceBuildLog { - io: self.io, - data: self.data, - state: PhantomData::, - leader: Some("\n".to_string()), - }) - } -} - -// Store internal state, print leading character exactly once on warning or important -#[derive(Debug)] -struct AnnounceBuildLog -where - W: Write + Send + Sync + Debug + 'static, -{ - io: W, - data: BuildData, - state: PhantomData, - leader: Option, -} - -impl AnnounceBuildLog -where - T: Debug, - W: Write + Send + Sync + Debug + 'static, -{ - fn log_warning_shared(&mut self, s: &str) { - if let Some(leader) = self.leader.take() { - write_now(&mut self.io, leader); - } - - writeln_now(&mut self.io, fmt::warning(s.trim())); - writeln_now(&mut self.io, ""); - } - - fn log_important_shared(&mut self, s: &str) { - if let Some(leader) = self.leader.take() { - write_now(&mut self.io, leader); - } - writeln_now(&mut self.io, fmt::important(s.trim())); - writeln_now(&mut self.io, ""); - } - - fn log_warn_later_shared(&mut self, s: &str) { - let mut formatted = fmt::warning(s.trim()); - formatted.push('\n'); - - match crate::output::warn_later::try_push(formatted) { - Ok(()) => {} - Err(error) => { - eprintln!("[Buildpack Warning]: Cannot use the delayed warning feature due to error: {error}"); - self.log_warning_shared(s); - } - }; - } -} - -impl ErrorLogger for AnnounceBuildLog -where - T: Debug, - W: Write + Send + Sync + Debug + 'static, -{ - fn error(mut self: Box, s: &str) { - if let Some(leader) = self.leader.take() { - write_now(&mut self.io, leader); - } - writeln_now(&mut self.io, fmt::error(s.trim())); - writeln_now(&mut self.io, ""); - } -} - -impl AnnounceLogger for AnnounceBuildLog -where - W: Write + Send + Sync + Debug + 'static, -{ - type ReturnTo = Box; - - fn warning(mut self: Box, s: &str) -> Box> { - self.log_warning_shared(s); - - self - } - - fn warn_later( - mut self: Box, - s: &str, - ) -> Box> { - self.log_warn_later_shared(s); - - self - } - - fn important( - mut self: Box, - s: &str, - ) -> Box> { - self.log_important_shared(s); - - self - } - - fn end_announce(self: Box) -> Box { - Box::new(BuildLog { - io: self.io, - data: self.data, - state: PhantomData::, - }) - } -} - -impl AnnounceLogger for AnnounceBuildLog -where - W: Write + Send + Sync + Debug + 'static, -{ - type ReturnTo = Box; - - fn warning(mut self: Box, s: &str) -> Box> { - self.log_warning_shared(s); - self - } - - fn warn_later( - mut self: Box, - s: &str, - ) -> Box> { - self.log_warn_later_shared(s); - self - } - - fn important( - mut self: Box, - s: &str, - ) -> Box> { - self.log_important_shared(s); - self - } - - fn end_announce(self: Box) -> Box { - Box::new(BuildLog { - io: self.io, - data: self.data, - state: PhantomData::, - }) - } -} - -/// Implements Box -/// -/// Ensures that the `W` can be passed across thread boundries -/// by wrapping in a mutex. -/// -/// It implements writing by unlocking and delegating to the internal writer. -/// Can be used for `Box::io()` -#[derive(Debug)] -struct LockedWriter { - arc: Arc>, -} - -impl Write for LockedWriter -where - W: Write + Send + Sync + Debug + 'static, -{ - fn write(&mut self, buf: &[u8]) -> std::io::Result { - let mut io = self.arc.lock().expect("Logging mutex poisoned"); - io.write(buf) - } - - fn flush(&mut self) -> std::io::Result<()> { - let mut io = self.arc.lock().expect("Logging mutex poisoned"); - io.flush() - } -} - -/// Used to implement `Box` interface -/// -/// Mostly used for logging a running command -#[derive(Debug)] -struct StreamTimed { - data: BuildData, - arc_io: Arc>, - started: Instant, -} - -impl StreamTimed -where - W: Write + Send + Sync + Debug, -{ - fn start(&mut self) { - let mut guard = self.arc_io.lock().expect("Logging mutex posioned"); - let mut io = guard.by_ref(); - // Newline before stream - writeln_now(&mut io, ""); - } -} - -// Need a trait that is both write a debug -trait WriteDebug: Write + Debug {} -impl WriteDebug for T where T: Write + Debug {} - -/// Attempt to unwrap an io inside of an `Arc` if this fails because there is more -/// than a single reference don't panic, return the original IO instead. -/// -/// This prevents a runtime panic and allows us to continue logging -fn try_unwrap_arc_io(arc_io: Arc>) -> Box -where - W: Write + Send + Sync + Debug + 'static, -{ - match Arc::try_unwrap(arc_io) { - Ok(mutex) => Box::new(mutex.into_inner().expect("Logging mutex was poisioned")), - Err(original) => Box::new(LockedWriter { arc: original }), - } -} - -impl StreamLogger for StreamTimed -where - W: Write + Send + Sync + Debug + 'static, -{ - /// Yield boxed writer that can be used for formatting and streaming contents - /// back to the logger. - fn io(&mut self) -> Box { - Box::new(libherokubuildpack::write::line_mapped( - LockedWriter { - arc: self.arc_io.clone(), - }, - fmt::cmd_stream_format, - )) - } - - fn finish_timed_stream(self: Box) -> Box { - let duration = self.started.elapsed(); - let mut io = try_unwrap_arc_io(self.arc_io); - - // // Newline after stream - writeln_now(&mut io, ""); - - let mut section = BuildLog { - io, - data: self.data, - state: PhantomData::, - }; - - section.mut_step(&format!( - "Done {}", - fmt::details(fmt::time::human(&duration)) - )); - - Box::new(section) - } -} - -/// Implements `Box` -/// -/// Used to end a background inline timer i.e. Installing ...... (<0.1s) -#[derive(Debug)] -struct FinishTimedStep { - data: BuildData, - arc_io: Arc>, - background: StopJoinGuard, -} - -impl TimedStepLogger for FinishTimedStep -where - W: Write + Send + Sync + Debug + 'static, -{ - fn finish_timed_step(self: Box) -> Box { - // Must stop background writing thread before retrieving IO - let duration = self.background.stop().elapsed(); - let mut io = try_unwrap_arc_io(self.arc_io); - - writeln_now(&mut io, fmt::details(fmt::time::human(&duration))); - - Box::new(BuildLog { - io, - data: self.data, - state: PhantomData::, - }) - } -} - -/// Internal helper, ensures that all contents are always flushed (never buffered) -/// -/// This is especially important for writing individual characters to the same line -fn write_now(destination: &mut D, msg: impl AsRef) { - write!(destination, "{}", msg.as_ref()).expect("Logging error: UI writer closed"); - - destination - .flush() - .expect("Logging error: UI writer closed"); -} - -/// Internal helper, ensures that all contents are always flushed (never buffered) -fn writeln_now(destination: &mut D, msg: impl AsRef) { - writeln!(destination, "{}", msg.as_ref()).expect("Logging error: UI writer closed"); - - destination - .flush() - .expect("Logging error: UI writer closed"); -} - -#[cfg(test)] -mod test { - use super::*; - use crate::output::fmt::{self, strip_control_codes}; - use crate::output::util::{strip_trailing_whitespace, ReadYourWrite}; - use crate::output::warn_later::WarnGuard; - use indoc::formatdoc; - use libcnb_test::assert_contains; - use libherokubuildpack::command::CommandExt; - use pretty_assertions::assert_eq; - - #[test] - fn test_captures() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - - let mut stream = BuildLog::new(writer) - .buildpack_name("Heroku Ruby Buildpack") - .section("Ruby version `3.1.3` from `Gemfile.lock`") - .step_timed("Installing") - .finish_timed_step() - .end_section() - .section("Hello world") - .step_timed_stream("Streaming stuff"); - - let value = "stuff".to_string(); - writeln!(stream.io(), "{value}").unwrap(); - - stream.finish_timed_stream().end_section().finish_logging(); - - let expected = formatdoc! {" - - # Heroku Ruby Buildpack - - - Ruby version `3.1.3` from `Gemfile.lock` - - Installing ... (< 0.1s) - - Hello world - - Streaming stuff - - stuff - - - Done (< 0.1s) - - Done (finished in < 0.1s) - "}; - - assert_eq!( - expected, - strip_trailing_whitespace(fmt::strip_control_codes(reader.read_lossy().unwrap())) - ); - } - - #[test] - fn test_streaming_a_command() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - - let mut stream = BuildLog::new(writer) - .buildpack_name("Streaming buildpack demo") - .section("Command streaming") - .step_timed_stream("Streaming stuff"); - - std::process::Command::new("echo") - .arg("hello world") - .output_and_write_streams(stream.io(), stream.io()) - .unwrap(); - - stream.finish_timed_stream().end_section().finish_logging(); - - let actual = - strip_trailing_whitespace(fmt::strip_control_codes(reader.read_lossy().unwrap())); - - assert_contains!(actual, " hello world\n"); - } - - #[test] - fn warning_step_padding() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - - BuildLog::new(writer) - .buildpack_name("RCT") - .section("Guest thoughs") - .step("The scenery here is wonderful") - .announce() - .warning("It's too crowded here\nI'm tired") - .end_announce() - .step("The jumping fountains are great") - .step("The music is nice here") - .end_section() - .finish_logging(); - - let expected = formatdoc! {" - - # RCT - - - Guest thoughs - - The scenery here is wonderful - - ! It's too crowded here - ! I'm tired - - - The jumping fountains are great - - The music is nice here - - Done (finished in < 0.1s) - "}; - - assert_eq!(expected, strip_control_codes(reader.read_lossy().unwrap())); - } - - #[test] - fn double_warning_step_padding() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - - let logger = BuildLog::new(writer) - .buildpack_name("RCT") - .section("Guest thoughs") - .step("The scenery here is wonderful") - .announce(); - - logger - .warning("It's too crowded here") - .warning("I'm tired") - .end_announce() - .step("The jumping fountains are great") - .step("The music is nice here") - .end_section() - .finish_logging(); - - let expected = formatdoc! {" - - # RCT - - - Guest thoughs - - The scenery here is wonderful - - ! It's too crowded here - - ! I'm tired - - - The jumping fountains are great - - The music is nice here - - Done (finished in < 0.1s) - "}; - - assert_eq!(expected, strip_control_codes(reader.read_lossy().unwrap())); - } - - #[test] - fn warn_later_doesnt_output_newline() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - - let warn_later = WarnGuard::new(writer.clone()); - BuildLog::new(writer) - .buildpack_name("Walkin' on the Sun") - .section("So don't delay, act now, supplies are running out") - .step("Allow if you're still alive, six to eight years to arrive") - .step("And if you follow, there may be a tomorrow") - .announce() - .warn_later("And all that glitters is gold") - .warn_later("Only shooting stars break the mold") - .end_announce() - .step("But if the offer's shunned") - .step("You might as well be walking on the Sun") - .end_section() - .finish_logging(); - - drop(warn_later); - - let expected = formatdoc! {" - - # Walkin' on the Sun - - - So don't delay, act now, supplies are running out - - Allow if you're still alive, six to eight years to arrive - - And if you follow, there may be a tomorrow - - But if the offer's shunned - - You might as well be walking on the Sun - - Done (finished in < 0.1s) - - ! And all that glitters is gold - - ! Only shooting stars break the mold - "}; - - assert_eq!(expected, strip_control_codes(reader.read_lossy().unwrap())); - } - - #[test] - fn announce_and_exit_makes_no_whitespace() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - - BuildLog::new(writer) - .buildpack_name("Quick and simple") - .section("Start") - .step("Step") - .announce() // <== Here - .end_announce() // <== Here - .end_section() - .finish_logging(); - - let expected = formatdoc! {" - - # Quick and simple - - - Start - - Step - - Done (finished in < 0.1s) - "}; - - assert_eq!(expected, strip_control_codes(reader.read_lossy().unwrap())); - } -} diff --git a/commons/src/output/fmt.rs b/commons/src/output/fmt.rs deleted file mode 100644 index 3f73cd9d..00000000 --- a/commons/src/output/fmt.rs +++ /dev/null @@ -1,342 +0,0 @@ -//! Helpers for formatting and colorizing your output -use crate::output::util::LinesWithEndings; -use const_format::formatcp; -use std::fmt::Write; - -/// Decorated str for prefixing "Help:" -#[deprecated(note = "Use `bullet_stream` instead")] -pub const HELP: &str = formatcp!("{IMPORTANT_COLOR}! HELP{RESET}"); - -/// Decorated str for prefixing "Debug info:" -#[deprecated(note = "Use `bullet_stream` instead")] -pub const DEBUG_INFO: &str = formatcp!("{IMPORTANT_COLOR}Debug info{RESET}"); - -/// Decorate a URL for the build output -#[must_use] -#[deprecated(note = "Use `bullet_stream` instead")] -pub fn url(contents: impl AsRef) -> String { - colorize(URL_COLOR, contents) -} - -/// Decorate the name of a command being run i.e. `bundle install` -#[must_use] -#[deprecated(note = "Use `bullet_stream` instead")] -pub fn command(contents: impl AsRef) -> String { - value(colorize(COMMAND_COLOR, contents.as_ref())) -} - -/// Decorate an important value i.e. `2.3.4` -#[must_use] -#[deprecated(note = "Use `bullet_stream` instead")] -pub fn value(contents: impl AsRef) -> String { - let contents = colorize(VALUE_COLOR, contents.as_ref()); - format!("`{contents}`") -} - -/// Decorate additional information at the end of a line -#[must_use] -#[deprecated(note = "Use `bullet_stream` instead")] -pub fn details(contents: impl AsRef) -> String { - let contents = contents.as_ref(); - format!("({contents})") -} - -pub(crate) const RED: &str = "\x1B[0;31m"; -pub(crate) const YELLOW: &str = "\x1B[0;33m"; -pub(crate) const CYAN: &str = "\x1B[0;36m"; - -pub(crate) const BOLD_CYAN: &str = "\x1B[1;36m"; -pub(crate) const BOLD_PURPLE: &str = "\x1B[1;35m"; // magenta - -pub(crate) const DEFAULT_DIM: &str = "\x1B[2;1m"; // Default color but softer/less vibrant -pub(crate) const RESET: &str = "\x1B[0m"; - -#[cfg(test)] -pub(crate) const NOCOLOR: &str = "\x1B[1;39m"; // Differentiate between color clear and explicit no color https://github.com/heroku/buildpacks-ruby/pull/155#discussion_r1260029915 -pub(crate) const ALL_CODES: [&str; 7] = [ - RED, - YELLOW, - CYAN, - BOLD_CYAN, - BOLD_PURPLE, - DEFAULT_DIM, - RESET, -]; - -pub(crate) const HEROKU_COLOR: &str = BOLD_PURPLE; -pub(crate) const VALUE_COLOR: &str = YELLOW; -pub(crate) const COMMAND_COLOR: &str = BOLD_CYAN; -pub(crate) const URL_COLOR: &str = CYAN; -pub(crate) const IMPORTANT_COLOR: &str = CYAN; -pub(crate) const ERROR_COLOR: &str = RED; - -#[allow(dead_code)] -pub(crate) const WARNING_COLOR: &str = YELLOW; - -const SECTION_PREFIX: &str = "- "; -const STEP_PREFIX: &str = " - "; -const CMD_INDENT: &str = " "; - -/// Used with libherokubuildpack linemapped command output -/// -#[must_use] -pub(crate) fn cmd_stream_format(mut input: Vec) -> Vec { - let mut result: Vec = CMD_INDENT.into(); - result.append(&mut input); - result -} - -#[must_use] -pub(crate) fn background_timer_start() -> String { - colorize(DEFAULT_DIM, " .") -} - -#[must_use] -pub(crate) fn background_timer_tick() -> String { - colorize(DEFAULT_DIM, ".") -} - -#[must_use] -pub(crate) fn background_timer_end() -> String { - colorize(DEFAULT_DIM, ". ") -} - -#[must_use] -pub(crate) fn section(topic: impl AsRef) -> String { - prefix_indent(SECTION_PREFIX, topic) -} - -#[must_use] -pub(crate) fn step(contents: impl AsRef) -> String { - prefix_indent(STEP_PREFIX, contents) -} - -/// Used to decorate a buildpack -#[must_use] -pub(crate) fn header(contents: impl AsRef) -> String { - let contents = contents.as_ref(); - colorize(HEROKU_COLOR, format!("\n# {contents}")) -} - -// Prefix is expected to be a single line -// -// If contents is multi line then indent additional lines to align with the end of the prefix. -pub(crate) fn prefix_indent(prefix: impl AsRef, contents: impl AsRef) -> String { - let prefix = prefix.as_ref(); - let contents = contents.as_ref(); - let non_whitespace_re = regex::Regex::new("\\S").expect("Clippy"); - let clean_prefix = strip_control_codes(prefix); - - let indent_str = non_whitespace_re.replace_all(&clean_prefix, " "); // Preserve whitespace characters like tab and space, replace all characters with spaces - let lines = LinesWithEndings::from(contents).collect::>(); - - if let Some((first, rest)) = lines.split_first() { - format!( - "{prefix}{first}{}", - rest.iter().fold(String::new(), |mut output, line| { - let _ = write!(output, "{indent_str}{line}"); - output - }) - ) - } else { - prefix.to_string() - } -} - -#[must_use] -pub(crate) fn important(contents: impl AsRef) -> String { - colorize(IMPORTANT_COLOR, bangify(contents)) -} - -#[must_use] -pub(crate) fn warning(contents: impl AsRef) -> String { - colorize(WARNING_COLOR, bangify(contents)) -} - -#[must_use] -pub(crate) fn error(contents: impl AsRef) -> String { - colorize(ERROR_COLOR, bangify(contents)) -} - -/// Helper method that adds a bang i.e. `!` before strings -pub(crate) fn bangify(body: impl AsRef) -> String { - prepend_each_line("!", " ", body) -} - -// Ensures every line starts with `prepend` -pub(crate) fn prepend_each_line( - prepend: impl AsRef, - separator: impl AsRef, - contents: impl AsRef, -) -> String { - let body = contents.as_ref(); - let prepend = prepend.as_ref(); - let separator = separator.as_ref(); - - let lines = LinesWithEndings::from(body) - .map(|line| { - if line.trim().is_empty() { - format!("{prepend}{line}") - } else { - format!("{prepend}{separator}{line}") - } - }) - .collect::(); - lines -} - -/// Colorizes a body while preserving existing color/reset combinations and clearing before newlines -/// -/// Colors with newlines are a problem since the contents stream to git which prepends `remote:` before the `libcnb_test` -/// if we don't clear, then we will colorize output that isn't ours. -/// -/// Explicitly uncolored output is handled by treating `\x1b[1;39m` (NOCOLOR) as a distinct case from `\x1b[0m` -pub(crate) fn colorize(color: &str, body: impl AsRef) -> String { - body.as_ref() - .split('\n') - // If sub contents are colorized it will contain SUBCOLOR ... RESET. After the reset, - // ensure we change back to the current color - .map(|line| line.replace(RESET, &format!("{RESET}{color}"))) // Handles nested color - // Set the main color for each line and reset after so we don't colorize `remote:` by accident - .map(|line| format!("{color}{line}{RESET}")) - // The above logic causes redundant colors and resets, clean them up - .map(|line| line.replace(&format!("{RESET}{color}{RESET}"), RESET)) - .map(|line| line.replace(&format!("{color}{color}"), color)) // Reduce useless color - .collect::>() - .join("\n") -} - -pub(crate) fn strip_control_codes(contents: impl AsRef) -> String { - let mut contents = contents.as_ref().to_string(); - for code in ALL_CODES { - contents = contents.replace(code, ""); - } - contents -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_prefix_indent() { - assert_eq!("- hello", &prefix_indent("- ", "hello")); - assert_eq!("- hello\n world", &prefix_indent("- ", "hello\nworld")); - assert_eq!("- hello\n world\n", &prefix_indent("- ", "hello\nworld\n")); - let actual = prefix_indent(format!("- {RED}help:{RESET} "), "hello\nworld\n"); - assert_eq!( - &format!("- {RED}help:{RESET} hello\n world\n"), - &actual - ); - } - - #[test] - fn test_bangify() { - let actual = bangify("hello"); - assert_eq!("! hello", actual); - - let actual = bangify("\n"); - assert_eq!("!\n", actual); - } - - #[test] - fn handles_explicitly_removed_colors() { - let nested = colorize(NOCOLOR, "nested"); - - let out = colorize(RED, format!("hello {nested} color")); - let expected = format!("{RED}hello {NOCOLOR}nested{RESET}{RED} color{RESET}"); - - assert_eq!(expected, out); - } - - #[test] - fn handles_nested_colors() { - let nested = colorize(CYAN, "nested"); - - let out = colorize(RED, format!("hello {nested} color")); - let expected = format!("{RED}hello {CYAN}nested{RESET}{RED} color{RESET}"); - - assert_eq!(expected, out); - } - - #[test] - fn splits_newlines() { - let actual = colorize(RED, "hello\nworld"); - let expected = format!("{RED}hello{RESET}\n{RED}world{RESET}"); - - assert_eq!(expected, actual); - } - - #[test] - fn simple_case() { - let actual = colorize(RED, "hello world"); - assert_eq!(format!("{RED}hello world{RESET}"), actual); - } -} - -pub(crate) mod time { - use std::time::Duration; - - // Returns the part of a duration only in miliseconds - pub(crate) fn milliseconds(duration: &Duration) -> u32 { - duration.subsec_millis() - } - - pub(crate) fn seconds(duration: &Duration) -> u64 { - duration.as_secs() % 60 - } - - pub(crate) fn minutes(duration: &Duration) -> u64 { - (duration.as_secs() / 60) % 60 - } - - pub(crate) fn hours(duration: &Duration) -> u64 { - (duration.as_secs() / 3600) % 60 - } - - #[must_use] - pub(crate) fn human(duration: &Duration) -> String { - let hours = hours(duration); - let minutes = minutes(duration); - let seconds = seconds(duration); - let miliseconds = milliseconds(duration); - - if hours > 0 { - format!("{hours}h {minutes}m {seconds}s") - } else if minutes > 0 { - format!("{minutes}m {seconds}s") - } else if seconds > 0 || miliseconds > 100 { - // 0.1 - format!("{seconds}.{miliseconds:0>3}s") - } else { - String::from("< 0.1s") - } - } - - #[cfg(test)] - mod test { - use super::*; - - #[test] - fn test_millis_and_seconds() { - let duration = Duration::from_millis(1024); - assert_eq!(24, milliseconds(&duration)); - assert_eq!(1, seconds(&duration)); - } - - #[test] - fn test_display_duration() { - let duration = Duration::from_millis(99); - assert_eq!("< 0.1s", human(&duration).as_str()); - - let duration = Duration::from_millis(1024); - assert_eq!("1.024s", human(&duration).as_str()); - - let duration = std::time::Duration::from_millis(60 * 1024); - assert_eq!("1m 1s", human(&duration).as_str()); - - let duration = std::time::Duration::from_millis(3600 * 1024); - assert_eq!("1h 1m 26s", human(&duration).as_str()); - } - } -} diff --git a/commons/src/output/interface.rs b/commons/src/output/interface.rs deleted file mode 100644 index c4da4ec6..00000000 --- a/commons/src/output/interface.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Consuming stateful logger interface -//! -//! The log pattern used by `BuildLog` is a consuming state machine that is designed to minimize -//! the amount of mistakes that can result in malformed build output. -//! -//! The interface isn't stable and may need to change. -//! -use std::fmt::Debug; -use std::io::Write; - -pub trait Logger: Debug { - fn buildpack_name(self, s: &str) -> Box; - fn without_buildpack_name(self) -> Box; -} - -pub trait StartedLogger: Debug { - fn section(self: Box, s: &str) -> Box; - fn finish_logging(self: Box); - - fn announce(self: Box) -> Box>>; -} - -pub trait SectionLogger: Debug { - fn step(self: Box, s: &str) -> Box; - fn mut_step(&mut self, s: &str); - fn step_timed(self: Box, s: &str) -> Box; - fn step_timed_stream(self: Box, s: &str) -> Box; - fn end_section(self: Box) -> Box; - - fn announce(self: Box) -> Box>>; -} - -pub trait AnnounceLogger: ErrorLogger + Debug { - type ReturnTo; - - fn warning(self: Box, s: &str) -> Box>; - fn warn_later(self: Box, s: &str) -> Box>; - fn important(self: Box, s: &str) -> Box>; - - fn end_announce(self: Box) -> Self::ReturnTo; -} - -pub trait TimedStepLogger: Debug { - fn finish_timed_step(self: Box) -> Box; -} - -pub trait StreamLogger: Debug { - fn io(&mut self) -> Box; - fn finish_timed_stream(self: Box) -> Box; -} - -pub trait ErrorLogger: Debug { - fn error(self: Box, s: &str); -} diff --git a/commons/src/output/mod.rs b/commons/src/output/mod.rs deleted file mode 100644 index 9a293aa4..00000000 --- a/commons/src/output/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod background_timer; - -#[allow(deprecated)] -pub mod build_log; -#[allow(deprecated)] -pub mod fmt; -#[allow(deprecated)] -pub mod interface; -#[allow(deprecated)] -pub mod section_log; -mod util; -#[allow(deprecated)] -pub mod warn_later; diff --git a/commons/src/output/section_log.rs b/commons/src/output/section_log.rs deleted file mode 100644 index 0ecdfc13..00000000 --- a/commons/src/output/section_log.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! Write to the build output in a `Box` format with functions -//! -//! ## What -//! -//! Logging from within a layer can be difficult because calls to the layer interface are not -//! mutable nor consumable. Functions can be used at any time with no restrictions. The -//! only downside is that the buildpack author (you) is now responsible for: -//! -//! - Ensuring that `Box::section()` was called right before any of these -//! functions are called. -//! - Ensuring that you are not attempting to log while already logging i.e. calling `step()` within a -//! `step_timed()` call. -//! -//! ## Use -//! -//! The main use case is logging inside of a layer: -//! -//! ```no_run -//! use commons::output::section_log::log_step_timed; -//! -//! // fn create( -//! // &self, -//! // context: &libcnb::build::BuildContext, -//! // layer_path: &std::path::Path, -//! // ) -> Result< -//! // libcnb::layer::LayerResult, -//! // ::Error, -//! // > { -//! log_step_timed("Installing", || { -//! // Install logic here -//! todo!() -//! }) -//! // } -//! ``` -use crate::output::build_log::{state, BuildData, BuildLog}; -#[allow(clippy::wildcard_imports)] -pub use crate::output::interface::*; -use std::io::Stdout; -use std::marker::PhantomData; - -/// Output a message as a single step, ideally a short message -/// -/// ``` -/// use commons::output::section_log::log_step; -/// -/// log_step("Clearing cache (ruby version changed)"); -/// ``` -#[deprecated(note = "Use `bullet_stream` instead")] -pub fn log_step(s: impl AsRef) { - logger().step(s.as_ref()); -} - -/// Will print the input string followed by a background timer -/// that will emit to the UI until the passed in function ends -/// -/// ``` -/// use commons::output::section_log::log_step_timed; -/// -/// log_step_timed("Installing", || { -/// // Install logic here -/// }); -/// ``` -/// -/// Timing information will be output at the end of the step. -#[deprecated(note = "Use `bullet_stream` instead")] -pub fn log_step_timed(s: impl AsRef, f: impl FnOnce() -> T) -> T { - let timer = logger().step_timed(s.as_ref()); - let out = f(); - timer.finish_timed_step(); - out -} - -#[deprecated(note = "Use `bullet_stream` instead")] -pub fn log_step_stream( - s: impl AsRef, - f: impl FnOnce(&mut Box) -> T, -) -> T { - let mut stream = logger().step_timed_stream(s.as_ref()); - let out = f(&mut stream); - stream.finish_timed_stream(); - out -} - -/// Print an error block to the output -#[deprecated(note = "Use `bullet_stream` instead")] -pub fn log_error(s: impl AsRef) { - logger().announce().error(s.as_ref()); -} - -/// Print an warning block to the output -#[deprecated(note = "Use `bullet_stream` instead")] -pub fn log_warning(s: impl AsRef) { - logger().announce().warning(s.as_ref()); -} - -/// Print an warning block to the output at a later time -#[deprecated(note = "Use `bullet_stream` instead")] -pub fn log_warning_later(s: impl AsRef) { - logger().announce().warn_later(s.as_ref()); -} - -/// Print an important block to the output -#[deprecated(note = "Use `bullet_stream` instead")] -pub fn log_important(s: impl AsRef) { - logger().announce().important(s.as_ref()); -} - -fn logger() -> Box { - Box::new(BuildLog:: { - io: std::io::stdout(), - // Be careful not to do anything that might access this state - // as it's ephemeral data (i.e. not passed in from the start of the build) - data: BuildData::default(), - state: PhantomData, - }) -} diff --git a/commons/src/output/util.rs b/commons/src/output/util.rs deleted file mode 100644 index ee82a7b9..00000000 --- a/commons/src/output/util.rs +++ /dev/null @@ -1,186 +0,0 @@ -use lazy_static::lazy_static; -use std::fmt::Debug; -use std::io::Write; -use std::ops::Deref; -use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; - -lazy_static! { - static ref TRAILING_WHITESPACE_RE: regex::Regex = regex::Regex::new(r"\s+$").expect("clippy"); -} - -/// Threadsafe writer that can be read from -/// -/// Useful for testing -#[derive(Debug)] -pub(crate) struct ReadYourWrite -where - W: Write + AsRef<[u8]>, -{ - arc: Arc>, -} - -impl Clone for ReadYourWrite -where - W: Write + AsRef<[u8]> + Debug, -{ - fn clone(&self) -> Self { - Self { - arc: self.arc.clone(), - } - } -} - -impl Write for ReadYourWrite -where - W: Write + AsRef<[u8]> + Debug, -{ - fn write(&mut self, buf: &[u8]) -> std::io::Result { - let mut writer = self.arc.lock().expect("Internal error"); - writer.write(buf) - } - - fn flush(&mut self) -> std::io::Result<()> { - let mut writer = self.arc.lock().expect("Internal error"); - writer.flush() - } -} - -impl ReadYourWrite -where - W: Write + AsRef<[u8]>, -{ - #[allow(dead_code)] - pub(crate) fn writer(writer: W) -> Self { - Self { - arc: Arc::new(Mutex::new(writer)), - } - } - - #[must_use] - #[allow(dead_code)] - pub(crate) fn reader(&self) -> Reader { - Reader { - arc: self.arc.clone(), - } - } - - #[must_use] - #[allow(dead_code)] - pub(crate) fn arc_io(&self) -> Arc> { - self.arc.clone() - } -} - -pub(crate) struct Reader -where - W: Write + AsRef<[u8]>, -{ - arc: Arc>, -} - -impl Reader -where - W: Write + AsRef<[u8]>, -{ - #[allow(dead_code)] - pub(crate) fn read_lossy(&self) -> Result>> { - let io = &self.arc.lock()?; - - Ok(String::from_utf8_lossy(io.as_ref()).to_string()) - } -} - -impl Deref for Reader -where - W: Write + AsRef<[u8]>, -{ - type Target = Arc>; - - fn deref(&self) -> &Self::Target { - &self.arc - } -} - -/// Iterator yielding every line in a string. The line includes newline character(s). -/// -/// -/// -/// The problem this solves is when iterating over lines of a string, the whitespace may be significant. -/// For example if you want to split a string and then get the original string back then calling -/// `lines().collect>().join("\n")` will never preserve trailing newlines. -/// -/// There's another option to `lines().fold(String::new(), |s, l| s + l + "\n")`, but that -/// always adds a trailing newline even if the original string doesn't have one. -pub(crate) struct LinesWithEndings<'a> { - input: &'a str, -} - -impl<'a> LinesWithEndings<'a> { - pub(crate) fn from(input: &'a str) -> LinesWithEndings<'a> { - LinesWithEndings { input } - } -} - -impl<'a> Iterator for LinesWithEndings<'a> { - type Item = &'a str; - - #[inline] - fn next(&mut self) -> Option<&'a str> { - if self.input.is_empty() { - return None; - } - let split = self.input.find('\n').map_or(self.input.len(), |i| i + 1); - - let (line, rest) = self.input.split_at(split); - self.input = rest; - Some(line) - } -} - -/// Removes trailing whitespace from lines -/// -/// Useful because most editors strip trailing whitespace (in test fixtures) -/// but commands emit newlines -/// with leading spaces. These can be sanatized by removing trailing whitespace. -#[allow(dead_code)] -pub(crate) fn strip_trailing_whitespace(s: impl AsRef) -> String { - LinesWithEndings::from(s.as_ref()) - .map(|line| { - // Remove empty indented lines - TRAILING_WHITESPACE_RE.replace(line, "\n").to_string() - }) - .collect::() -} - -#[cfg(test)] -mod test { - use super::*; - use std::fmt::Write; - - #[test] - fn test_trailing_whitespace() { - let actual = strip_trailing_whitespace("hello \n"); - assert_eq!("hello\n", &actual); - - let actual = strip_trailing_whitespace("hello\n \nworld\n"); - assert_eq!("hello\n\nworld\n", &actual); - } - - #[test] - fn test_lines_with_endings() { - let actual = LinesWithEndings::from("foo\nbar").fold(String::new(), |mut output, line| { - let _ = write!(output, "z{line}"); - output - }); - - assert_eq!("zfoo\nzbar", actual); - - let actual = - LinesWithEndings::from("foo\nbar\n").fold(String::new(), |mut output, line| { - let _ = write!(output, "z{line}"); - output - }); - - assert_eq!("zfoo\nzbar\n", actual); - } -} diff --git a/commons/src/output/warn_later.rs b/commons/src/output/warn_later.rs deleted file mode 100644 index ae361a97..00000000 --- a/commons/src/output/warn_later.rs +++ /dev/null @@ -1,351 +0,0 @@ -//! Queue a warning for later -//! -//! Build logs can be quite large and people don't always scroll back up to read every line. Delaying -//! a warning and emitting it right before the end of the build can increase the chances the app -//! developer will read it. -//! -//! ## Use - Setup a `WarnGuard` in your buildpack -//! -//! To ensure warnings are printed, even in the event of errors, you must create a `WarnGuard` -//! in your buildpack that will print any delayed warnings when dropped: -//! -//! ```no_run -//! // src/main.rs -//! use commons::output::warn_later::WarnGuard; -//! -//! // fn build(&self, context: BuildContext) -> libcnb::Result { -//! let warn_later = WarnGuard::new(std::io::stdout()); -//! // ... -//! -//! // Warnings will be emitted when the warn guard is dropped -//! drop(warn_later); -//! // } -//! ``` -//! -//! Alternatively you can manually print delayed warnings: -//! -//! ```no_run -//! use commons::output::warn_later::WarnGuard; -//! -//! // fn build(&self, context: BuildContext) -> libcnb::Result { -//! let warn_later = WarnGuard::new(std::io::stdout()); -//! // ... -//! -//! // Consumes the guard, prints and clears any delayed warnings. -//! warn_later.warn_now(); -//! // } -//! ``` -//! -//! ## Use - Issue a delayed warning -//! -//! Once a warn guard is in place you can queue a warning using `section_log::log_warning_later` or `build_log::*`: -//! -//! ``` -//! use commons::output::warn_later::WarnGuard; -//! use commons::output::build_log::*; -//! -//! // src/main.rs -//! let warn_later = WarnGuard::new(std::io::stdout()); -//! -//! BuildLog::new(std::io::stdout()) -//! .buildpack_name("Julius Caesar") -//! .announce() -//! .warn_later("Beware the ides of march"); -//! ``` -//! -//! ``` -//! use commons::output::warn_later::WarnGuard; -//! use commons::output::section_log::log_warning_later; -//! -//! // src/main.rs -//! let warn_later = WarnGuard::new(std::io::stdout()); -//! -//! // src/layers/greenday.rs -//! log_warning_later("WARNING: Live without warning"); -//! ``` - -use indoc::formatdoc; -use std::cell::RefCell; -use std::fmt::{Debug, Display}; -use std::io::Write; -use std::marker::PhantomData; -use std::rc::Rc; -use std::thread::ThreadId; - -pub type PhantomUnsync = PhantomData>; - -thread_local!(static WARN_LATER: RefCell>> = const { RefCell::new(None) }); - -/// Pushes a string to a thread local warning vec for to be emitted later -/// -/// # Errors -/// -/// If the internal `WARN_LATER` option is `None` this will emit a `WarnLaterError` because -/// the function call might not be visible to the application owner using the buildpack. -/// -/// This state can happen if no `WarnGuard` is created in the thread where the delayed warning -/// message is trying to be pushed. It can also happen if multiple `WarnGuard`-s are created in the -/// same thread and one of them "takes" the contents before the others go out of scope. -/// -/// For best practice create one and only one `WarnGuard` per thread at a time to avoid this error -/// state. -pub(crate) fn try_push(s: impl AsRef) -> Result<(), WarnLaterError> { - WARN_LATER.with(|cell| match &mut *cell.borrow_mut() { - Some(warnings) => { - warnings.push(s.as_ref().to_string()); - Ok(()) - } - None => Err(WarnLaterError::MissingGuardForThread( - std::thread::current().id(), - )), - }) -} - -/// Ensures a warning vec is present and pushes to it -/// -/// Should only ever be used within a `WarnGuard`. -/// -/// The state where the warnings are taken, but a warn guard is still present -/// can happen when more than one warn guard is created in the same thread -fn force_push(s: impl AsRef) { - WARN_LATER.with(|cell| { - let option = &mut *cell.borrow_mut(); - option - .get_or_insert(Vec::new()) - .push(s.as_ref().to_string()); - }); -} - -/// Removes all delayed warnings from current thread -/// -/// Should only execute from within a `WarnGuard` -fn take() -> Option> { - WARN_LATER.with(|cell| cell.replace(None)) -} - -#[derive(Debug)] -pub enum WarnLaterError { - MissingGuardForThread(ThreadId), -} - -impl Display for WarnLaterError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - WarnLaterError::MissingGuardForThread(id) => { - writeln!( - f, - "{}", - formatdoc! {" - Cannot use warn_later unless a WarnGuard has been created - and not yet dropped in the current thread: {id:?} - "} - ) - } - } - } -} - -/// Delayed Warning emitter -/// -/// To use the delayed warnings feature you'll need to first register a guard. -/// This guard will emit warnings when it goes out of scope or when you manually force it -/// to emit warnings. -/// -/// This struct allows delayed warnings to be emitted even in the even there's an error. -/// -/// See the [`warn_later`] module docs for usage instructions. -/// -/// The internal design of this features relies on state tied to the current thread. -/// As a result, this struct is not send or sync: -/// -/// ```compile_fail -/// // Fails to compile -/// # // Do not remove this test, it is the only thing that asserts this is not sync -/// use commons::output::warn_later::WarnGuard; -/// -/// fn is_sync(t: impl Sync) {} -/// -/// is_sync(WarnGuard::new(std::io::stdout())) -/// ``` -/// -/// ```compile_fail -/// // Fails to compile -/// # // Do not remove this test, it is the only thing that asserts this is not send -/// use commons::output::warn_later::WarnGuard; -/// -/// fn is_send(t: impl Send) {} -/// -/// is_send(WarnGuard::new(std::io::stdout())) -/// ``` -/// -/// If you are warning in multiple threads you can pass queued warnings from one thread to another. -/// -/// ```rust -/// use commons::output::warn_later::{WarnGuard, DelayedWarnings}; -/// -/// let main_guard = WarnGuard::new(std::io::stdout()); -/// -/// let (delayed_send, delayed_recv) = std::sync::mpsc::channel::(); -/// -/// std::thread::spawn(move || { -/// let sub_guard = WarnGuard::new(std::io::stdout()); -/// // ... -/// delayed_send -/// .send(sub_guard.consume_quiet()) -/// .unwrap(); -/// }) -/// .join(); -/// -/// main_guard -/// .extend_warnings(delayed_recv.recv().unwrap()); -/// ``` -#[derive(Debug)] -pub struct WarnGuard -where - W: Write + Debug, -{ - // Private inner to force public construction through `new()` which tracks creation state per thread. - io: W, - /// The use of `WarnGuard` is directly tied to the thread where it was created - /// This forces the struct to not be send or sync - /// - /// To move warn later data between threads, drain quietly, send the data to another - /// thread, and re-apply those warnings to a `WarnGuard` in the other thread. - unsync: PhantomUnsync, -} - -impl WarnGuard -where - W: Write + Debug, -{ - #[must_use] - #[allow(clippy::new_without_default)] - pub fn new(io: W) -> Self { - WARN_LATER.with(|cell| { - let maybe_warnings = &mut *cell.borrow_mut(); - if let Some(warnings) = maybe_warnings.take() { - let _ = maybe_warnings.insert(warnings); - eprintln!("[Buildpack warning]: Multiple `WarnGuard`-s in thread {id:?}, this may cause unexpected delayed warning output", id = std::thread::current().id()); - } else { - let _ = maybe_warnings.insert(Vec::new()); - } - }); - - Self { - io, - unsync: PhantomData, - } - } - - /// Use to move warnings from a different thread into this one - pub fn extend_warnings(&self, warnings: DelayedWarnings) { - for warning in warnings.inner { - force_push(warning.clone()); - } - } - - /// Use to move warnings out of the current thread without emitting to the UI. - pub fn consume_quiet(self) -> DelayedWarnings { - DelayedWarnings { - inner: take().unwrap_or_default(), - } - } - - /// Consumes self, prints and drains all existing delayed warnings - pub fn warn_now(self) { - drop(self); - } -} - -impl Drop for WarnGuard -where - W: Write + Debug, -{ - fn drop(&mut self) { - if let Some(warnings) = take() { - if !warnings.is_empty() { - for warning in &warnings { - writeln!(&mut self.io).expect("warn guard IO is writeable"); - write!(&mut self.io, "{warning}").expect("warn guard IO is writeable"); - } - } - } - } -} - -/// Holds warnings from a consumed `WarnGuard` -/// -/// The intended use of this struct is to pass warnings from one `WarnGuard` to another. -#[derive(Debug)] -pub struct DelayedWarnings { - // Private inner, must be constructed within a WarnGuard - inner: Vec, -} - -#[cfg(test)] -mod test { - use super::*; - use crate::output::util::ReadYourWrite; - use libcnb_test::assert_contains; - - #[test] - fn test_warn_guard_registers_itself() { - // Err when a guard is not yet created - assert!(try_push("lol").is_err()); - - // Don't err when a guard is created - let _warn_guard = WarnGuard::new(Vec::new()); - try_push("lol").unwrap(); - } - - #[test] - fn test_logging_a_warning() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - let warn_guard = WarnGuard::new(writer); - drop(warn_guard); - - assert_eq!(String::new(), reader.read_lossy().unwrap()); - - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - let warn_guard = WarnGuard::new(writer); - let message = - "Possessing knowledge and performing an action are two entirely different processes"; - - try_push(message).unwrap(); - drop(warn_guard); - - assert_contains!(reader.read_lossy().unwrap(), message); - - // Assert empty after calling drain - assert!(take().is_none()); - } - - #[test] - fn test_delayed_warnings_on_drop() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - let guard = WarnGuard::new(writer); - - let message = "You don't have to have a reason to be tired. You don't have to earn rest or comfort. You're allowed to just be."; - try_push(message).unwrap(); - drop(guard); - - assert_contains!(reader.read_lossy().unwrap(), message); - } - - #[test] - fn does_not_double_whitespace() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - let guard = WarnGuard::new(writer); - - let message = "Caution: This test is hot\n"; - try_push(message).unwrap(); - drop(guard); - - let expected = "\nCaution: This test is hot\n".to_string(); - assert_eq!(expected, reader.read_lossy().unwrap()); - } -}