diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c3ff5d8..2fbfc5e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,19 +95,3 @@ jobs: run: pack build my-image --force-color --builder heroku/builder:24 --trust-extra-buildpacks --buildpack heroku/nodejs-engine --buildpack packaged/${{ matrix.target }}/debug/heroku_ruby --path tmp/ruby-getting-started --pull-policy never - name: "PRINT: Cached getting started guide output" run: pack build my-image --force-color --builder heroku/builder:24 --trust-extra-buildpacks --buildpack heroku/nodejs-engine --buildpack packaged/${{ matrix.target }}/debug/heroku_ruby --path tmp/ruby-getting-started --pull-policy never - - print-style-guide: - runs-on: ubuntu-24.04 - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Install musl-tools - run: sudo apt-get install musl-tools --no-install-recommends - - name: Update Rust toolchain - run: rustup update - - name: Install Rust linux-musl target - run: rustup target add x86_64-unknown-linux-musl - - name: Rust Cache - uses: Swatinem/rust-cache@v2.7.3 - - name: "PRINT: Style guide" - run: cargo run --quiet --bin print_style_guide diff --git a/buildpacks/ruby/src/steps/default_env.rs b/buildpacks/ruby/src/steps/default_env.rs index fa7b4b51..473770ba 100644 --- a/buildpacks/ruby/src/steps/default_env.rs +++ b/buildpacks/ruby/src/steps/default_env.rs @@ -1,5 +1,6 @@ use crate::{RubyBuildpack, RubyBuildpackError}; -use commons::layer::DefaultEnvLayer; +use libcnb::layer::UncachedLayerDefinition; +use libcnb::layer_env::{LayerEnv, ModificationBehavior}; use libcnb::{ build::BuildContext, data::{layer_name, store::Store}, @@ -9,7 +10,6 @@ use libcnb::{ use rand::Rng; // Set default environment values -#[allow(deprecated)] pub(crate) fn default_env( context: &BuildContext, platform_env: &Env, @@ -24,25 +24,32 @@ pub(crate) fn default_env( } let (default_secret_key_base, store) = fetch_secret_key_base_from_store(&context.store); - - let env_defaults_layer = context // - .handle_layer( - layer_name!("env_defaults"), - DefaultEnvLayer::new( - [ - ("SECRET_KEY_BASE", default_secret_key_base.as_str()), - ("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(), - ), - )?; - env = env_defaults_layer.env.apply(Scope::Build, &env); + let layer_ref = context.uncached_layer( + layer_name!("env_defaults"), + UncachedLayerDefinition { + build: true, + launch: true, + }, + )?; + let env = layer_ref + .write_env({ + [ + ("SECRET_KEY_BASE", default_secret_key_base.as_str()), + ("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"), + ] + .iter() + .fold(LayerEnv::new(), |layer_env, (name, value)| { + layer_env.chainable_insert(Scope::All, ModificationBehavior::Default, name, value) + }) + }) + .and_then(|()| layer_ref.read_env())? + .apply(Scope::Build, &env); Ok((env, store)) } diff --git a/buildpacks/ruby/src/user_errors.rs b/buildpacks/ruby/src/user_errors.rs index d824ed2c..7c665bd9 100644 --- a/buildpacks/ruby/src/user_errors.rs +++ b/buildpacks/ruby/src/user_errors.rs @@ -1,25 +1,22 @@ +use std::io::Stdout; use std::process::Command; -#[allow(clippy::wildcard_imports)] -use commons::output::{ - build_log::*, - fmt::{self, DEBUG_INFO}, -}; - use crate::{DetectError, RubyBuildpackError}; +use bullet_stream::{state::Bullet, state::SubBullet, style, Print}; use fun_run::{CmdError, CommandWithName}; use indoc::formatdoc; +const DEBUG_INFO_STR: &str = "Debug info"; pub(crate) fn on_error(err: libcnb::Error) { - let log = BuildLog::new(std::io::stdout()).without_buildpack_name(); + let output = Print::new(std::io::stdout()).without_header(); + let debug_info = style::important(DEBUG_INFO_STR); match cause(err) { - Cause::OurError(error) => log_our_error(log, error), + Cause::OurError(error) => log_our_error(output, error), Cause::FrameworkError(error) => - log - .section(DEBUG_INFO) - .step(&error.to_string()) - .announce() - .error(&formatdoc! {" + output + .bullet(&debug_info) + .sub_bullet(error.to_string()) + .error(formatdoc! {" Error: heroku/buildpack-ruby internal buildpack error The framework used by this buildpack encountered an unexpected error. @@ -37,20 +34,21 @@ pub(crate) fn on_error(err: libcnb::Error) { } #[allow(clippy::too_many_lines)] -fn log_our_error(mut log: Box, error: RubyBuildpackError) { +fn log_our_error(mut output: Print>, error: RubyBuildpackError) { let git_branch_url = - fmt::url("https://devcenter.heroku.com/articles/git#deploy-from-a-branch-besides-main"); + style::url("https://devcenter.heroku.com/articles/git#deploy-from-a-branch-besides-main"); let ruby_versions_url = - fmt::url("https://devcenter.heroku.com/articles/ruby-support#ruby-versions"); - let rubygems_status_url = fmt::url("https://status.rubygems.org/"); + style::url("https://devcenter.heroku.com/articles/ruby-support#ruby-versions"); + let rubygems_status_url = style::url("https://status.rubygems.org/"); + let debug_info = style::important(DEBUG_INFO_STR); match error { RubyBuildpackError::BuildpackDetectionError(DetectError::Gemfile(error)) => { - log.announce().error(&formatdoc! {" + output.error(formatdoc! {" Error: `Gemfile` found with error - There was an error trying to read the contents of the application's Gemfile. \ - The buildpack cannot continue if the Gemfile is unreadable. + There was an error trying to read the contents of the application's Gemfile. \ + The buildpack cannot continue if the Gemfile is unreadable. {error} @@ -58,7 +56,7 @@ fn log_our_error(mut log: Box, error: RubyBuildpackError) { "}); } RubyBuildpackError::BuildpackDetectionError(DetectError::PackageJson(error)) => { - log.announce().error(&formatdoc! {" + output.error(formatdoc! {" Error: `package.json` found with error The Ruby buildpack detected a package.json file but it is not readable \ @@ -74,7 +72,7 @@ fn log_our_error(mut log: Box, error: RubyBuildpackError) { "}); } RubyBuildpackError::BuildpackDetectionError(DetectError::GemfileLock(error)) => { - log.announce().error(&formatdoc! {" + output.error(formatdoc! {" Error: `Gemfile.lock` found with error There was an error trying to read the contents of the application's Gemfile.lock. \ @@ -86,7 +84,7 @@ fn log_our_error(mut log: Box, error: RubyBuildpackError) { "}); } RubyBuildpackError::BuildpackDetectionError(DetectError::YarnLock(error)) => { - log.announce().error(&formatdoc! {" + output.error(formatdoc! {" Error: `yarn.lock` found with error The Ruby buildpack detected a yarn.lock file but it is not readable \ @@ -102,25 +100,25 @@ fn log_our_error(mut log: Box, error: RubyBuildpackError) { "}); } RubyBuildpackError::MissingGemfileLock(path, error) => { - log = log - .section(&format!( + output = output + .bullet(format!( "Could not find {}, details:", - fmt::value(path.to_string_lossy()) + style::value(path.to_string_lossy()) )) - .step(&error.to_string()) - .end_section(); + .sub_bullet(error.to_string()) + .done(); if let Some(dir) = path.parent() { - log = debug_cmd( - log.section(&format!( - "{DEBUG_INFO} Contents of the {} directory", - fmt::value(dir.to_string_lossy()) + output = debug_cmd( + output.bullet(format!( + "{debug_info} Contents of the {} directory", + style::value(dir.to_string_lossy()) )), Command::new("ls").args(["la", &dir.to_string_lossy()]), ); } - log.announce().error(&formatdoc! {" + output.error(formatdoc! {" Error: `Gemfile.lock` not found A `Gemfile.lock` file is required and was not found in the root of your application. @@ -136,10 +134,9 @@ fn log_our_error(mut log: Box, error: RubyBuildpackError) { // Future: // - In the future use a manifest file to list if version is available on a different stack // - In the future add a "did you mean" Levenshtein distance to see if they typoed like "3.6.0" when they meant "3.0.6" - log.section(DEBUG_INFO) - .step(&error.to_string()) - .announce() - .error(&formatdoc! {" + output.bullet(debug_info) + .sub_bullet(error.to_string()) + .error(formatdoc! {" Error installing Ruby Could not install the detected Ruby version. Ensure that you're using a supported @@ -150,14 +147,14 @@ fn log_our_error(mut log: Box, error: RubyBuildpackError) { "}); } RubyBuildpackError::GemInstallBundlerCommandError(error) => { - log = log - .section(DEBUG_INFO) - .step(&error.to_string()) - .end_section(); + output = output + .bullet(&debug_info) + .sub_bullet(error.to_string()) + .done(); - log = debug_cmd(log.section(DEBUG_INFO), Command::new("gem").arg("env")); + output = debug_cmd(output.bullet(&debug_info), Command::new("gem").arg("env")); - log.announce().error(&formatdoc! {" + output.error(formatdoc! {" Error installing bundler The ruby package managment tool, `bundler`, failed to install. Bundler is required @@ -173,12 +170,11 @@ fn log_our_error(mut log: Box, error: RubyBuildpackError) { // Future: // - Grep error output for common things like using sqlite3, use classic buildpack let local_command = local_command_debug(&error); - log - .section(DEBUG_INFO) - .step(&error.to_string()) - .end_section() - .announce() - .error(&formatdoc! {" + output + .bullet(&debug_info) + .sub_bullet(error.to_string()) + .done() + .error(formatdoc! {" Error installing your applications's dependencies Could not install gems to the system via bundler. Gems are dependencies @@ -194,22 +190,22 @@ fn log_our_error(mut log: Box, error: RubyBuildpackError) { "}); } RubyBuildpackError::BundleInstallDigestError(path, error) => { - log = log - .section(DEBUG_INFO) - .step(&error.to_string()) - .end_section(); + output = output + .bullet(&debug_info) + .sub_bullet(error.to_string()) + .done(); if let Some(dir) = path.parent() { - log = debug_cmd( - log.section(&format!( - "{DEBUG_INFO} Contents of the {} directory", - fmt::value(dir.to_string_lossy()) + output = debug_cmd( + output.bullet(format!( + "{debug_info} Contents of the {} directory", + style::value(dir.to_string_lossy()) )), Command::new("ls").args(["la", &dir.to_string_lossy()]), ); } - log.announce().error(&formatdoc! {" + output.error(formatdoc! {" Error generating file digest An error occurred while generating a file digest. To provide the fastest possible @@ -229,69 +225,68 @@ fn log_our_error(mut log: Box, error: RubyBuildpackError) { // Future: // - Annotate with information on requiring test or development only gems in the Rakefile let local_command = local_command_debug(&error); - log = log - .section(DEBUG_INFO) - .step(&error.to_string()) - .end_section(); + output + .bullet(debug_info) + .sub_bullet(error.to_string()) + .done() + .error(formatdoc! {" + Error detecting rake tasks - log.announce().error(&formatdoc! {" - Error detecting rake tasks + The Ruby buildpack uses rake task information from your application to guide + build logic. Without this information, the Ruby buildpack cannot continue. - The Ruby buildpack uses rake task information from your application to guide - build logic. Without this information, the Ruby buildpack cannot continue. - - {local_command} + {local_command} - Use the information above to debug further. - "}); + Use the information above to debug further. + "}); } RubyBuildpackError::RakeAssetsPrecompileFailed(error) => { let local_command = local_command_debug(&error); - log = log - .section(DEBUG_INFO) - .step(&error.to_string()) - .end_section(); - - log.announce().error(&formatdoc! {" - Error compiling assets + output + .bullet(debug_info) + .sub_bullet(error.to_string()) + .done() + .error(formatdoc! {" + Error compiling assets - An error occured while compiling assets via rake command. + An error occured while compiling assets via rake command. - {local_command} + {local_command} - Use the information above to debug further. - "}); + Use the information above to debug further. + "}); } RubyBuildpackError::InAppDirCacheError(error) => { // Future: // - Separate between failures in layer dirs or in app dirs, if we can isolate to an app dir we could debug more // to determine if there's bad permissions or bad file symlink - log = log - .section(DEBUG_INFO) - .step(&error.to_string()) - .end_section(); - - log.announce().error(&formatdoc! {" - Error caching frontend assets - - An error occurred while attempting to cache frontend assets, and the Ruby buildpack - cannot continue. - - Ensure that the permissions on the files in your application directory are correct and that - all symlinks correctly resolve. - "}); + output + .bullet(debug_info) + .sub_bullet(error.to_string()) + .done() + .error(formatdoc! {" + Error caching frontend assets + + An error occurred while attempting to cache frontend assets, and the Ruby buildpack + cannot continue. + + Ensure that the permissions on the files in your application directory are correct and that + all symlinks correctly resolve. + "}); } RubyBuildpackError::GemListGetError(error) => { - log = log - .section(DEBUG_INFO) - .step(&error.to_string()) - .end_section(); - - log = debug_cmd(log.section(DEBUG_INFO), Command::new("gem").arg("env")); - - log = debug_cmd(log.section(DEBUG_INFO), Command::new("bundle").arg("env")); - - log.announce().error(&formatdoc! {" + output = output + .bullet(&debug_info) + .sub_bullet(error.to_string()) + .done(); + + output = debug_cmd(output.bullet(&debug_info), Command::new("gem").arg("env")); + output = debug_cmd( + output.bullet(&debug_info), + Command::new("bundle").arg("env"), + ); + + output.error(formatdoc! {" Error detecting dependencies The Ruby buildpack requires information about your application’s dependencies to @@ -301,16 +296,16 @@ fn log_our_error(mut log: Box, error: RubyBuildpackError) { "}); } RubyBuildpackError::MetricsAgentError(error) => { - log.section(DEBUG_INFO) - .step(&error.to_string()) - .end_section() - .announce() - .error(&formatdoc! {" + output + .bullet(debug_info) + .sub_bullet(error.to_string()) + .done() + .error(formatdoc! {" Error: Could not install Statsd agent An error occured while downloading and installing the metrics agent the buildpack cannot continue. - "}); + "}); } } } @@ -329,7 +324,7 @@ fn cause(err: libcnb::Error) -> Cause { } fn local_command_debug(error: &CmdError) -> String { - let cmd_name = replace_app_path_with_relative(fmt::command(error.name())); + let cmd_name = replace_app_path_with_relative(style::command(error.name())); formatdoc! {" Ensure you can run the following command locally with no errors before attempting another build: @@ -345,18 +340,14 @@ fn replace_app_path_with_relative(contents: impl AsRef) -> String { app_path_re.replace_all(contents.as_ref(), "./").to_string() } -fn debug_cmd(log: Box, command: &mut Command) -> Box { - let mut stream = log.step_timed_stream(&format!( - "Running debug command {}", - fmt::command(command.name()) - )); - - match command.stream_output(stream.io(), stream.io()) { - Ok(_) => stream.finish_timed_stream().end_section(), - Err(e) => stream - .finish_timed_stream() - .step(&e.to_string()) - .end_section(), +fn debug_cmd(mut log: Print>, command: &mut Command) -> Print> { + let result = log.stream_with( + format!("Running debug command {}", style::command(command.name())), + |stdout, stderr| command.stream_output(stdout, stderr), + ); + match result { + Ok(_) => log.done(), + Err(e) => log.sub_bullet(e.to_string()).done(), } } diff --git a/commons/CHANGELOG.md b/commons/CHANGELOG.md index e2f06180..e6701efb 100644 --- a/commons/CHANGELOG.md +++ b/commons/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog for commons features +## 2024-10-30 + +## Changed + +- Deprecate `layers` including `layers::ConfigureEnvLayer` and `layers::DefaultEnvLayer` () +- Remove `AppCacheCollection` () +- Deprecate `output` module in favor of the `bullet_stream` crate () + ## 2024-08-16 ### Fixed diff --git a/commons/Cargo.toml b/commons/Cargo.toml index 579826cb..7ca76b6d 100644 --- a/commons/Cargo.toml +++ b/commons/Cargo.toml @@ -3,10 +3,6 @@ name = "commons" edition.workspace = true rust-version.workspace = true -[[bin]] -name = "print_style_guide" -path = "bin/print_style_guide.rs" - [lints] workspace = true diff --git a/commons/bin/print_style_guide.rs b/commons/bin/print_style_guide.rs deleted file mode 100644 index b180b350..00000000 --- a/commons/bin/print_style_guide.rs +++ /dev/null @@ -1,216 +0,0 @@ -// Required due to: https://github.com/rust-lang/rust/issues/95513 -#![allow(unused_crate_dependencies)] - -use ascii_table::AsciiTable; -use commons::output::fmt::{self, DEBUG_INFO, HELP}; -#[allow(clippy::wildcard_imports)] -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; - -#[allow(clippy::too_many_lines)] -fn main() { - println!( - "{}", - formatdoc! {" - - Living build output style guide - =============================== - "} - ); - - { - let mut log = BuildLog::new(stdout()).buildpack_name("Section logging features"); - log = log - .section("Section heading example") - .step("step example") - .step("step example two") - .end_section(); - - log = log - .section("Section and step description") - .step( - "A section should be a noun i.e. 'Ruby Version', consider this the section topic.", - ) - .step("A step should be a verb i.e. 'Downloading'") - .step("Related verbs should be nested under a single section") - .step( - formatdoc! {" - Steps can be multiple lines long - However they're best as short, factual, - descriptions of what the program is doing. - "} - .trim(), - ) - .step("Prefer a single line when possible") - .step("Sections and steps are sentence cased with no ending puncuation") - .step(&format!("{HELP} capitalize the first letter")) - .end_section(); - - let mut command = Command::new("bash"); - command.args(["-c", "ps aux | grep cargo"]); - - let mut stream = log.section("Timer steps") - .step("Long running code should execute with a timer printing to the UI, to indicate the progam did not hang.") - .step("Example:") - .step_timed("Background progress timer") - .finish_timed_step() - .step("Output can be streamed. Mostly from commands. Example:") - .step_timed_stream(&format!("Running {}", fmt::command(command.name()))); - - // TODO: Remove usage of unwrap(): https://github.com/heroku/buildpacks-ruby/issues/238 - #[allow(clippy::unwrap_used)] - command.stream_output(stream.io(), stream.io()).unwrap(); - log = stream.finish_timed_stream().end_section(); - drop(log); - } - - { - let mut log = BuildLog::new(stdout()).buildpack_name("Section log functions"); - log = log - .section("Logging inside a layer") - .step( - formatdoc! {" - Layer interfaces are neither mutable nor consuming i.e. - - ``` - fn create( - &self, - _context: &BuildContext, - layer_path: &Path, - ) -> Result, RubyBuildpackError> - ``` - - To allow logging within a layer you can use the `output::section_log` interface. - "} - .trim_end(), - ) - .step("This `section_log` inteface allows you to log without state") - .step("That means you're responsonsible creating a section before calling it") - .step("Here's an example") - .end_section(); - - let section_log = log.section("Example:"); - - log_step("log_step()"); - log_step_timed("log_step_timed()", || { - // do work here - }); - log_step_stream("log_step_stream()", |stream| { - // TODO: Remove usage of unwrap(): https://github.com/heroku/buildpacks-ruby/issues/238 - #[allow(clippy::unwrap_used)] - Command::new("bash") - .args(["-c", "ps aux | grep cargo"]) - .stream_output(stream.io(), stream.io()) - .unwrap() - }); - log_step(formatdoc! {" - If you want to help make sure you're within a section then you can require your layer - takes a reference to `&'a dyn SectionLogger` - "}); - section_log.end_section(); - } - - { - // TODO: Remove usage of unwrap(): https://github.com/heroku/buildpacks-ruby/issues/238 - #[allow(clippy::unwrap_used)] - let cmd_error = Command::new("iDoNotExist").named_output().err().unwrap(); - - let mut log = BuildLog::new(stdout()).buildpack_name("Error and warnings"); - log = log - .section("Debug information") - .step("Should go above errors in section/step format") - .end_section(); - - log = log - .section(DEBUG_INFO) - .step(&cmd_error.to_string()) - .end_section(); - - log.announce() - .warning(&formatdoc! {" - Warning: This is a warning header - - This is a warning body. Warnings are for when we know for a fact a problem exists - but it's not bad enough to abort the build. - "}) - .important(&formatdoc! {" - Important: This is important - - Important is for when there's critical information that needs to be read - however it may or may not be a problem. If we know for a fact that there's - a problem then use a warning instead. - - An example of something that is important but might not be a problem is - that an application owner upgraded to a new stack. - "}) - .error(&formatdoc! {" - Error: This is an error header - - This is the error body. Use an error for when the build cannot continue. - An error should include a header with a short description of why it cannot continue. - - The body should include what error state was observed, why that's a problem, and - what remediation steps an application owner using the buildpack to deploy can - take to solve the issue. - "}); - } - - { - let mut log = BuildLog::new(stdout()).buildpack_name("Formatting helpers"); - - log = log - .section("The fmt module") - .step(&formatdoc! {" - Formatting helpers can be used to enhance log output: - "}) - .end_section(); - - let mut table = AsciiTable::default(); - table.set_max_width(240); - table.column(0).set_header("Example"); - table.column(1).set_header("Code"); - table.column(2).set_header("When to use"); - - let data: Vec> = vec![ - vec![ - fmt::value("2.3.4"), - "fmt::value(\"2.3.f\")".to_string(), - "With versions, file names or other important values worth highlighting".to_string(), - ], - vec![ - fmt::url("https://www.schneems.com"), - "fmt::url(\"https://www.schneems.com\")".to_string(), - "With urls".to_string(), - ], - vec![ - fmt::command("bundle install"), - "fmt::command(command.name())".to_string(), - "With commands (alongside of `fun_run::CommandWithName`)".to_string(), - ], - vec![ - fmt::details("extra information"), - "fmt::details(\"extra information\")".to_string(), - "Add specific information at the end of a line i.e. 'Cache cleared (ruby version changed)'".to_string() - ], - vec![ - fmt::HELP.to_string(), - "fmt::HELP.to_string()".to_string(), - "A help prefix, use it in a step or section title".to_string() - ], - vec![ - fmt::DEBUG_INFO.to_string(), - "fmt::DEBUG_INFO.to_string()".to_string(), - "A debug prefix, use it in a step or section title".to_string() - ] - ]; - - table.print(data); - drop(log); - } -} diff --git a/commons/src/cache.rs b/commons/src/cache.rs index 12e8b3b6..a6735c4b 100644 --- a/commons/src/cache.rs +++ b/commons/src/cache.rs @@ -1,14 +1,10 @@ mod app_cache; -mod app_cache_collection; mod clean; mod config; mod error; -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; pub use self::config::{mib, KeepPath}; diff --git a/commons/src/cache/app_cache.rs b/commons/src/cache/app_cache.rs index cf099615..773cb9c9 100644 --- a/commons/src/cache/app_cache.rs +++ b/commons/src/cache/app_cache.rs @@ -1,10 +1,11 @@ use crate::cache::clean::{lru_clean, FilesWithSize}; -use crate::cache::in_app_dir_cache_layer::InAppDirCacheLayer; use crate::cache::{CacheConfig, CacheError, KeepPath}; use byte_unit::{AdjustedByte, Byte, UnitType}; use fs_extra::dir::CopyOptions; use libcnb::build::BuildContext; use libcnb::data::layer::LayerName; +use libcnb::layer::{CachedLayerDefinition, InvalidMetadataAction, RestoredLayerAction}; +use serde::{Deserialize, Serialize}; use std::path::Path; use std::path::PathBuf; use walkdir::WalkDir; @@ -235,6 +236,25 @@ pub enum PathState { HasFiles, } +/// # Caches a folder in the application directory +/// +/// Layers are used for caching, however layers cannot be inside of the app directory. +/// This layer can be used to hold a directory's contents so they are preserved +/// between deploys. +/// +/// The primary usecase of this is for caching assets. After `rake assets:precompile` runs +/// file in `/public/assets` need to be preserved between deploys. This allows +/// for faster deploys, and also allows for prior generated assets to remain on the system +/// until "cleaned." +/// +/// Historically, sprockets will keep 3 versions of old files on disk. This +/// allows for emails, that might live a long time, to reference a specific SHA of an +/// asset. +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] +pub(crate) struct Metadata { + pub(crate) app_dir_path: PathBuf, +} + /// Converts a `CacheConfig` into an `AppCache` /// /// Same as `AppCache::new_and_load` without loading @@ -243,7 +263,6 @@ pub enum PathState { /// # Errors /// /// - If the layer cannot be created -#[allow(deprecated)] pub fn build( context: &BuildContext, config: CacheConfig, @@ -257,11 +276,27 @@ pub fn build( let layer_name = create_layer_name(&context.app_dir, &path)?; let create_state = layer_name_cache_state(&context.layers_dir, &layer_name); - let layer = context - .handle_layer(layer_name, InAppDirCacheLayer::new(path.clone())) - .map_err(|error| CacheError::InternalLayerError(format!("{error:?}")))?; - - let cache = layer.path; + let metadata = Metadata { + app_dir_path: path.clone(), + }; + let cache = context + .cached_layer( + layer_name, + CachedLayerDefinition { + build: true, + launch: true, + invalid_metadata_action: &|_| InvalidMetadataAction::DeleteLayer, + restored_layer_action: &|old: &Metadata, _| { + if old == &metadata { + RestoredLayerAction::KeepLayer + } else { + RestoredLayerAction::DeleteLayer + } + }, + }, + ) + .map_err(|error| CacheError::InternalLayerError(format!("{error:?}"))) + .map(|layer_ref| layer_ref.path())?; Ok(AppCache { path, diff --git a/commons/src/cache/app_cache_collection.rs b/commons/src/cache/app_cache_collection.rs deleted file mode 100644 index ef11e0ad..00000000 --- a/commons/src/cache/app_cache_collection.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::cache::{AppCache, CacheConfig, CacheError, CacheState, PathState}; -use crate::output::{interface::SectionLogger, section_log as log}; -use libcnb::{build::BuildContext, Buildpack}; -use std::fmt::Debug; - -/// App Cache Collection -/// -/// Load and store multiple cache's from an application's directory. This essentially acts -/// as a group of `InAppDirCache` that run together -/// -/// Used for loading/unloading asset cache and communicating what's happening to the user. -/// -/// 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 - /// - /// # Errors - /// - /// - Error if the cache layer cannot be created - /// - Error if loading (copying) files from the cache to the app fails - /// - Error if app or cache directory cannot be created (for example, due to permissions) - pub fn new_and_load( - context: &BuildContext, - config: impl IntoIterator, - log: &'a dyn SectionLogger, - ) -> Result { - let caches = config - .into_iter() - .map(|config| { - AppCache::new_and_load(context, config).inspect(|store| { - let path = store.path().display(); - - log::log_step(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}"), - }); - }) - }) - .collect::, CacheError>>()?; - - Ok(Self { - collection: caches, - _log: log, - }) - } - - /// # Errors - /// - /// Returns an error if the cache cannot be moved or copied, for example - /// due to file permissions or another process deleting the target directory. - /// Returns an error if cleaning the cache directory cannot - /// be completed. For example due to file permissions. - pub fn save_and_clean(&self) -> Result<(), CacheError> { - for store in &self.collection { - let path = store.path().display(); - - log::log_step(match store.path_state() { - PathState::Empty => format!("Storing cache for (empty) {path}"), - PathState::HasFiles => format!("Storing cache for {path}"), - }); - - if let Some(removed) = store.save_and_clean()? { - let path = store.path().display(); - let limit = store.limit(); - let removed_len = removed.files.len(); - let removed_size = removed.adjusted_bytes(); - - log::log_step(format!( - "Detected cache size exceeded (over {limit} limit by {removed_size}) for {path}" - )); - log::log_step(format!( - "Removed {removed_len} files from the cache for {path}", - )); - } - } - - Ok(()) - } -} diff --git a/commons/src/cache/in_app_dir_cache_layer.rs b/commons/src/cache/in_app_dir_cache_layer.rs deleted file mode 100644 index 78650795..00000000 --- a/commons/src/cache/in_app_dir_cache_layer.rs +++ /dev/null @@ -1,83 +0,0 @@ -use libcnb::build::BuildContext; -use libcnb::data::layer_content_metadata::LayerTypes; -#[allow(deprecated)] -use libcnb::layer::{ExistingLayerStrategy, Layer, LayerData, LayerResult, LayerResultBuilder}; -use libcnb::Buildpack; -use serde::{Deserialize, Serialize}; -use std::marker::PhantomData; -use std::path::Path; -use std::path::PathBuf; - -/// # Caches a folder in the application directory -/// -/// Layers are used for caching, however layers cannot be inside of the app directory. -/// This layer can be used to hold a directory's contents so they are preserved -/// between deploys. -/// -/// The primary usecase of this is for caching assets. After `rake assets:precompile` runs -/// file in `/public/assets` need to be preserved between deploys. This allows -/// for faster deploys, and also allows for prior generated assets to remain on the system -/// until "cleaned." -/// -/// Historically, sprockets will keep 3 versions of old files on disk. This -/// allows for emails, that might live a long time, to reference a specific SHA of an -/// asset. -#[derive(Deserialize, Serialize, Debug, Clone)] -pub(crate) struct InAppDirCacheLayer { - pub(crate) app_dir_path: PathBuf, - buildpack: PhantomData, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub(crate) struct InAppDirCacheLayerMetadata { - app_dir_path: PathBuf, -} - -impl InAppDirCacheLayer { - pub(crate) fn new(app_dir_path: PathBuf) -> Self { - Self { - app_dir_path, - buildpack: PhantomData, - } - } -} - -#[allow(deprecated)] -impl Layer for InAppDirCacheLayer -where - B: Buildpack, -{ - type Buildpack = B; - type Metadata = InAppDirCacheLayerMetadata; - - fn types(&self) -> LayerTypes { - LayerTypes { - build: true, - launch: true, - cache: true, - } - } - - fn create( - &mut self, - _context: &BuildContext, - _layer_path: &Path, - ) -> Result, B::Error> { - LayerResultBuilder::new(InAppDirCacheLayerMetadata { - app_dir_path: self.app_dir_path.clone(), - }) - .build() - } - - fn existing_layer_strategy( - &mut self, - _context: &BuildContext, - layer_data: &LayerData, - ) -> Result { - if self.app_dir_path == layer_data.content_metadata.metadata.app_dir_path { - Ok(ExistingLayerStrategy::Keep) - } else { - Ok(ExistingLayerStrategy::Recreate) - } - } -} diff --git a/commons/src/lib.rs b/commons/src/lib.rs index 15edfa45..0f80c633 100644 --- a/commons/src/lib.rs +++ b/commons/src/lib.rs @@ -6,6 +6,10 @@ pub mod cache; pub mod display; pub mod gem_version; pub mod gemfile_lock; +#[deprecated( + since = "0.0.0", + note = "Use the struct layer API in the latest libcnb.rs instead" +)] pub mod layer; pub mod metadata_digest; pub mod output; diff --git a/commons/src/output/build_log.rs b/commons/src/output/build_log.rs index 39e0d47f..d22e9a4e 100644 --- a/commons/src/output/build_log.rs +++ b/commons/src/output/build_log.rs @@ -1,6 +1,8 @@ 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; @@ -28,10 +30,9 @@ use std::time::{Duration, Instant}; /// ``` /// /// To log inside of a layer see [`section_log`]. -/// -/// For usage details run `cargo run --bin print_style_guide` #[derive(Debug)] +#[deprecated(since = "0.0.0", note = "Use `bullet_stream` instead")] pub struct BuildLog { pub(crate) io: W, pub(crate) data: BuildData, diff --git a/commons/src/output/fmt.rs b/commons/src/output/fmt.rs index abe5c7a6..693c4f2b 100644 --- a/commons/src/output/fmt.rs +++ b/commons/src/output/fmt.rs @@ -5,25 +5,30 @@ use std::fmt::Write; /// Helpers for formatting and colorizing your output /// Decorated str for prefixing "Help:" +#[deprecated(since = "0.0.0", note = "Use `bullet_stream` instead")] pub const HELP: &str = formatcp!("{IMPORTANT_COLOR}! HELP{RESET}"); /// Decorated str for prefixing "Debug info:" +#[deprecated(since = "0.0.0", 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(since = "0.0.0", 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(since = "0.0.0", 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(since = "0.0.0", note = "Use `bullet_stream` instead")] pub fn value(contents: impl AsRef) -> String { let contents = colorize(VALUE_COLOR, contents.as_ref()); format!("`{contents}`") @@ -31,6 +36,7 @@ pub fn value(contents: impl AsRef) -> String { /// Decorate additional information at the end of a line #[must_use] +#[deprecated(since = "0.0.0", note = "Use `bullet_stream` instead")] pub fn details(contents: impl AsRef) -> String { let contents = contents.as_ref(); format!("({contents})") diff --git a/commons/src/output/mod.rs b/commons/src/output/mod.rs index 705d12ac..9a293aa4 100644 --- a/commons/src/output/mod.rs +++ b/commons/src/output/mod.rs @@ -1,7 +1,13 @@ 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 index 654d9c64..f6a1cee8 100644 --- a/commons/src/output/section_log.rs +++ b/commons/src/output/section_log.rs @@ -17,8 +17,6 @@ use std::marker::PhantomData; /// - Ensuring that you are not attempting to log while already logging i.e. calling `step()` within a /// `step_timed()` call. /// -/// For usage details run `cargo run --bin print_style_guide` -/// /// ## Use /// /// The main use case is logging inside of a layer: @@ -48,6 +46,7 @@ use std::marker::PhantomData; /// /// log_step("Clearing cache (ruby version changed)"); /// ``` +#[deprecated(since = "0.0.0", note = "Use `bullet_stream` instead")] pub fn log_step(s: impl AsRef) { logger().step(s.as_ref()); } @@ -64,6 +63,7 @@ pub fn log_step(s: impl AsRef) { /// ``` /// /// Timing information will be output at the end of the step. +#[deprecated(since = "0.0.0", 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(); @@ -88,6 +88,7 @@ pub fn log_step_timed(s: impl AsRef, f: impl FnOnce() -> T) -> T { /// ``` /// /// Timing information will be output at the end of the step. +#[deprecated(since = "0.0.0", note = "Use `bullet_stream` instead")] pub fn log_step_stream( s: impl AsRef, f: impl FnOnce(&mut Box) -> T, @@ -99,21 +100,25 @@ pub fn log_step_stream( } /// Print an error block to the output +#[deprecated(since = "0.0.0", 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(since = "0.0.0", 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(since = "0.0.0", 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(since = "0.0.0", note = "Use `bullet_stream` instead")] pub fn log_important(s: impl AsRef) { logger().announce().important(s.as_ref()); }