diff --git a/buildpacks/ruby/CHANGELOG.md b/buildpacks/ruby/CHANGELOG.md index ae2da3a3..7de9ea6e 100644 --- a/buildpacks/ruby/CHANGELOG.md +++ b/buildpacks/ruby/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `fun_run` commons library was moved to it's own crate ([#232](https://github.com/heroku/buildpacks-ruby/pull/232)) +### Added + +- Raise a helpful error when a file cannot be accessed at the time of buildpack detection ([#243](https://github.com/heroku/buildpacks-ruby/pull/243)) + + ## [2.1.2] - 2023-10-31 ### Fixed diff --git a/buildpacks/ruby/src/main.rs b/buildpacks/ruby/src/main.rs index 33e73ae8..751aa65b 100644 --- a/buildpacks/ruby/src/main.rs +++ b/buildpacks/ruby/src/main.rs @@ -5,6 +5,7 @@ use commons::output::warn_later::WarnGuard; #[allow(clippy::wildcard_imports)] use commons::output::{build_log::*, fmt}; use core::str::FromStr; +use fs_err::PathExt; use fun_run::CmdError; use layers::{ bundle_download_layer::{BundleDownloadLayer, BundleDownloadLayerMetadata}, @@ -37,6 +38,21 @@ use clap as _; struct RubyBuildpack; +#[derive(Debug, thiserror::Error)] +enum DetectError { + #[error("Cannot read Gemfile {0}")] + Gemfile(std::io::Error), + + #[error("Cannot read Gemfile.lock {0}")] + GemfileLock(std::io::Error), + + #[error("Cannot read package.json {0}")] + PackageJson(std::io::Error), + + #[error("Cannot read yarn.lock {0}")] + YarnLock(std::io::Error), +} + impl Buildpack for RubyBuildpack { type Platform = GenericPlatform; type Metadata = GenericMetadata; @@ -45,21 +61,49 @@ impl Buildpack for RubyBuildpack { fn detect(&self, context: DetectContext) -> libcnb::Result { let mut plan_builder = BuildPlanBuilder::new().provides("ruby"); - if let Ok(lockfile) = fs_err::read_to_string(context.app_dir.join("Gemfile.lock")) { + let lockfile = context.app_dir.join("Gemfile.lock"); + + if lockfile + .fs_err_try_exists() + .map_err(DetectError::GemfileLock) + .map_err(RubyBuildpackError::BuildpackDetectionError)? + { plan_builder = plan_builder.requires("ruby"); - if context.app_dir.join("package.json").exists() { + if context + .app_dir + .join("package.json") + .fs_err_try_exists() + .map_err(DetectError::PackageJson) + .map_err(RubyBuildpackError::BuildpackDetectionError)? + { plan_builder = plan_builder.requires("node"); } - if context.app_dir.join("yarn.lock").exists() { + if context + .app_dir + .join("yarn.lock") + .fs_err_try_exists() + .map_err(DetectError::YarnLock) + .map_err(RubyBuildpackError::BuildpackDetectionError)? + { plan_builder = plan_builder.requires("yarn"); } - if needs_java(&lockfile) { + if fs_err::read_to_string(lockfile) + .map_err(DetectError::GemfileLock) + .map_err(RubyBuildpackError::BuildpackDetectionError) + .map(needs_java)? + { plan_builder = plan_builder.requires("jdk"); } - } else if context.app_dir.join("Gemfile").exists() { + } else if context + .app_dir + .join("Gemfile") + .fs_err_try_exists() + .map_err(DetectError::Gemfile) + .map_err(RubyBuildpackError::BuildpackDetectionError)? + { plan_builder = plan_builder.requires("ruby"); } @@ -232,13 +276,14 @@ impl Buildpack for RubyBuildpack { } } -fn needs_java(gemfile_lock: &str) -> bool { +fn needs_java(gemfile_lock: impl AsRef) -> bool { let java_regex = regex::Regex::new(r"\(jruby ").expect("clippy"); - java_regex.is_match(gemfile_lock) + java_regex.is_match(gemfile_lock.as_ref()) } #[derive(Debug)] -enum RubyBuildpackError { +pub(crate) enum RubyBuildpackError { + BuildpackDetectionError(DetectError), RakeDetectError(CmdError), GemListGetError(CmdError), RubyInstallError(RubyInstallError), diff --git a/buildpacks/ruby/src/user_errors.rs b/buildpacks/ruby/src/user_errors.rs index 3a8a43ee..78c7c501 100644 --- a/buildpacks/ruby/src/user_errors.rs +++ b/buildpacks/ruby/src/user_errors.rs @@ -6,7 +6,7 @@ use commons::output::{ fmt::{self, DEBUG_INFO}, }; -use crate::RubyBuildpackError; +use crate::{DetectError, RubyBuildpackError}; use fun_run::{CmdError, CommandWithName}; use indoc::formatdoc; @@ -45,6 +45,62 @@ fn log_our_error(mut log: Box, error: RubyBuildpackError) { let rubygems_status_url = fmt::url("https://status.rubygems.org/"); match error { + RubyBuildpackError::BuildpackDetectionError(DetectError::Gemfile(error)) => { + log.announce().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. + + {error} + + Debug using the above information and try again. + "}); + } + RubyBuildpackError::BuildpackDetectionError(DetectError::PackageJson(error)) => { + log.announce().error(&formatdoc! {" + Error: `package.json` found with error + + The Ruby buildpack detected a package.json file but it is not readable + due to the following errors: + + {error} + + If your application does not need any node dependencies installed you + may delete this file and try again. + + If you are expecting node dependencies to be installed, please + debug using the above information and try again. + "}); + } + RubyBuildpackError::BuildpackDetectionError(DetectError::GemfileLock(error)) => { + log.announce().error(&formatdoc! {" + Error: `Gemfile.lock` found with error + + There was an error trying to read the contents of the application's Gemfile.lock. + The buildpack cannot continue if the Gemfile is unreadable. + + {error} + + Debug using the above information and try again. + "}); + } + RubyBuildpackError::BuildpackDetectionError(DetectError::YarnLock(error)) => { + log.announce().error(&formatdoc! {" + Error: `yarn.lock` found with error + + The Ruby buildpack detected a yarn.lock file but it is not readable + due to the following errors: + + {error} + + If your application does not need any yarn installed you + may delete this file and try again. + + If you are expecting yarn to be installed, please + debug using the above information and try again. + "}); + } RubyBuildpackError::MissingGemfileLock(path, error) => { log = log .section(&format!(