diff --git a/buildpacks/ruby/src/gem_list.rs b/buildpacks/ruby/src/gem_list.rs index 33ba1102..f680f2a8 100644 --- a/buildpacks/ruby/src/gem_list.rs +++ b/buildpacks/ruby/src/gem_list.rs @@ -1,13 +1,11 @@ +use bullet_stream::{state::SubBullet, style, Print}; 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; +use std::io::Stdout; use std::process::Command; /// ## Gets list of an application's dependencies @@ -18,6 +16,31 @@ pub(crate) struct GemList { pub(crate) gems: HashMap, } +/// Calls `bundle list` and returns a `GemList` struct +/// +/// # Errors +/// +/// Errors if the command `bundle list` is unsuccessful. +pub(crate) fn bundle_list( + bullet: Print>, + envs: T, +) -> Result<(Print>, GemList), CmdError> +where + T: IntoIterator, + K: AsRef, + V: AsRef, +{ + let mut cmd = Command::new("bundle"); + cmd.arg("list").env_clear().envs(envs); + + let timer = bullet.start_timer(format!("Running {}", style::command(cmd.name()))); + let gem_list = cmd + .named_output() + .map(|output| output.stdout_lossy()) + .and_then(|output| GemList::from_str(&output))?; + Ok((timer.done(), gem_list)) +} + /// Converts the output of `$ gem list` into a data structure that can be inspected and compared /// /// ``` @@ -54,30 +77,6 @@ pub(crate) struct GemList { /// ); /// ``` impl GemList { - /// Calls `bundle list` and returns a `GemList` struct - /// - /// # Errors - /// - /// Errors if the command `bundle list` is unsuccessful. - pub(crate) fn from_bundle_list( - envs: T, - _logger: &dyn SectionLogger, - ) -> Result - where - T: IntoIterator, - K: AsRef, - V: AsRef, - { - let mut cmd = Command::new("bundle"); - cmd.arg("list").env_clear().envs(envs); - - let output = log_step_timed(format!("Running {}", fmt::command(cmd.name())), || { - cmd.named_output() - })?; - - GemList::from_str(&output.stdout_lossy()) - } - #[must_use] pub(crate) fn has(&self, str: &str) -> bool { self.gems.contains_key(&str.trim().to_lowercase()) diff --git a/buildpacks/ruby/src/main.rs b/buildpacks/ruby/src/main.rs index a20ce457..62e42c29 100644 --- a/buildpacks/ruby/src/main.rs +++ b/buildpacks/ruby/src/main.rs @@ -2,9 +2,6 @@ use bullet_stream::{style, Print}; use commons::cache::CacheError; use commons::gemfile_lock::GemfileLock; use commons::metadata_digest::MetadataDigest; -#[allow(clippy::wildcard_imports)] -use commons::output::build_log::*; -use commons::output::warn_later::WarnGuard; use core::str::FromStr; use fs_err::PathExt; use fun_run::CmdError; @@ -111,11 +108,8 @@ impl Buildpack for RubyBuildpack { } #[allow(clippy::too_many_lines)] - #[allow(deprecated)] fn build(&self, context: BuildContext) -> libcnb::Result { let mut build_output = Print::new(stdout()).h2("Heroku Ruby Buildpack"); - let logger = BuildLog::new(stdout()).without_buildpack_name(); - let warn_later = WarnGuard::new(stdout()); // ## Set default environment let (mut env, store) = @@ -185,7 +179,7 @@ impl Buildpack for RubyBuildpack { }; // ## Bundle install - (_, env) = { + (build_output, env) = { let bullet = build_output.bullet("Bundle install gems"); let (bullet, layer_env) = layers::bundle_install_layer::handle( &context, @@ -219,30 +213,33 @@ impl Buildpack for RubyBuildpack { }; // ## Detect gems - let (mut logger, gem_list, default_process) = { - let section = logger.section("Setting default processes"); + let (mut build_output, gem_list, default_process) = { + let bullet = build_output.bullet("Default process detection"); - let gem_list = gem_list::GemList::from_bundle_list(&env, section.as_ref()) - .map_err(RubyBuildpackError::GemListGetError)?; - let default_process = steps::get_default_process(section.as_ref(), &context, &gem_list); + let (bullet, gem_list) = + gem_list::bundle_list(bullet, &env).map_err(RubyBuildpackError::GemListGetError)?; + let (bullet, default_process) = steps::get_default_process(bullet, &context, &gem_list); - (section.end_section(), gem_list, default_process) + (bullet.done(), gem_list, default_process) }; // ## Assets install - logger = { - let section = logger.section("Rake assets install"); - let rake_detect = - crate::steps::detect_rake_tasks(section.as_ref(), &gem_list, &context, &env)?; + build_output = { + let (bullet, rake_detect) = crate::steps::detect_rake_tasks( + build_output.bullet("Rake assets install"), + &gem_list, + &context, + &env, + )?; if let Some(rake_detect) = rake_detect { - crate::steps::rake_assets_install(section.as_ref(), &context, &env, &rake_detect)?; + crate::steps::rake_assets_install(bullet, &context, &env, &rake_detect)? + } else { + bullet } - - section.end_section() + .done() }; - logger.finish_logging(); - warn_later.warn_now(); + build_output.done(); if let Some(default_process) = default_process { BuildResultBuilder::new() diff --git a/buildpacks/ruby/src/rake_task_detect.rs b/buildpacks/ruby/src/rake_task_detect.rs index cfdedc65..2d287673 100644 --- a/buildpacks/ruby/src/rake_task_detect.rs +++ b/buildpacks/ruby/src/rake_task_detect.rs @@ -1,10 +1,10 @@ -use commons::output::{ - fmt, - section_log::{log_step_timed, SectionLogger}, +use bullet_stream::{ + state::SubBullet, + {style, Print}, }; - use core::str::FromStr; use fun_run::{CmdError, CommandWithName}; +use std::io::Stdout; use std::{ffi::OsStr, process::Command}; /// Run `rake -P` and parse output to show what rake tasks an application has @@ -21,41 +21,36 @@ pub(crate) struct RakeDetect { output: String, } -impl RakeDetect { - /// # Errors - /// - /// Will return `Err` if `bundle exec rake -p` command cannot be invoked by the operating system. - pub(crate) fn from_rake_command< - T: IntoIterator, - K: AsRef, - V: AsRef, - >( - _logger: &dyn SectionLogger, - envs: T, - error_on_failure: bool, - ) -> Result { - let mut cmd = Command::new("bundle"); - cmd.args(["exec", "rake", "-P", "--trace"]) - .env_clear() - .envs(envs); +/// # Errors +/// +/// Will return `Err` if `bundle exec rake -p` command cannot be invoked by the operating system. +pub(crate) fn call, K: AsRef, V: AsRef>( + bullet: Print>, + envs: T, + error_on_failure: bool, +) -> Result<(Print>, RakeDetect), CmdError> { + let mut cmd = Command::new("bundle"); + cmd.args(["exec", "rake", "-P", "--trace"]) + .env_clear() + .envs(envs); - log_step_timed(format!("Running {}", fmt::command(cmd.name())), || { - cmd.named_output() - }) - .or_else(|error| { - if error_on_failure { - Err(error) - } else { - match error { - CmdError::SystemError(_, _) => Err(error), - CmdError::NonZeroExitNotStreamed(output) - | CmdError::NonZeroExitAlreadyStreamed(output) => Ok(output), - } + let timer = bullet.start_timer(format!("Running {}", style::command(cmd.name()))); + let output = cmd.named_output().or_else(|error| { + if error_on_failure { + Err(error) + } else { + match error { + CmdError::SystemError(_, _) => Err(error), + CmdError::NonZeroExitNotStreamed(output) + | CmdError::NonZeroExitAlreadyStreamed(output) => Ok(output), } - }) - .and_then(|output| RakeDetect::from_str(&output.stdout_lossy())) - } + } + })?; + + RakeDetect::from_str(&output.stdout_lossy()).map(|rake_detect| (timer.done(), rake_detect)) +} +impl RakeDetect { #[must_use] pub(crate) fn has_task(&self, string: &str) -> bool { let task_re = regex::Regex::new(&format!("\\s{string}")).expect("clippy"); diff --git a/buildpacks/ruby/src/steps/detect_rake_tasks.rs b/buildpacks/ruby/src/steps/detect_rake_tasks.rs index 97126225..4f71f4cc 100644 --- a/buildpacks/ruby/src/steps/detect_rake_tasks.rs +++ b/buildpacks/ruby/src/steps/detect_rake_tasks.rs @@ -1,78 +1,80 @@ -use commons::output::{ - fmt::{self, HELP}, - section_log::{log_step, SectionLogger}, -}; - use crate::gem_list::GemList; use crate::rake_status::{check_rake_ready, RakeStatus}; -use crate::rake_task_detect::RakeDetect; +use crate::rake_task_detect::{self, RakeDetect}; use crate::RubyBuildpack; use crate::RubyBuildpackError; +use bullet_stream::state::SubBullet; +use bullet_stream::{style, Print}; use libcnb::build::BuildContext; use libcnb::Env; +use std::io::Stdout; pub(crate) fn detect_rake_tasks( - logger: &dyn SectionLogger, + bullet: Print>, gem_list: &GemList, context: &BuildContext, env: &Env, -) -> Result, RubyBuildpackError> { - let rake = fmt::value("rake"); - let gemfile = fmt::value("Gemfile"); - let rakefile = fmt::value("Rakefile"); +) -> Result<(Print>, Option), RubyBuildpackError> { + let help = style::important("HELP"); + let rake = style::value("rake"); + let gemfile = style::value("Gemfile"); + let rakefile = style::value("Rakefile"); match check_rake_ready( &context.app_dir, gem_list, [".sprockets-manifest-*.json", "manifest-*.json"], ) { - RakeStatus::MissingRakeGem => { - log_step(format!( - "Skipping rake tasks {}", - fmt::details(format!("no {rake} gem in {gemfile}")) - )); - - log_step(format!( - "{HELP} Add {gem} to your {gemfile} to enable", - gem = fmt::value("gem 'rake'") - )); - - Ok(None) - } - RakeStatus::MissingRakefile => { - log_step(format!( - "Skipping rake tasks {}", - fmt::details(format!("no {rakefile}")) - )); - log_step(format!("{HELP} Add {rakefile} to your project to enable",)); - - Ok(None) - } + RakeStatus::MissingRakeGem => Ok(( + bullet + .sub_bullet(format!( + "Skipping rake tasks ({rake} gem not found in {gemfile})" + )) + .sub_bullet(format!( + "{help} Add {gem} to your {gemfile} to enable", + gem = style::value("gem 'rake'") + )), + None, + )), + RakeStatus::MissingRakefile => Ok(( + bullet + .sub_bullet(format!("Skipping rake tasks ({rakefile} not found)",)) + .sub_bullet(format!("{help} Add {rakefile} to your project to enable",)), + None, + )), RakeStatus::SkipManifestFound(paths) => { - let files = paths + let manifest_files = paths .iter() - .map(|path| fmt::value(path.to_string_lossy())) + .map(|path| style::value(path.to_string_lossy())) .collect::>() .join(", "); - log_step(format!( - "Skipping rake tasks {}", - fmt::details(format!("Manifest files found {files}")) - )); - log_step(format!("{HELP} Delete files to enable running rake tasks")); - - Ok(None) + Ok(( + bullet + .sub_bullet(format!( + "Skipping rake tasks (Manifest {files} found {manifest_files})", + files = if manifest_files.len() > 1 { + "files" + } else { + "file" + } + )) + .sub_bullet(format!("{help} Delete files to enable running rake tasks")), + None, + )) } RakeStatus::Ready(path) => { - log_step(format!( - "Detected rake ({rake} gem found, {rakefile} found at {path})", - path = fmt::value(path.to_string_lossy()) - )); - - let rake_detect = RakeDetect::from_rake_command(logger, env, true) - .map_err(RubyBuildpackError::RakeDetectError)?; + let (bullet, rake_detect) = rake_task_detect::call( + bullet.sub_bullet(format!( + "Detected rake ({rake} gem found, {rakefile} found at {path})", + path = style::value(path.to_string_lossy()) + )), + env, + true, + ) + .map_err(RubyBuildpackError::RakeDetectError)?; - Ok(Some(rake_detect)) + Ok((bullet, Some(rake_detect))) } } } diff --git a/buildpacks/ruby/src/steps/get_default_process.rs b/buildpacks/ruby/src/steps/get_default_process.rs index b2dba89e..503ac958 100644 --- a/buildpacks/ruby/src/steps/get_default_process.rs +++ b/buildpacks/ruby/src/steps/get_default_process.rs @@ -1,51 +1,46 @@ use crate::gem_list::GemList; use crate::RubyBuildpack; -use commons::output::{ - fmt, - section_log::{log_step, SectionLogger}, -}; +use bullet_stream::style; +use bullet_stream::{state::SubBullet, Print}; use libcnb::build::BuildContext; use libcnb::data::launch::Process; use libcnb::data::launch::ProcessBuilder; use libcnb::data::process_type; +use std::io::Stdout; use std::path::Path; pub(crate) fn get_default_process( - _logger: &dyn SectionLogger, + bullet: Print>, context: &BuildContext, gem_list: &GemList, -) -> Option { - let config_ru = fmt::value("config.ru"); - let rails = fmt::value("rails"); - let rack = fmt::value("rack"); - let railties = fmt::value("railties"); +) -> (Print>, Option) { + let config_ru = style::value("config.ru"); + let rails = style::value("rails"); + let rack = style::value("rack"); + let railties = style::value("railties"); match detect_web(gem_list, &context.app_dir) { - WebProcess::Rails => { - log_step(format!("Detected rails app ({rails} gem found)")); - - Some(default_rails()) - } - WebProcess::RackWithConfigRU => { - log_step(format!( + WebProcess::Rails => ( + bullet.sub_bullet(format!("Detected rails app ({rails} gem found)")), + Some(default_rails()), + ), + WebProcess::RackWithConfigRU => ( + bullet.sub_bullet(format!( "Detected rack app ({rack} gem found and {config_ru} at root of application)" - )); - - Some(default_rack()) - } - WebProcess::RackMissingConfigRu => { - log_step(format!( + )), + Some(default_rack()), + ), + WebProcess::RackMissingConfigRu => ( + bullet.sub_bullet(format!( "Skipping default web process ({rack} gem found but missing {config_ru} file)" - )); - - None - } - WebProcess::Missing => { - log_step(format!( + )), + None, + ), + WebProcess::Missing => ( + bullet.sub_bullet(format!( "Skipping default web process ({rails}, {railties}, and {rack} not found)" - )); - - None - } + )), + None, + ), } } diff --git a/buildpacks/ruby/src/steps/rake_assets_install.rs b/buildpacks/ruby/src/steps/rake_assets_install.rs index 86296979..58938d35 100644 --- a/buildpacks/ruby/src/steps/rake_assets_install.rs +++ b/buildpacks/ruby/src/steps/rake_assets_install.rs @@ -1,47 +1,55 @@ use crate::rake_task_detect::RakeDetect; use crate::RubyBuildpack; use crate::RubyBuildpackError; -use commons::cache::{mib, AppCacheCollection, CacheConfig, KeepPath}; -use commons::output::{ - fmt::{self, HELP}, - section_log::{log_step, log_step_stream, SectionLogger}, -}; -use fun_run::{self, CmdError, CommandWithName}; +use bullet_stream::state::SubBullet; +use bullet_stream::{style, Print}; +use commons::cache::{mib, AppCache, CacheConfig, CacheError, CacheState, KeepPath, PathState}; +use fun_run::{self, CommandWithName}; use libcnb::build::BuildContext; use libcnb::Env; +use std::io::Stdout; use std::process::Command; pub(crate) fn rake_assets_install( - logger: &dyn SectionLogger, + mut bullet: Print>, context: &BuildContext, env: &Env, rake_detect: &RakeDetect, -) -> Result<(), RubyBuildpackError> { +) -> Result>, RubyBuildpackError> { + let help = style::important("HELP"); let cases = asset_cases(rake_detect); - let rake_assets_precompile = fmt::value("rake assets:precompile"); - let rake_assets_clean = fmt::value("rake assets:clean"); - let rake_detect_cmd = fmt::value("bundle exec rake -P"); + let rake_assets_precompile = style::value("rake assets:precompile"); + let rake_assets_clean = style::value("rake assets:clean"); + let rake_detect_cmd = style::value("bundle exec rake -P"); match cases { AssetCases::None => { - log_step(format!( - "Skipping {rake_assets_precompile} {}", - fmt::details(format!("task not found via {rake_detect_cmd}")) - )); - log_step(format!("{HELP} Enable compiling assets by ensuring {rake_assets_precompile} is present when running the detect command locally")); + bullet = bullet.sub_bullet(format!( + "Skipping {rake_assets_clean} (task not found via {rake_detect_cmd})", + )).sub_bullet(format!("{help} Enable cleaning assets by ensuring {rake_assets_clean} is present when running the detect command locally")); } AssetCases::PrecompileOnly => { - log_step(format!( - "Compiling assets without cache {}", - fmt::details(format!("Clean task not found via {rake_detect_cmd}")) - )); - log_step(format!("{HELP} Enable caching by ensuring {rake_assets_clean} is present when running the detect command locally")); - - run_rake_assets_precompile(env) + bullet = bullet.sub_bullet( + format!("Compiling assets without cache (Clean task not found via {rake_detect_cmd})"), + ).sub_bullet(format!("{help} Enable caching by ensuring {rake_assets_clean} is present when running the detect command locally")); + + let mut cmd = Command::new("bundle"); + cmd.args(["exec", "rake", "assets:precompile", "--trace"]) + .env_clear() + .envs(env); + + bullet + .stream_with( + format!("Running {}", style::command(cmd.name())), + |stdout, stderr| cmd.stream_output(stdout, stderr), + ) + .map_err(|error| { + fun_run::map_which_problem(error, &mut cmd, env.get("PATH").cloned()) + }) .map_err(RubyBuildpackError::RakeAssetsPrecompileFailed)?; } AssetCases::PrecompileAndClean => { - log_step(format!("Compiling assets with cache {}", fmt::details(format!("detected {rake_assets_precompile} and {rake_assets_clean} via {rake_detect_cmd}")))); + bullet = bullet.sub_bullet(format!("Compiling assets with cache (detected {rake_assets_precompile} and {rake_assets_clean} via {rake_detect_cmd})")); let cache_config = [ CacheConfig { @@ -56,58 +64,70 @@ pub(crate) fn rake_assets_install( }, ]; - let cache = { - AppCacheCollection::new_and_load(context, cache_config, logger) - .map_err(RubyBuildpackError::InAppDirCacheError)? - }; - - run_rake_assets_precompile_with_clean(env) - .map_err(RubyBuildpackError::RakeAssetsPrecompileFailed)?; - - cache - .save_and_clean() + let caches = cache_config + .into_iter() + .map(|config| AppCache::new_and_load(context, config)) + .collect::, CacheError>>() .map_err(RubyBuildpackError::InAppDirCacheError)?; - } - } - Ok(()) -} - -fn run_rake_assets_precompile(env: &Env) -> Result<(), CmdError> { - let path_env = env.get("PATH").cloned(); - let mut cmd = Command::new("bundle"); + for store in &caches { + let path = store.path().display(); + bullet = bullet.sub_bullet(match store.cache_state() { + CacheState::NewEmpty => format!("Creating cache for {path}"), + CacheState::ExistsEmpty => format!("Loading (empty) cache for {path}"), + CacheState::ExistsWithContents => format!("Loading cache for {path}"), + }); + } + + let mut cmd = Command::new("bundle"); + cmd.args([ + "exec", + "rake", + "assets:precompile", + "assets:clean", + "--trace", + ]) + .env_clear() + .envs(env); + + bullet + .stream_with( + format!("Running {}", style::command(cmd.name())), + |stdout, stderr| cmd.stream_output(stdout, stderr), + ) + .map_err(|error| { + fun_run::map_which_problem(error, &mut cmd, env.get("PATH").cloned()) + }) + .map_err(RubyBuildpackError::RakeAssetsPrecompileFailed)?; - cmd.args(["exec", "rake", "assets:precompile", "--trace"]) - .env_clear() - .envs(env); + for store in caches { + let path = store.path().display(); - log_step_stream(format!("Running {}", fmt::command(cmd.name())), |stream| { - cmd.stream_output(stream.io(), stream.io()) - .map_err(|error| fun_run::map_which_problem(error, &mut cmd, path_env)) - })?; + bullet = bullet.sub_bullet(match store.path_state() { + PathState::Empty => format!("Storing cache for (empty) {path}"), + PathState::HasFiles => format!("Storing cache for {path}"), + }); - Ok(()) -} + if let Some(removed) = store + .save_and_clean() + .map_err(RubyBuildpackError::InAppDirCacheError)? + { + let path = store.path().display(); + let limit = store.limit(); + let removed_len = removed.files.len(); + let removed_size = removed.adjusted_bytes(); + + bullet = bullet.sub_bullet(format!("Detected cache size exceeded (over {limit} limit by {removed_size}) for {path}")) + .sub_bullet( + format!("Removed {removed_len} files from the cache for {path}"), + + ); + } + } + } + } -fn run_rake_assets_precompile_with_clean(env: &Env) -> Result<(), CmdError> { - let path_env = env.get("PATH").cloned(); - let mut cmd = Command::new("bundle"); - cmd.args([ - "exec", - "rake", - "assets:precompile", - "assets:clean", - "--trace", - ]) - .env_clear() - .envs(env); - - log_step_stream(format!("Running {}", fmt::command(cmd.name())), |stream| { - cmd.stream_output(stream.io(), stream.io()) - }) - .map_err(|error| fun_run::map_which_problem(error, &mut cmd, path_env))?; - - Ok(()) + Ok(bullet) } #[derive(Clone, Debug)] diff --git a/commons/CHANGELOG.md b/commons/CHANGELOG.md index 4d5a51b2..5889f3cc 100644 --- a/commons/CHANGELOG.md +++ b/commons/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog for commons features +## 2024-08-15 + +### Changed + +- Deprecate `AppCacheCollection` (https://github.com/heroku/buildpacks-ruby/pull/334) + ## 1.0.0 ### Changed diff --git a/commons/src/cache.rs b/commons/src/cache.rs index f7b2ce22..12e8b3b6 100644 --- a/commons/src/cache.rs +++ b/commons/src/cache.rs @@ -7,6 +7,7 @@ mod in_app_dir_cache_layer; pub use self::app_cache::{build, PathState}; pub use self::app_cache::{AppCache, CacheState}; +#[allow(deprecated)] pub use self::app_cache_collection::AppCacheCollection; pub use self::clean::FilesWithSize; pub use self::config::CacheConfig; diff --git a/commons/src/cache/app_cache_collection.rs b/commons/src/cache/app_cache_collection.rs index fd746b36..ef11e0ad 100644 --- a/commons/src/cache/app_cache_collection.rs +++ b/commons/src/cache/app_cache_collection.rs @@ -13,11 +13,15 @@ use std::fmt::Debug; /// Default logging is provided for each operation. /// #[derive(Debug)] +#[deprecated( + since = "0.1.0", + note = "Use `AppCache` directly to manage cache for a single path" +)] pub struct AppCacheCollection<'a> { _log: &'a dyn SectionLogger, collection: Vec, } - +#[allow(deprecated)] impl<'a> AppCacheCollection<'a> { /// Store multiple application paths in the cache ///