diff --git a/buildpacks/gradle/src/main.rs b/buildpacks/gradle/src/main.rs index 2cc7d4f1..dfc8d2af 100644 --- a/buildpacks/gradle/src/main.rs +++ b/buildpacks/gradle/src/main.rs @@ -7,8 +7,8 @@ use crate::layers::gradle_home::handle_gradle_home_layer; use crate::GradleBuildpackError::{GradleBuildIoError, GradleBuildUnexpectedStatusError}; use buildpacks_jvm_shared as shared; use buildpacks_jvm_shared::output::{ - print_buildpack_name, print_section, print_subsection, track_timing, BuildpackOutputText, - BuildpackOutputTextSection, + print_buildpack_name, print_section, print_subsection, track_buildpack_timing, + track_subsection_timing, BuildpackOutputText, BuildpackOutputTextSection, }; #[cfg(test)] use buildpacks_jvm_shared_test as _; @@ -78,83 +78,85 @@ impl Buildpack for GradleBuildpack { } fn build(&self, context: BuildContext) -> libcnb::Result { - print_buildpack_name("Heroku Gradle Buildpack"); - - let buildpack_config = GradleBuildpackConfig::from(&context); - - let gradle_wrapper_executable_path = Some(context.app_dir.join("gradlew")) - .filter(|path| path.exists()) - .ok_or(GradleBuildpackError::GradleWrapperNotFound)?; - - shared::fs::set_executable(&gradle_wrapper_executable_path) - .map_err(GradleBuildpackError::CannotSetGradleWrapperExecutableBit)?; - - let mut gradle_env = Env::from_current(); - handle_gradle_home_layer(&context, &mut gradle_env)?; - - print_section("Running Gradle build"); - - track_timing(|| { - print_subsection("Starting Gradle daemon"); - gradle_command::start_daemon(&gradle_wrapper_executable_path, &gradle_env) - .map_err(GradleBuildpackError::StartGradleDaemonError) - })?; - - let project_tasks = track_timing(|| { - print_subsection("Querying tasks"); - gradle_command::tasks(&context.app_dir, &gradle_env) - .map_err(|command_error| command_error.map_parse_error(|_| ())) - .map_err(GradleBuildpackError::GetTasksError) - })?; - - let dependency_report = track_timing(|| { - print_subsection("Querying dependency report"); - gradle_command::dependency_report(&context.app_dir, &gradle_env) - .map_err(GradleBuildpackError::GetDependencyReportError) - })?; - - let task_name = buildpack_config - .gradle_task - .as_deref() - .or_else(|| project_tasks.has_task("stage").then_some("stage")) - .or_else(|| { - detect_framework(&dependency_report).map(|framework| match framework { - Framework::SpringBoot | Framework::Quarkus => "build", - Framework::Ratpack => "installDist", - Framework::Micronaut => "shadowJar", + track_buildpack_timing(|| { + print_buildpack_name("Heroku Gradle Buildpack"); + + let buildpack_config = GradleBuildpackConfig::from(&context); + + let gradle_wrapper_executable_path = Some(context.app_dir.join("gradlew")) + .filter(|path| path.exists()) + .ok_or(GradleBuildpackError::GradleWrapperNotFound)?; + + shared::fs::set_executable(&gradle_wrapper_executable_path) + .map_err(GradleBuildpackError::CannotSetGradleWrapperExecutableBit)?; + + let mut gradle_env = Env::from_current(); + handle_gradle_home_layer(&context, &mut gradle_env)?; + + print_section("Running Gradle build"); + + track_subsection_timing(|| { + print_subsection("Starting Gradle daemon"); + gradle_command::start_daemon(&gradle_wrapper_executable_path, &gradle_env) + .map_err(GradleBuildpackError::StartGradleDaemonError) + })?; + + let project_tasks = track_subsection_timing(|| { + print_subsection("Querying tasks"); + gradle_command::tasks(&context.app_dir, &gradle_env) + .map_err(|command_error| command_error.map_parse_error(|_| ())) + .map_err(GradleBuildpackError::GetTasksError) + })?; + + let dependency_report = track_subsection_timing(|| { + print_subsection("Querying dependency report"); + gradle_command::dependency_report(&context.app_dir, &gradle_env) + .map_err(GradleBuildpackError::GetDependencyReportError) + })?; + + let task_name = buildpack_config + .gradle_task + .as_deref() + .or_else(|| project_tasks.has_task("stage").then_some("stage")) + .or_else(|| { + detect_framework(&dependency_report).map(|framework| match framework { + Framework::SpringBoot | Framework::Quarkus => "build", + Framework::Ratpack => "installDist", + Framework::Micronaut => "shadowJar", + }) }) - }) - .ok_or(GradleBuildpackError::BuildTaskUnknown)?; - - print_section("Running Gradle build"); - print_subsection(BuildpackOutputText::new(vec![ - BuildpackOutputTextSection::regular("Running "), - BuildpackOutputTextSection::command(format!("./gradlew {task_name} -x check")), - ])); - - let output = Command::new(&gradle_wrapper_executable_path) - .current_dir(&context.app_dir) - .envs(&gradle_env) - .args([task_name, "-x", "check"]) - .output_and_write_streams(stdout(), stderr()) - .map_err(GradleBuildIoError)?; - - if !output.status.success() { - Err(GradleBuildUnexpectedStatusError(output.status))?; - } - - // Explicitly ignoring the result. If the daemon cannot be stopped, that is not a build - // failure, nor can we recover from it in any way. - let _ = gradle_command::stop_daemon(&gradle_wrapper_executable_path, &gradle_env); - - let process = default_app_process(&dependency_report, &context.app_dir) - .map_err(GradleBuildpackError::CannotDetermineDefaultAppProcess)?; - - process - .map_or(BuildResultBuilder::new(), |process| { - BuildResultBuilder::new().launch(LaunchBuilder::new().process(process).build()) - }) - .build() + .ok_or(GradleBuildpackError::BuildTaskUnknown)?; + + print_section("Running Gradle build"); + print_subsection(BuildpackOutputText::new(vec![ + BuildpackOutputTextSection::regular("Running "), + BuildpackOutputTextSection::command(format!("./gradlew {task_name} -x check")), + ])); + + let output = Command::new(&gradle_wrapper_executable_path) + .current_dir(&context.app_dir) + .envs(&gradle_env) + .args([task_name, "-x", "check"]) + .output_and_write_streams(stdout(), stderr()) + .map_err(GradleBuildIoError)?; + + if !output.status.success() { + Err(GradleBuildUnexpectedStatusError(output.status))?; + } + + // Explicitly ignoring the result. If the daemon cannot be stopped, that is not a build + // failure, nor can we recover from it in any way. + let _ = gradle_command::stop_daemon(&gradle_wrapper_executable_path, &gradle_env); + + let process = default_app_process(&dependency_report, &context.app_dir) + .map_err(GradleBuildpackError::CannotDetermineDefaultAppProcess)?; + + process + .map_or(BuildResultBuilder::new(), |process| { + BuildResultBuilder::new().launch(LaunchBuilder::new().process(process).build()) + }) + .build() + }) } fn on_error(&self, error: libcnb::Error) { diff --git a/buildpacks/jvm/src/layers/openjdk.rs b/buildpacks/jvm/src/layers/openjdk.rs index 0b72cbde..6b027b9c 100644 --- a/buildpacks/jvm/src/layers/openjdk.rs +++ b/buildpacks/jvm/src/layers/openjdk.rs @@ -79,7 +79,7 @@ pub(crate) fn handle_openjdk_layer( _ => {} } - output::track_timing(|| { + output::track_subsection_timing(|| { output::print_subsection("Downloading and unpacking OpenJDK distribution"); let temp_dir = @@ -127,7 +127,7 @@ pub(crate) fn handle_openjdk_layer( jdk_overlay_applied = true; - output::track_timing(|| { + output::track_subsection_timing(|| { let jdk_overlay_contents = util::list_directory_contents(&app_jdk_overlay_dir_path) .map_err(OpenJdkBuildpackError::CannotListJdkOverlayContents)?; diff --git a/buildpacks/jvm/src/main.rs b/buildpacks/jvm/src/main.rs index 7ab8f74e..dd62fcd3 100644 --- a/buildpacks/jvm/src/main.rs +++ b/buildpacks/jvm/src/main.rs @@ -19,7 +19,9 @@ use crate::version_resolver::{ resolve_version, OpenJdkArtifactRequirementSource, VersionResolveError, }; use buildpacks_jvm_shared::output; -use buildpacks_jvm_shared::output::{BuildpackOutputText, BuildpackOutputTextSection}; +use buildpacks_jvm_shared::output::{ + track_buildpack_timing, BuildpackOutputText, BuildpackOutputTextSection, +}; use buildpacks_jvm_shared::system_properties::{read_system_properties, ReadSystemPropertiesError}; #[cfg(test)] use buildpacks_jvm_shared_test as _; @@ -92,18 +94,19 @@ impl Buildpack for OpenJdkBuildpack { } fn build(&self, context: BuildContext) -> libcnb::Result { - output::print_buildpack_name("Heroku OpenJDK Buildpack"); - - let resolved_version = resolve_version(&context.app_dir) - .map_err(OpenJdkBuildpackError::ResolveVersionError)?; - - if matches!( - resolved_version.source, - OpenJdkArtifactRequirementSource::DefaultVersionLatestLts - ) { - output::print_warning( - "No OpenJDK version specified", - formatdoc! {" + track_buildpack_timing(|| { + output::print_buildpack_name("Heroku OpenJDK Buildpack"); + + let resolved_version = resolve_version(&context.app_dir) + .map_err(OpenJdkBuildpackError::ResolveVersionError)?; + + if matches!( + resolved_version.source, + OpenJdkArtifactRequirementSource::DefaultVersionLatestLts + ) { + output::print_warning( + "No OpenJDK version specified", + formatdoc! {" Your application does not explicitly specify an OpenJDK version. The latest long-term support (LTS) version will be installed. This currently is OpenJDK {OPENJDK_LATEST_LTS_VERSION}. @@ -115,74 +118,79 @@ impl Buildpack for OpenJdkBuildpack { directory of your application to contain: java.runtime.version = {OPENJDK_LATEST_LTS_VERSION}"}, - ); - } - - output::print_section("OpenJDK version resolution"); - - match resolved_version.source { - OpenJdkArtifactRequirementSource::SystemProperties => { - output::print_subsection(BuildpackOutputText::new(vec![ - BuildpackOutputTextSection::regular("Using version string provided in "), - BuildpackOutputTextSection::value("system.properties"), - ])); + ); } - OpenJdkArtifactRequirementSource::DefaultVersionLatestLts => { - output::print_subsection("No explicit configuration found, using latest LTS"); - } - OpenJdkArtifactRequirementSource::DefaultVersionFunctions => { - output::print_subsection(BuildpackOutputText::new(vec![ - BuildpackOutputTextSection::regular("No explicit configuration found, using "), - BuildpackOutputTextSection::value("8"), - ])); - } - }; - - let openjdk_inventory = include_str!("../openjdk_inventory.toml") - .parse::>() - .map_err(OpenJdkBuildpackError::ParseInventoryError)?; - - let openjdk_artifact = openjdk_inventory - .partial_resolve( - context - .target - .os - .parse::() - .expect("OS should be always parseable, buildpack will not run on unsupported operating systems."), - // On platform API <= `0.9` together with lifecycle <= `0.17`, the `CNB_TARGET_ARCH` environment variable will not be set. - // This will be the case for the `salesforce-functions` builder. To ensure this buildpack can run there, we will - // fall back to Rust's architecture constant when the architecture cannot be determined. This workaround can be removed when - // the `salesforce-functions` builder is EOL. - Some(context.target.arch.as_str()) - .filter(|value| !value.is_empty()) - .unwrap_or(consts::ARCH) - .parse::() - .expect("arch should be always parseable, buildpack will not run on unsupported architectures."), - &resolved_version.requirement, - ) - .ok_or(OpenJdkBuildpackError::UnsupportedOpenJdkVersion( - resolved_version.requirement.clone(), - ))?; - - output::print_subsection(match resolved_version.requirement.version { - HerokuOpenJdkVersionRequirement::Major(major_version) => { - BuildpackOutputText::new(vec![ - BuildpackOutputTextSection::regular("Selected major version "), - BuildpackOutputTextSection::value(format!("{major_version}")), - BuildpackOutputTextSection::regular(" resolves to "), - BuildpackOutputTextSection::value(format!("{}", openjdk_artifact.version)), - ]) - } - HerokuOpenJdkVersionRequirement::Specific(version) => BuildpackOutputText::new(vec![ - BuildpackOutputTextSection::regular("Selected version "), - BuildpackOutputTextSection::value(format!("{version}")), - ]), - }); - - handle_openjdk_layer(&context, openjdk_artifact)?; - handle_runtime_layer(&context)?; - BuildResultBuilder::new().build() + output::print_section("OpenJDK version resolution"); + + match resolved_version.source { + OpenJdkArtifactRequirementSource::SystemProperties => { + output::print_subsection(BuildpackOutputText::new(vec![ + BuildpackOutputTextSection::regular("Using version string provided in "), + BuildpackOutputTextSection::value("system.properties"), + ])); + } + OpenJdkArtifactRequirementSource::DefaultVersionLatestLts => { + output::print_subsection("No explicit configuration found, using latest LTS"); + } + OpenJdkArtifactRequirementSource::DefaultVersionFunctions => { + output::print_subsection(BuildpackOutputText::new(vec![ + BuildpackOutputTextSection::regular( + "No explicit configuration found, using ", + ), + BuildpackOutputTextSection::value("8"), + ])); + } + }; + + let openjdk_inventory = include_str!("../openjdk_inventory.toml") + .parse::>() + .map_err(OpenJdkBuildpackError::ParseInventoryError)?; + + let openjdk_artifact = openjdk_inventory + .partial_resolve( + context + .target + .os + .parse::() + .expect("OS should be always parseable, buildpack will not run on unsupported operating systems."), + // On platform API <= `0.9` together with lifecycle <= `0.17`, the `CNB_TARGET_ARCH` environment variable will not be set. + // This will be the case for the `salesforce-functions` builder. To ensure this buildpack can run there, we will + // fall back to Rust's architecture constant when the architecture cannot be determined. This workaround can be removed when + // the `salesforce-functions` builder is EOL. + Some(context.target.arch.as_str()) + .filter(|value| !value.is_empty()) + .unwrap_or(consts::ARCH) + .parse::() + .expect("arch should be always parseable, buildpack will not run on unsupported architectures."), + &resolved_version.requirement, + ) + .ok_or(OpenJdkBuildpackError::UnsupportedOpenJdkVersion( + resolved_version.requirement.clone(), + ))?; + + output::print_subsection(match resolved_version.requirement.version { + HerokuOpenJdkVersionRequirement::Major(major_version) => { + BuildpackOutputText::new(vec![ + BuildpackOutputTextSection::regular("Selected major version "), + BuildpackOutputTextSection::value(format!("{major_version}")), + BuildpackOutputTextSection::regular(" resolves to "), + BuildpackOutputTextSection::value(format!("{}", openjdk_artifact.version)), + ]) + } + HerokuOpenJdkVersionRequirement::Specific(version) => { + BuildpackOutputText::new(vec![ + BuildpackOutputTextSection::regular("Selected version "), + BuildpackOutputTextSection::value(format!("{version}")), + ]) + } + }); + + handle_openjdk_layer(&context, openjdk_artifact)?; + handle_runtime_layer(&context)?; + + BuildResultBuilder::new().build() + }) } fn on_error(&self, error: libcnb::Error) { diff --git a/buildpacks/maven/src/main.rs b/buildpacks/maven/src/main.rs index b746711b..8a2e582c 100644 --- a/buildpacks/maven/src/main.rs +++ b/buildpacks/maven/src/main.rs @@ -22,7 +22,9 @@ use std::path::{Path, PathBuf}; use std::process::{Command, ExitStatus}; use buildpacks_jvm_shared::output; -use buildpacks_jvm_shared::output::{BuildpackOutputText, BuildpackOutputTextSection}; +use buildpacks_jvm_shared::output::{ + track_buildpack_timing, BuildpackOutputText, BuildpackOutputTextSection, +}; #[cfg(test)] use buildpacks_jvm_shared_test as _; #[cfg(test)] @@ -102,191 +104,193 @@ impl Buildpack for MavenBuildpack { #[allow(clippy::too_many_lines)] fn build(&self, context: BuildContext) -> libcnb::Result { - output::print_buildpack_name("Heroku Maven Buildpack"); + track_buildpack_timing(|| { + output::print_buildpack_name("Heroku Maven Buildpack"); - let mut current_or_platform_env = Env::from_current(); - for (key, value) in context.platform.env() { - current_or_platform_env.insert(key, value); - } + let mut current_or_platform_env = Env::from_current(); + for (key, value) in context.platform.env() { + current_or_platform_env.insert(key, value); + } - let mut mvn_env = Env::from_current(); - handle_maven_repository_layer(&context, &mut mvn_env)?; + let mut mvn_env = Env::from_current(); + handle_maven_repository_layer(&context, &mut mvn_env)?; - let maven_mode = determine_mode( - &context.app_dir, - &context.buildpack_descriptor.metadata.default_version, - ) - .map_err(MavenBuildpackError::DetermineModeError)?; + let maven_mode = determine_mode( + &context.app_dir, + &context.buildpack_descriptor.metadata.default_version, + ) + .map_err(MavenBuildpackError::DetermineModeError)?; - output::print_section("Installing Maven"); + output::print_section("Installing Maven"); - let mvn_executable = match maven_mode { - Mode::UseWrapper => { - output::print_subsection("Skipping (Maven wrapper detected)"); + let mvn_executable = match maven_mode { + Mode::UseWrapper => { + output::print_subsection("Skipping (Maven wrapper detected)"); - let maven_wrapper_path = context.app_dir.join("mvnw"); + let maven_wrapper_path = context.app_dir.join("mvnw"); - fs::set_permissions(maven_wrapper_path, Permissions::from_mode(0o777)) - .map_err(MavenBuildpackError::CannotSetMavenWrapperExecutableBit)?; + fs::set_permissions(maven_wrapper_path, Permissions::from_mode(0o777)) + .map_err(MavenBuildpackError::CannotSetMavenWrapperExecutableBit)?; - PathBuf::from("./mvnw") - } - Mode::InstallVersion { - version, - warn_about_unused_maven_wrapper, - warn_about_default_version, - } => { - if warn_about_unused_maven_wrapper { - log_unused_maven_wrapper_warning(&version); + PathBuf::from("./mvnw") } - - if warn_about_default_version { - log_default_maven_version_warning(&version); + Mode::InstallVersion { + version, + warn_about_unused_maven_wrapper, + warn_about_default_version, + } => { + if warn_about_unused_maven_wrapper { + log_unused_maven_wrapper_warning(&version); + } + + if warn_about_default_version { + log_default_maven_version_warning(&version); + } + + output::print_subsection(BuildpackOutputText::new(vec![ + BuildpackOutputTextSection::regular("Selected Maven version "), + BuildpackOutputTextSection::value(&version), + ])); + + output::track_subsection_timing(|| { + let tarball = context + .buildpack_descriptor + .metadata + .tarballs + .get(&version) + .cloned() + .ok_or_else(|| { + MavenBuildpackError::UnsupportedMavenVersion(version.clone()) + })?; + + handle_maven_layer(&context, &tarball, &mut mvn_env) + })?; + + PathBuf::from("mvn") } + }; - output::print_subsection(BuildpackOutputText::new(vec![ - BuildpackOutputTextSection::regular("Selected Maven version "), - BuildpackOutputTextSection::value(&version), - ])); - - output::track_timing(|| { - let tarball = context - .buildpack_descriptor - .metadata - .tarballs - .get(&version) - .cloned() - .ok_or_else(|| { - MavenBuildpackError::UnsupportedMavenVersion(version.clone()) - })?; - - handle_maven_layer(&context, &tarball, &mut mvn_env) - })?; - - PathBuf::from("mvn") + if let Some(java_home) = current_or_platform_env.get("JAVA_HOME") { + mvn_env.insert("JAVA_HOME", java_home); } - }; - - if let Some(java_home) = current_or_platform_env.get("JAVA_HOME") { - mvn_env.insert("JAVA_HOME", java_home); - } - let maven_goals = current_or_platform_env - .get("MAVEN_CUSTOM_GOALS") - .map_or_else( - || Ok(default_maven_goals()), - |maven_custom_goals_string| { - shell_words::split(&maven_custom_goals_string.to_string_lossy()) - .map_err(MavenBuildpackError::CannotSplitMavenCustomGoals) - }, - )?; - - let mut maven_options = current_or_platform_env - .get("MAVEN_CUSTOM_OPTS") - .map_or_else( - || Ok(default_maven_opts()), - |maven_custom_opts_string| { - // Since this is a single environment variable, when users want to add multiple - // options, they will expect them to be split like a UNIX shell would. This means - // we need to support proper escaping for options that contain spaces. - shell_words::split(&maven_custom_opts_string.to_string_lossy()) - .map_err(MavenBuildpackError::CannotSplitMavenCustomOpts) - }, - )?; - - let settings_xml_path = - resolve_settings_xml_path(&context.app_dir, ¤t_or_platform_env) - .map_err(MavenBuildpackError::SettingsError)?; - - if let Some(settings_xml_path) = settings_xml_path { - maven_options.push(String::from("-s")); - maven_options.push(settings_xml_path.to_string_lossy().to_string()); - } + let maven_goals = current_or_platform_env + .get("MAVEN_CUSTOM_GOALS") + .map_or_else( + || Ok(default_maven_goals()), + |maven_custom_goals_string| { + shell_words::split(&maven_custom_goals_string.to_string_lossy()) + .map_err(MavenBuildpackError::CannotSplitMavenCustomGoals) + }, + )?; + + let mut maven_options = current_or_platform_env + .get("MAVEN_CUSTOM_OPTS") + .map_or_else( + || Ok(default_maven_opts()), + |maven_custom_opts_string| { + // Since this is a single environment variable, when users want to add multiple + // options, they will expect them to be split like a UNIX shell would. This means + // we need to support proper escaping for options that contain spaces. + shell_words::split(&maven_custom_opts_string.to_string_lossy()) + .map_err(MavenBuildpackError::CannotSplitMavenCustomOpts) + }, + )?; + + let settings_xml_path = + resolve_settings_xml_path(&context.app_dir, ¤t_or_platform_env) + .map_err(MavenBuildpackError::SettingsError)?; + + if let Some(settings_xml_path) = settings_xml_path { + maven_options.push(String::from("-s")); + maven_options.push(settings_xml_path.to_string_lossy().to_string()); + } - // We need to set some options that relate to buildpack implementation internals. Those - // options must not be overridden by the user via MAVEN_CUSTOM_OPTS for the buildpack to - // work correctly. We also don't want to show them when we log the Maven command we're - // running since they might be confusing to the user. - let internal_maven_options = vec![String::from("-B")]; - - output::print_section("Running Maven build"); - output::print_subsection(BuildpackOutputText::new(vec![ - BuildpackOutputTextSection::regular("Running "), - BuildpackOutputTextSection::command(format!( - "{} {} {}", - mvn_executable.to_string_lossy(), - shell_words::join(&maven_options), - shell_words::join(&maven_goals) - )), - ])); - - output::track_timing(|| { - let mut command = Command::new(&mvn_executable); - - command - .current_dir(&context.app_dir) - .args( - maven_options - .iter() - .chain(&internal_maven_options) - .chain(&maven_goals), + // We need to set some options that relate to buildpack implementation internals. Those + // options must not be overridden by the user via MAVEN_CUSTOM_OPTS for the buildpack to + // work correctly. We also don't want to show them when we log the Maven command we're + // running since they might be confusing to the user. + let internal_maven_options = vec![String::from("-B")]; + + output::print_section("Running Maven build"); + output::print_subsection(BuildpackOutputText::new(vec![ + BuildpackOutputTextSection::regular("Running "), + BuildpackOutputTextSection::command(format!( + "{} {} {}", + mvn_executable.to_string_lossy(), + shell_words::join(&maven_options), + shell_words::join(&maven_goals) + )), + ])); + + output::track_subsection_timing(|| { + let mut command = Command::new(&mvn_executable); + + command + .current_dir(&context.app_dir) + .args( + maven_options + .iter() + .chain(&internal_maven_options) + .chain(&maven_goals), + ) + .envs(&mvn_env); + + output::run_command( + command, + false, + MavenBuildpackError::MavenBuildIoError, + |output| MavenBuildpackError::MavenBuildUnexpectedExitCode(output.status), ) - .envs(&mvn_env); - - output::run_command( - command, - false, - MavenBuildpackError::MavenBuildIoError, - |output| MavenBuildpackError::MavenBuildUnexpectedExitCode(output.status), - ) - })?; - - output::print_section(BuildpackOutputText::new(vec![ - BuildpackOutputTextSection::regular("Running "), - BuildpackOutputTextSection::value(format!( - "{} dependency:list", - mvn_executable.to_string_lossy() - )), - BuildpackOutputTextSection::regular(" quietly"), - ])); - - output::track_timing(|| { - let mut command = Command::new(&mvn_executable); - - command - .current_dir(&context.app_dir) - .args( - maven_options.iter().chain(&internal_maven_options).chain( - [ - format!( - "-DoutputFile={}", - app_dependency_list_path(&context.app_dir).to_string_lossy() - ), - String::from("dependency:list"), - ] - .iter(), - ), + })?; + + output::print_section(BuildpackOutputText::new(vec![ + BuildpackOutputTextSection::regular("Running "), + BuildpackOutputTextSection::value(format!( + "{} dependency:list", + mvn_executable.to_string_lossy() + )), + BuildpackOutputTextSection::regular(" quietly"), + ])); + + output::track_subsection_timing(|| { + let mut command = Command::new(&mvn_executable); + + command + .current_dir(&context.app_dir) + .args( + maven_options.iter().chain(&internal_maven_options).chain( + [ + format!( + "-DoutputFile={}", + app_dependency_list_path(&context.app_dir).to_string_lossy() + ), + String::from("dependency:list"), + ] + .iter(), + ), + ) + .envs(&mvn_env); + + output::run_command( + command, + true, + MavenBuildpackError::MavenBuildIoError, + |output| MavenBuildpackError::MavenBuildUnexpectedExitCode(output.status), ) - .envs(&mvn_env); + })?; - output::run_command( - command, - true, - MavenBuildpackError::MavenBuildIoError, - |output| MavenBuildpackError::MavenBuildUnexpectedExitCode(output.status), - ) - })?; + let mut build_result_builder = BuildResultBuilder::new(); - let mut build_result_builder = BuildResultBuilder::new(); - - if let Some(process) = framework::default_app_process(&context.app_dir) - .map_err(MavenBuildpackError::DefaultAppProcessError)? - { - build_result_builder = - build_result_builder.launch(LaunchBuilder::new().process(process).build()); - } + if let Some(process) = framework::default_app_process(&context.app_dir) + .map_err(MavenBuildpackError::DefaultAppProcessError)? + { + build_result_builder = + build_result_builder.launch(LaunchBuilder::new().process(process).build()); + } - build_result_builder.build() + build_result_builder.build() + }) } fn on_error(&self, error: Error) { diff --git a/buildpacks/sbt/src/main.rs b/buildpacks/sbt/src/main.rs index 86177c71..e33f741d 100644 --- a/buildpacks/sbt/src/main.rs +++ b/buildpacks/sbt/src/main.rs @@ -23,7 +23,9 @@ use libherokubuildpack::error::on_error as on_buildpack_error; use std::process::Command; use buildpacks_jvm_shared::output; -use buildpacks_jvm_shared::output::{BuildpackOutputText, BuildpackOutputTextSection}; +use buildpacks_jvm_shared::output::{ + track_buildpack_timing, BuildpackOutputText, BuildpackOutputTextSection, +}; #[cfg(test)] use buildpacks_jvm_shared_test as _; #[cfg(test)] @@ -60,75 +62,80 @@ impl Buildpack for SbtBuildpack { } fn build(&self, context: BuildContext) -> libcnb::Result { - output::print_buildpack_name("Heroku sbt Buildpack"); - - let buildpack_configuration = read_system_properties(&context.app_dir) - .map_err(SbtBuildpackError::ReadSystemPropertiesError) - .and_then(|system_properties| { - read_sbt_buildpack_configuration(&system_properties, context.platform.env()) - .map_err(SbtBuildpackError::ReadSbtBuildpackConfigurationError) + track_buildpack_timing(|| { + output::print_buildpack_name("Heroku sbt Buildpack"); + + let buildpack_configuration = read_system_properties(&context.app_dir) + .map_err(SbtBuildpackError::ReadSystemPropertiesError) + .and_then(|system_properties| { + read_sbt_buildpack_configuration(&system_properties, context.platform.env()) + .map_err(SbtBuildpackError::ReadSbtBuildpackConfigurationError) + })?; + + let sbt_version = sbt::version::read_sbt_version(&context.app_dir) + .map_err(SbtBuildpackError::ReadSbtVersionError) + .and_then(|version| version.ok_or(SbtBuildpackError::UnknownSbtVersion))?; + + if !sbt::version::is_supported_sbt_version(&sbt_version) { + Err(SbtBuildpackError::UnsupportedSbtVersion( + sbt_version.clone(), + ))?; + } + + let sbt_available_at_launch = buildpack_configuration + .sbt_available_at_launch + .unwrap_or_default(); + + let mut env = Env::from_current(); + + handle_dependency_resolver_home( + &context, + sbt_available_at_launch, + DependencyResolver::Ivy, + &mut env, + )?; + + handle_dependency_resolver_home( + &context, + sbt_available_at_launch, + DependencyResolver::Coursier, + &mut env, + )?; + + handle_sbt_extras(&context, sbt_available_at_launch, &mut env)?; + handle_sbt_boot(&context, sbt_version, sbt_available_at_launch, &mut env)?; + handle_sbt_global(&context, sbt_available_at_launch, &mut env)?; + + let tasks = sbt::tasks::from_config(&buildpack_configuration); + + output::track_subsection_timing(|| { + output::print_section("Running sbt build"); + output::print_subsection(BuildpackOutputText::new(vec![ + BuildpackOutputTextSection::regular("Running "), + BuildpackOutputTextSection::command(format!( + "sbt {}", + shell_words::join(&tasks) + )), + ])); + + let mut command = Command::new("sbt"); + command.current_dir(&context.app_dir).args(tasks).envs(&env); + + output::run_command( + command, + false, + SbtBuildpackError::SbtBuildIoError, + |output| { + SbtBuildpackError::SbtBuildUnexpectedExitStatus( + output.status, + sbt::output::parse_errors(&output.stdout), + ) + }, + ) })?; - let sbt_version = sbt::version::read_sbt_version(&context.app_dir) - .map_err(SbtBuildpackError::ReadSbtVersionError) - .and_then(|version| version.ok_or(SbtBuildpackError::UnknownSbtVersion))?; - - if !sbt::version::is_supported_sbt_version(&sbt_version) { - Err(SbtBuildpackError::UnsupportedSbtVersion( - sbt_version.clone(), - ))?; - } - - let sbt_available_at_launch = buildpack_configuration - .sbt_available_at_launch - .unwrap_or_default(); - - let mut env = Env::from_current(); - - handle_dependency_resolver_home( - &context, - sbt_available_at_launch, - DependencyResolver::Ivy, - &mut env, - )?; - - handle_dependency_resolver_home( - &context, - sbt_available_at_launch, - DependencyResolver::Coursier, - &mut env, - )?; - - handle_sbt_extras(&context, sbt_available_at_launch, &mut env)?; - handle_sbt_boot(&context, sbt_version, sbt_available_at_launch, &mut env)?; - handle_sbt_global(&context, sbt_available_at_launch, &mut env)?; - - let tasks = sbt::tasks::from_config(&buildpack_configuration); - - output::track_timing(|| { - output::print_section("Running sbt build"); - output::print_subsection(BuildpackOutputText::new(vec![ - BuildpackOutputTextSection::regular("Running "), - BuildpackOutputTextSection::command(format!("sbt {}", shell_words::join(&tasks))), - ])); - - let mut command = Command::new("sbt"); - command.current_dir(&context.app_dir).args(tasks).envs(&env); - - output::run_command( - command, - false, - SbtBuildpackError::SbtBuildIoError, - |output| { - SbtBuildpackError::SbtBuildUnexpectedExitStatus( - output.status, - sbt::output::parse_errors(&output.stdout), - ) - }, - ) - })?; - - BuildResultBuilder::new().build() + BuildResultBuilder::new().build() + }) } fn on_error(&self, error: Error) { diff --git a/shared/src/output.rs b/shared/src/output.rs index 6ef63528..fe9cf6ed 100644 --- a/shared/src/output.rs +++ b/shared/src/output.rs @@ -22,6 +22,13 @@ pub fn print_timing_done_subsection(duration: &Duration) { println!("{ANSI_RESET_CODE} - Done ({})", format_duration(duration)); } +pub fn print_buildpack_done(duration: &Duration) { + println!( + "{ANSI_RESET_CODE}- Done (finished in {})", + format_duration(duration) + ); +} + pub fn print_warning(title: impl AsRef, body: impl Into) { let title = title.as_ref(); @@ -264,16 +271,33 @@ impl From> for BuildpackOutputText { } } -pub fn track_timing(f: F) -> Result +pub fn track_subsection_timing(f: F) -> Result +where + F: FnOnce() -> Result, +{ + let (result, duration) = track_timing(f); + print_timing_done_subsection(&duration); + result +} + +pub fn track_buildpack_timing(f: F) -> Result +where + F: FnOnce() -> Result, +{ + let (result, duration) = track_timing(f); + print_buildpack_done(&duration); + result +} + +pub fn track_timing(f: F) -> (Result, Duration) where F: FnOnce() -> Result, { let start_time = Instant::now(); - let ret = f(); + let result = f(); let end_time = Instant::now(); - print_timing_done_subsection(&end_time.duration_since(start_time)); - ret + (result, end_time.duration_since(start_time)) } fn format_duration(duration: &Duration) -> String {