diff --git a/buildpacks/gradle/CHANGELOG.md b/buildpacks/gradle/CHANGELOG.md index 53818d19..3f81d8f2 100644 --- a/buildpacks/gradle/CHANGELOG.md +++ b/buildpacks/gradle/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- The buildpack will add a default process type if a supported framework is detected and the expected build output is found. This mirrors the same feature from the Maven buildpack. ([#726](https://github.com/heroku/buildpacks-jvm/pull/726)) +- Support for the Micronaut and Quarkus frameworks. Both previously worked with the buildpack but required some configuration. Unless heavily customized, no build task needs to be specified anymore. ([#726](https://github.com/heroku/buildpacks-jvm/pull/726)) + ## [6.0.1] - 2024-07-19 - No changes. diff --git a/buildpacks/gradle/src/errors.rs b/buildpacks/gradle/src/errors.rs index 26e617d1..bedc07f4 100644 --- a/buildpacks/gradle/src/errors.rs +++ b/buildpacks/gradle/src/errors.rs @@ -75,5 +75,12 @@ pub(crate) fn on_error_gradle_buildpack(error: GradleBuildpackError) { error, ); } + GradleBuildpackError::CannotDetermineDefaultAppProcess(error) => { + log_please_try_again_error( + "Failed to determine default app process", + "Failed to determine default app process", + error, + ); + } } } diff --git a/buildpacks/gradle/src/framework.rs b/buildpacks/gradle/src/framework.rs index c10b7098..0350a1c2 100644 --- a/buildpacks/gradle/src/framework.rs +++ b/buildpacks/gradle/src/framework.rs @@ -1,26 +1,87 @@ use crate::gradle_command::GradleDependencyReport; +use buildpacks_jvm_shared::fs::list_directory_contents; +use libcnb::data::launch::{Process, ProcessBuilder}; +use libcnb::data::process_type; +use std::path::Path; pub(crate) fn detect_framework(dependency_report: &GradleDependencyReport) -> Option { - DEPENDENCY_TO_FRAMEWORK_MAPPINGS - .into_iter() - .find_map(|(group_id, artifact_id, framework)| { + DEPENDENCY_TO_FRAMEWORK_MAPPINGS.into_iter().find_map( + |(configuration, group_id, artifact_id, framework)| { dependency_report - .contains_dependency("runtimeClasspath", group_id, artifact_id) + .contains_dependency(configuration, group_id, artifact_id) .then_some(framework) - }) + }, + ) +} + +#[allow(clippy::case_sensitive_file_extension_comparisons)] +pub(crate) fn default_app_process>( + dependency_report: &GradleDependencyReport, + app_dir: P, +) -> Result, std::io::Error> { + let jar_path = match detect_framework(dependency_report) { + Some(Framework::SpringBoot | Framework::Micronaut) => { + list_directory_contents(app_dir.as_ref().join("build/libs"))?.find(|path| { + path.file_name() + .map(|file_name| file_name.to_string_lossy().to_string()) + .is_some_and(|file_name| { + file_name.ends_with(".jar") + && !file_name.ends_with("-plain.jar") // Spring Boot JAR without dependencies + && !file_name.ends_with("-sources.jar") + && !file_name.ends_with("-javadoc.jar") + }) + }) + } + Some(Framework::Quarkus) => { + let quarkus_run_jar = app_dir.as_ref().join("build/quarkus-app/quarkus-run.jar"); + quarkus_run_jar.is_file().then_some(quarkus_run_jar) + } + _ => None, + }; + + let process = jar_path.map(|jar_path| { + ProcessBuilder::new( + process_type!("web"), + ["java", "-jar", &jar_path.to_string_lossy()], + ) + .default(true) + .build() + }); + + Ok(process) } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub(crate) enum Framework { Ratpack, SpringBoot, + Micronaut, + Quarkus, } -const DEPENDENCY_TO_FRAMEWORK_MAPPINGS: [(&str, &str, Framework); 2] = [ - ("io.ratpack", "ratpack-core", Framework::Ratpack), +const DEPENDENCY_TO_FRAMEWORK_MAPPINGS: [(&str, &str, &str, Framework); 4] = [ ( + "runtimeClasspath", "org.springframework.boot", "spring-boot", Framework::SpringBoot, ), + ( + "runtimeClasspath", + "io.ratpack", + "ratpack-core", + Framework::Ratpack, + ), + ( + "runtimeClasspath", + "io.micronaut", + "micronaut-core", + Framework::Micronaut, + ), + ( + "quarkusProdRuntimeClasspathConfigurationDeployment", + "io.quarkus", + "quarkus-core", + Framework::Quarkus, + ), ]; diff --git a/buildpacks/gradle/src/main.rs b/buildpacks/gradle/src/main.rs index 9e8d6460..31e4d379 100644 --- a/buildpacks/gradle/src/main.rs +++ b/buildpacks/gradle/src/main.rs @@ -1,7 +1,7 @@ use crate::config::GradleBuildpackConfig; use crate::detect::is_gradle_project_directory; use crate::errors::on_error_gradle_buildpack; -use crate::framework::{detect_framework, Framework}; +use crate::framework::{default_app_process, detect_framework, Framework}; use crate::gradle_command::GradleCommandError; use crate::layers::gradle_home::handle_gradle_home_layer; use crate::GradleBuildpackError::{GradleBuildIoError, GradleBuildUnexpectedStatusError}; @@ -10,6 +10,7 @@ use buildpacks_jvm_shared as shared; use buildpacks_jvm_shared_test as _; use libcnb::build::{BuildContext, BuildResult, BuildResultBuilder}; use libcnb::data::build_plan::BuildPlanBuilder; +use libcnb::data::launch::LaunchBuilder; use libcnb::detect::{DetectContext, DetectResult, DetectResultBuilder}; use libcnb::generic::GenericPlatform; use libcnb::{buildpack_main, Buildpack, Env}; @@ -41,6 +42,7 @@ enum GradleBuildpackError { WriteGradlePropertiesError(std::io::Error), WriteGradleInitScriptError(std::io::Error), CannotSetGradleWrapperExecutableBit(std::io::Error), + CannotDetermineDefaultAppProcess(std::io::Error), StartGradleDaemonError(GradleCommandError<()>), BuildTaskUnknown, } @@ -94,18 +96,18 @@ impl Buildpack for GradleBuildpack { .map_err(|command_error| command_error.map_parse_error(|_| ())) .map_err(GradleBuildpackError::GetTasksError)?; - let detected_framework = gradle_command::dependency_report(&context.app_dir, &gradle_env) - .map_err(GradleBuildpackError::GetDependencyReportError) - .map(|dependency_report| detect_framework(&dependency_report))?; + let 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(|| { - detected_framework.map(|framework| match framework { - Framework::SpringBoot => "build", + detect_framework(&dependency_report).map(|framework| match framework { + Framework::SpringBoot | Framework::Quarkus => "build", Framework::Ratpack => "installDist", + Framework::Micronaut => "shadowJar", }) }) .ok_or(GradleBuildpackError::BuildTaskUnknown)?; @@ -127,7 +129,14 @@ impl Buildpack for GradleBuildpack { // failure, nor can we recover from it in any way. let _ = gradle_command::stop_daemon(&gradle_wrapper_executable_path, &gradle_env); - BuildResultBuilder::new().build() + 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) {