From af4d783c8ad9360f1826b28a52a1a0d021749a4b Mon Sep 17 00:00:00 2001 From: Manuel Fuchs Date: Thu, 31 Aug 2023 13:38:01 +0200 Subject: [PATCH] Add Gadle buildpack --- .gitmodules | 4 + Cargo.lock | 136 ++-- Cargo.toml | 1 + README.md | 9 +- buildpacks/gradle/CHANGELOG.md | 7 + buildpacks/gradle/Cargo.toml | 18 + buildpacks/gradle/README.md | 35 + buildpacks/gradle/buildpack.toml | 21 + buildpacks/gradle/package.toml | 2 + buildpacks/gradle/src/config.rs | 19 + buildpacks/gradle/src/detect.rs | 17 + buildpacks/gradle/src/errors.rs | 28 + buildpacks/gradle/src/framework.rs | 26 + .../gradle/src/gradle_command/daemon.rs | 47 ++ .../src/gradle_command/dependency_report.rs | 642 ++++++++++++++++++ buildpacks/gradle/src/gradle_command/mod.rs | 65 ++ buildpacks/gradle/src/gradle_command/tasks.rs | 189 ++++++ buildpacks/gradle/src/layers/gradle_home.rs | 90 +++ buildpacks/gradle/src/layers/mod.rs | 1 + buildpacks/gradle/src/main.rs | 155 +++++ .../test-apps/heroku-gradle-getting-started | 1 + buildpacks/gradle/tests/integration/main.rs | 21 + buildpacks/gradle/tests/integration/smoke.rs | 20 + buildpacks/gradle/tests/integration/ux.rs | 36 + meta-buildpacks/java/buildpack.toml | 15 + meta-buildpacks/java/package.toml | 3 + shared/src/fs.rs | 24 + shared/src/system_properties.rs | 25 +- 28 files changed, 1593 insertions(+), 64 deletions(-) create mode 100644 buildpacks/gradle/CHANGELOG.md create mode 100644 buildpacks/gradle/Cargo.toml create mode 100644 buildpacks/gradle/README.md create mode 100644 buildpacks/gradle/buildpack.toml create mode 100644 buildpacks/gradle/package.toml create mode 100644 buildpacks/gradle/src/config.rs create mode 100644 buildpacks/gradle/src/detect.rs create mode 100644 buildpacks/gradle/src/errors.rs create mode 100644 buildpacks/gradle/src/framework.rs create mode 100644 buildpacks/gradle/src/gradle_command/daemon.rs create mode 100644 buildpacks/gradle/src/gradle_command/dependency_report.rs create mode 100644 buildpacks/gradle/src/gradle_command/mod.rs create mode 100644 buildpacks/gradle/src/gradle_command/tasks.rs create mode 100644 buildpacks/gradle/src/layers/gradle_home.rs create mode 100644 buildpacks/gradle/src/layers/mod.rs create mode 100644 buildpacks/gradle/src/main.rs create mode 160000 buildpacks/gradle/test-apps/heroku-gradle-getting-started create mode 100644 buildpacks/gradle/tests/integration/main.rs create mode 100644 buildpacks/gradle/tests/integration/smoke.rs create mode 100644 buildpacks/gradle/tests/integration/ux.rs diff --git a/.gitmodules b/.gitmodules index 0aa5b58d..5c5e4bbb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -9,3 +9,7 @@ [submodule "buildpacks/maven/test-apps/heroku-java-getting-started"] path = buildpacks/maven/test-apps/heroku-java-getting-started url = https://github.com/heroku/java-getting-started.git + +[submodule "buildpacks/gradle/test-apps/heroku-gradle-getting-started"] + path = buildpacks/gradle/test-apps/heroku-gradle-getting-started + url = https://github.com/heroku/gradle-getting-started.git diff --git a/Cargo.lock b/Cargo.lock index 3330794f..e24d5be1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,19 +10,13 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" +checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" dependencies = [ "memchr", ] -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - [[package]] name = "base64" version = "0.21.4" @@ -177,9 +171,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "libc", ] @@ -245,9 +239,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] @@ -260,9 +254,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" dependencies = [ "errno-dragonfly", "libc", @@ -376,10 +370,18 @@ dependencies = [ ] [[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +name = "gradle" +version = "0.0.0" +dependencies = [ + "buildpacks-jvm-shared", + "buildpacks-jvm-shared-test", + "indoc", + "libcnb", + "libcnb-test", + "libherokubuildpack", + "nom", + "serde", +] [[package]] name = "hashbrown" @@ -388,23 +390,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" [[package]] -name = "idna" -version = "0.4.0" +name = "home" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "windows-sys", ] [[package]] -name = "indexmap" -version = "1.9.3" +name = "idna" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ - "autocfg", - "hashbrown 0.12.3", + "unicode-bidi", + "unicode-normalization", ] [[package]] @@ -414,7 +415,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown", ] [[package]] @@ -558,9 +559,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.5" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" [[package]] name = "log" @@ -574,6 +575,12 @@ version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -583,6 +590,16 @@ dependencies = [ "adler", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "once_cell" version = "1.18.0" @@ -603,12 +620,12 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "petgraph" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 1.9.3", + "indexmap", ] [[package]] @@ -726,9 +743,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.8" +version = "0.38.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" +checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" dependencies = [ "bitflags 2.4.0", "errno", @@ -739,13 +756,13 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.6" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1feddffcfcc0b33f5c6ce9a29e341e4cd59c3f78e7ee45f4a40c038b1d6cbb" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ "log", "ring", - "rustls-webpki 0.101.3", + "rustls-webpki 0.101.5", "sct", ] @@ -761,9 +778,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.3" +version = "0.101.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261e9e0888cba427c3316e6322805653c9425240b6fd96cee7cb671ab70ab8d0" +checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed" dependencies = [ "ring", "untrusted", @@ -835,9 +852,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "2cc66a619ed80bf7a0f6b17dd063a84b88f6dea1813737cf469aef1d081142c2" dependencies = [ "itoa", "ryu", @@ -878,9 +895,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "syn" -version = "2.0.29" +version = "2.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" dependencies = [ "proc-macro2", "quote", @@ -922,18 +939,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.47" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.47" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", @@ -957,9 +974,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" dependencies = [ "serde", "serde_spanned", @@ -978,11 +995,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.14" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.0.0", + "indexmap", "serde", "serde_spanned", "toml_datetime", @@ -1152,13 +1169,14 @@ dependencies = [ [[package]] name = "which" -version = "4.4.0" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" dependencies = [ "either", - "libc", + "home", "once_cell", + "rustix", ] [[package]] @@ -1260,9 +1278,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d09770118a7eb1ccaf4a594a221334119a44a814fcb0d31c5b85e83e97227a97" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 4521fd99..a3f0ed9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "1" members = [ + "buildpacks/gradle", "buildpacks/jvm", "buildpacks/jvm-function-invoker", "buildpacks/maven", diff --git a/README.md b/README.md index 170d1831..ac9c370c 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,11 @@ to build your application. ### Build Tools -| ID | Name | Readme | Changelog | Latest Version | -|----------------|------------------------------------|--------------------------------------|--------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `heroku/maven` | [Maven](https://maven.apache.org/) | [Readme](buildpacks/maven/README.md) | [Changelog](buildpacks/maven/CHANGELOG.md) | [](https://registry.buildpacks.io/buildpacks/heroku/maven) | -| `heroku/sbt` | [sbt](https://www.scala-sbt.org/) | [Readme](buildpacks/sbt/README.md) | [Changelog](buildpacks/sbt/CHANGELOG.md) | [](https://registry.buildpacks.io/buildpacks/heroku/sbt) | +| ID | Name | Readme | Changelog | Latest Version | +|-----------------|------------------------------------|---------------------------------------|---------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `heroku/maven` | [Maven](https://maven.apache.org/) | [Readme](buildpacks/maven/README.md) | [Changelog](buildpacks/maven/CHANGELOG.md) | [](https://registry.buildpacks.io/buildpacks/heroku/maven) | +| `heroku/gradle` | [Gradle](https://gradle.org/) | [Readme](buildpacks/gradle/README.md) | [Changelog](buildpacks/gradle/CHANGELOG.md) | [](https://registry.buildpacks.io/buildpacks/heroku/gradle) | +| `heroku/sbt` | [sbt](https://www.scala-sbt.org/) | [Readme](buildpacks/sbt/README.md) | [Changelog](buildpacks/sbt/CHANGELOG.md) | [](https://registry.buildpacks.io/buildpacks/heroku/sbt) | ### Platforms diff --git a/buildpacks/gradle/CHANGELOG.md b/buildpacks/gradle/CHANGELOG.md new file mode 100644 index 00000000..3676ee19 --- /dev/null +++ b/buildpacks/gradle/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +* Initial release diff --git a/buildpacks/gradle/Cargo.toml b/buildpacks/gradle/Cargo.toml new file mode 100644 index 00000000..4f4f1dbc --- /dev/null +++ b/buildpacks/gradle/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "gradle" +version.workspace = true +rust-version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +libcnb.workspace = true +libherokubuildpack.workspace = true +serde = { version = "1", features = ["derive"] } +buildpacks-jvm-shared.workspace = true +nom = "7.1.1" +indoc = "2.0.1" + +[dev-dependencies] +libcnb-test.workspace = true +buildpacks-jvm-shared-test.workspace = true diff --git a/buildpacks/gradle/README.md b/buildpacks/gradle/README.md new file mode 100644 index 00000000..e7ea1fee --- /dev/null +++ b/buildpacks/gradle/README.md @@ -0,0 +1,35 @@ +# Heroku Cloud Native Gradle Buildpack +[![CI](https://github.com/heroku/buildpacks-jvm/actions/workflows/ci.yml/badge.svg)](https://github.com/heroku/buildpacks-jvm/actions/workflows/ci.yml) +[![Registry](https://img.shields.io/badge/dynamic/json?url=https://registry.buildpacks.io/api/v1/buildpacks/heroku/gradle&label=version&query=$.latest.version&color=DF0A6B&logo=data:image/png;base64,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&labelColor=white)](https://registry.buildpacks.io/buildpacks/heroku/gradle) + +Heroku's official Cloud Native Buildpack for [Gradle](https://gradle.org/). + +This buildpack is designed to work in conjunction with other Heroku buildpacks and cannot be used independently. If you +want to build a Java application, use the `heroku/java` buildpack ([Source](/meta-buildpacks/java), +[Readme](/meta-buildpacks/java/README.md)) which includes this Gradle buildpack. + +## Reference +### Detect + +Requires either `build.gradle`, `settings.gradle`, `build.gradle.kts` or `settings.gradle.kts` at the root of the application source. + +### Build Plan +#### Requires +##### `jdk` +To compile Java sources a JDK is required. It can be provided by the `heroku/jvm` ([Source](/buildpacks/jvm), +[Readme](/buildpacks/jvm/README.md)) buildpack. + +##### `jvm-application` +This is not a strict requirement of the buildpack. Requiring `jvm-application` ensures that this Gradle buildpack can be +used even when no other buildpack requires `jvm-application.` + +#### Provides +##### `jvm-application` +Allows other buildpacks to depend on a compiled JVM application. + +### Environment Variables +#### `GRADLE_TASK` +Allows overriding the Gradle task used during the build process. The default task is `stage`. + +## License +See [LICENSE](../../LICENSE) file. diff --git a/buildpacks/gradle/buildpack.toml b/buildpacks/gradle/buildpack.toml new file mode 100644 index 00000000..02b25389 --- /dev/null +++ b/buildpacks/gradle/buildpack.toml @@ -0,0 +1,21 @@ +api = "0.9" + +[buildpack] +id = "heroku/gradle" +version = "3.0.0" +name = "Gradle" +clear-env = true +homepage = "https://github.com/heroku/buildpacks-jvm" +description = "Official Heroku buildpack for Gradle applications." +keywords = ["java", "gradle"] + +[[buildpack.licenses]] +type = "BSD-3-Clause" + +[[stacks]] +id = "*" + +[metadata.release] + +[metadata.release.docker] +repository = "docker.io/heroku/buildpack-gradle" diff --git a/buildpacks/gradle/package.toml b/buildpacks/gradle/package.toml new file mode 100644 index 00000000..54b0d2e4 --- /dev/null +++ b/buildpacks/gradle/package.toml @@ -0,0 +1,2 @@ +[buildpack] +uri = "." diff --git a/buildpacks/gradle/src/config.rs b/buildpacks/gradle/src/config.rs new file mode 100644 index 00000000..6ad18b66 --- /dev/null +++ b/buildpacks/gradle/src/config.rs @@ -0,0 +1,19 @@ +use libcnb::build::BuildContext; +use libcnb::generic::GenericPlatform; +use libcnb::{Buildpack, Platform}; + +pub(crate) struct GradleBuildpackConfig { + pub(crate) gradle_task: Option, +} + +impl> From<&BuildContext> for GradleBuildpackConfig { + fn from(context: &BuildContext) -> Self { + GradleBuildpackConfig { + gradle_task: context + .platform + .env() + .get("GRADLE_TASK") + .map(|s| s.to_string_lossy().to_string()), + } + } +} diff --git a/buildpacks/gradle/src/detect.rs b/buildpacks/gradle/src/detect.rs new file mode 100644 index 00000000..62f6aae3 --- /dev/null +++ b/buildpacks/gradle/src/detect.rs @@ -0,0 +1,17 @@ +use std::path::Path; + +pub(crate) fn is_gradle_project_directory(root_dir: &Path) -> bool { + // We look for these Gradle specific files and not 'gradlew' directly. This allows us to + // fail with an error message explaining that 'gradlew' is required for Gradle projects. + // If we just fail detect on a missing Gradle Wrapper, we lose the opportunity to display + // such a message, worsening DX. + [ + "build.gradle", + "settings.gradle", + "build.gradle.kts", + "settings.gradle.kts", + ] + .into_iter() + .map(|file_name| root_dir.join(file_name)) + .any(|path| path.exists()) +} diff --git a/buildpacks/gradle/src/errors.rs b/buildpacks/gradle/src/errors.rs new file mode 100644 index 00000000..6796e4d8 --- /dev/null +++ b/buildpacks/gradle/src/errors.rs @@ -0,0 +1,28 @@ +use crate::GradleBuildpackError; +use indoc::indoc; +use libherokubuildpack::log::log_error; + +#[allow(clippy::too_many_lines, clippy::needless_pass_by_value)] +pub(crate) fn on_error_gradle_buildpack(error: GradleBuildpackError) { + match error { + GradleBuildpackError::GradleWrapperNotFound => { + log_error( + "Missing Gradle Wrapper", + indoc! {" + This buildpack leverages Gradle Wrapper to install the correct Gradle version to build your application. + However, it seems that your application does not contain the required Gradle Wrapper files. + + To add the Gradle Wrapper, run the following command in your application's root directory: + $ gradle wrapper + + Additional information about Gradle Wrapper and available configuration options can be found here: + https://docs.gradle.org/current/userguide/gradle_wrapper.html + "}, + ); + } + // TODO + _ => { + eprintln!("{error:?}"); + } + } +} diff --git a/buildpacks/gradle/src/framework.rs b/buildpacks/gradle/src/framework.rs new file mode 100644 index 00000000..c10b7098 --- /dev/null +++ b/buildpacks/gradle/src/framework.rs @@ -0,0 +1,26 @@ +use crate::gradle_command::GradleDependencyReport; + +pub(crate) fn detect_framework(dependency_report: &GradleDependencyReport) -> Option { + DEPENDENCY_TO_FRAMEWORK_MAPPINGS + .into_iter() + .find_map(|(group_id, artifact_id, framework)| { + dependency_report + .contains_dependency("runtimeClasspath", group_id, artifact_id) + .then_some(framework) + }) +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub(crate) enum Framework { + Ratpack, + SpringBoot, +} + +const DEPENDENCY_TO_FRAMEWORK_MAPPINGS: [(&str, &str, Framework); 2] = [ + ("io.ratpack", "ratpack-core", Framework::Ratpack), + ( + "org.springframework.boot", + "spring-boot", + Framework::SpringBoot, + ), +]; diff --git a/buildpacks/gradle/src/gradle_command/daemon.rs b/buildpacks/gradle/src/gradle_command/daemon.rs new file mode 100644 index 00000000..36f456e4 --- /dev/null +++ b/buildpacks/gradle/src/gradle_command/daemon.rs @@ -0,0 +1,47 @@ +use crate::gradle_command::GradleCommandError; +use crate::GRADLE_TASK_NAME_HEROKU_START_DAEMON; +use libcnb::Env; +use libherokubuildpack::command::CommandExt; +use std::io::{stderr, stdout}; +use std::path::Path; +use std::process::Command; + +pub(crate) fn start( + gradle_wrapper_executable_path: &Path, + gradle_env: &Env, +) -> Result<(), GradleCommandError<()>> { + let output = Command::new(gradle_wrapper_executable_path) + .args([ + // Fixes an issue when when running under Apple Rosetta emulation + "-Djdk.lang.Process.launchMechanism=vfork", + "--daemon", + GRADLE_TASK_NAME_HEROKU_START_DAEMON, + ]) + .envs(gradle_env) + .output_and_write_streams(stdout(), stderr()) + .map_err(GradleCommandError::Io)?; + + if output.status.success() { + Ok(()) + } else { + Err(GradleCommandError::UnexpectedExitStatus { + status: output.status, + stdout: String::from_utf8_lossy(&output.stdout).into_owned(), + stderr: String::from_utf8_lossy(&output.stderr).into_owned(), + }) + } +} + +pub(crate) fn stop( + gradle_wrapper_executable_path: &Path, + gradle_env: &Env, +) -> Result<(), GradleCommandError<()>> { + Command::new(gradle_wrapper_executable_path) + .args(["-q", "--stop"]) + .envs(gradle_env) + .spawn() + .and_then(|mut child| child.wait()) + .map_err(GradleCommandError::Io)?; + + Ok(()) +} diff --git a/buildpacks/gradle/src/gradle_command/dependency_report.rs b/buildpacks/gradle/src/gradle_command/dependency_report.rs new file mode 100644 index 00000000..e8614780 --- /dev/null +++ b/buildpacks/gradle/src/gradle_command/dependency_report.rs @@ -0,0 +1,642 @@ +use crate::gradle_command::GradleCommandError; +use libcnb::Env; +use std::collections::BTreeMap; +use std::path::Path; +use std::process::Command; + +pub(crate) fn dependency_report( + app_dir: &Path, + env: &Env, +) -> Result> { + let output = Command::new(app_dir.join("gradlew")) + .current_dir(app_dir) + .envs(env) + .args(["--quiet", "dependencies"]) + .output() + .map_err(GradleCommandError::Io)?; + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + if output.status.success() { + parser::dependency_report(&stdout) + .map_err(|_| GradleCommandError::Parse(())) + .map(|(_, dependency_report)| dependency_report) + } else { + Err(GradleCommandError::UnexpectedExitStatus { + status: output.status, + stdout: stdout.into_owned(), + stderr: stderr.into_owned(), + }) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) struct GradleDependencyReport { + pub(crate) entries: BTreeMap>, +} + +impl GradleDependencyReport { + pub(crate) fn contains_dependency( + &self, + configuration_name: &str, + group_id: &str, + artifact_id: &str, + ) -> bool { + self.flattened_dependencies(configuration_name) + .unwrap_or_default() + .into_iter() + .any(|dependency| { + dependency.group_id == group_id && dependency.artifact_id == artifact_id + }) + } + + pub(crate) fn flattened_dependencies( + &self, + configuration_name: &str, + ) -> Option> { + self.entries.get(configuration_name).map(|dependencies| { + let mut acc = vec![]; + + for dependency in dependencies { + acc.append(&mut dependency.flatten()); + } + + acc + }) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) struct Dependency { + pub(crate) group_id: String, + pub(crate) artifact_id: String, + pub(crate) package_version: Option, + pub(crate) resolved_package_version: Option, + pub(crate) suffix: Option, + pub(crate) dependencies: Vec, +} + +impl Dependency { + fn flatten(&self) -> Vec { + let mut acc = vec![]; + acc.push(Dependency { + group_id: self.group_id.clone(), + artifact_id: self.artifact_id.clone(), + package_version: self.package_version.clone(), + resolved_package_version: self.resolved_package_version.clone(), + suffix: self.suffix, + dependencies: vec![], + }); + + for transitive_dependency in &self.dependencies { + acc.append(&mut transitive_dependency.flatten()); + } + + acc + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(crate) enum Suffix { + DependencyConstraint, + DependenciesOmitted, + NotResolved, +} + +#[derive(Debug)] +pub(crate) enum ParseError {} + +mod parser { + use super::{Dependency, GradleDependencyReport, Suffix}; + use nom::branch::alt; + use nom::bytes::complete::tag; + use nom::character::complete::{alphanumeric1, char, line_ending, newline, not_line_ending}; + use nom::combinator::{map, opt, recognize}; + use nom::multi::{count, many0, many1, many_till}; + use nom::sequence::{delimited, preceded, terminated, tuple}; + use nom::IResult; + + pub(crate) fn dependency_report(report: &str) -> IResult<&str, GradleDependencyReport> { + let configuration_name_and_dependencies = tuple(( + configuration_line, + alt(( + map(tag("No dependencies"), |_| Vec::new()), + many1(dependency_tree(1)), + )), + )); + + map( + many0(map( + many_till(any_line, configuration_name_and_dependencies), + |(_, parsed_configuration)| parsed_configuration, + )), + |entries| GradleDependencyReport { + entries: entries.into_iter().collect(), + }, + )(report) + } + + fn configuration_line(input: &str) -> IResult<&str, String> { + map( + terminated(alphanumeric1, terminated(not_line_ending, line_ending)), + String::from, + )(input) + } + + fn any_line(input: &str) -> IResult<&str, String> { + map(terminated(not_line_ending, line_ending), String::from)(input) + } + + fn dependency_tree(start_at_depth: usize) -> impl FnMut(&str) -> IResult<&str, Dependency> { + move |input: &str| { + preceded( + count(tree_depth_indicator, start_at_depth), + map( + tuple(( + terminated( + tuple(( + group_or_artifact_id, + preceded(char(':'), group_or_artifact_id), + opt(preceded(char(':'), package_version)), + opt(preceded(tag(" -> "), package_version)), + opt(preceded(char(' '), dependency_suffix)), + )), + newline, + ), + many0(dependency_tree(start_at_depth + 1)), + )), + |( + (group_id, artifact_id, package_version, resolved_package_version, suffix), + children, + )| Dependency { + group_id, + artifact_id, + package_version, + resolved_package_version, + suffix, + dependencies: children, + }, + ), + )(input) + } + } + + fn dependency_suffix(input: &str) -> IResult<&str, Suffix> { + delimited( + char('('), + alt(( + map(char('c'), |_| Suffix::DependencyConstraint), + map(char('*'), |_| Suffix::DependenciesOmitted), + map(char('n'), |_| Suffix::NotResolved), + )), + char(')'), + )(input) + } + + fn tree_depth_indicator(input: &str) -> IResult<&str, &str> { + alt((tag("+--- "), tag("| "), tag(" "), tag("\\--- ")))(input) + } + + fn group_or_artifact_id(input: &str) -> IResult<&str, String> { + map( + recognize(many1(alt((alphanumeric1, tag("_"), tag("-"), tag("."))))), + String::from, + )(input) + } + + fn package_version(input: &str) -> IResult<&str, String> { + map( + recognize(many1(alt((alphanumeric1, tag("_"), tag("-"), tag("."))))), + String::from, + )(input) + } + + #[cfg(test)] + mod test { + use super::Suffix::NotResolved; + use super::{Dependency, GradleDependencyReport}; + use indoc::indoc; + use std::collections::BTreeMap; + + #[test] + #[allow(clippy::too_many_lines)] + fn test() { + let result = super::dependency_report(indoc! {r" + ------------------------------------------------------------ + Project ':app' + ------------------------------------------------------------ + + annotationProcessor - Annotation processors and their dependencies for source set 'main'. + No dependencies + + apiElements - API elements for main. (n) + No dependencies + + archives - Configuration for archive artifacts. (n) + No dependencies + + compileClasspath - Compile classpath for source set 'main'. + \--- com.google.guava:guava:30.1.1-jre + +--- com.google.guava:failureaccess:1.0.1 + +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava + +--- com.google.code.findbugs:jsr305:3.0.2 + +--- org.checkerframework:checker-qual:3.8.0 + +--- com.google.errorprone:error_prone_annotations:2.5.1 + \--- com.google.j2objc:j2objc-annotations:1.3 + + compileOnly - Compile only dependencies for source set 'main'. (n) + No dependencies + + default - Configuration for default artifacts. (n) + No dependencies + + implementation - Implementation only dependencies for source set 'main'. (n) + \--- com.google.guava:guava:30.1.1-jre (n) + + mainSourceElements - List of source directories contained in the Main SourceSet. (n) + No dependencies + + runtimeClasspath - Runtime classpath of source set 'main'. + \--- com.google.guava:guava:30.1.1-jre + +--- com.google.guava:failureaccess:1.0.1 + +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava + +--- com.google.code.findbugs:jsr305:3.0.2 + +--- org.checkerframework:checker-qual:3.8.0 + +--- com.google.errorprone:error_prone_annotations:2.5.1 + \--- com.google.j2objc:j2objc-annotations:1.3 + + runtimeElements - Elements of runtime for main. (n) + No dependencies + + runtimeOnly - Runtime only dependencies for source set 'main'. (n) + No dependencies + + testAnnotationProcessor - Annotation processors and their dependencies for source set 'test'. + No dependencies + + testCompileClasspath - Compile classpath for source set 'test'. + +--- com.google.guava:guava:30.1.1-jre + | +--- com.google.guava:failureaccess:1.0.1 + | +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava + | +--- com.google.code.findbugs:jsr305:3.0.2 + | +--- org.checkerframework:checker-qual:3.8.0 + | +--- com.google.errorprone:error_prone_annotations:2.5.1 + | \--- com.google.j2objc:j2objc-annotations:1.3 + \--- junit:junit:4.13.2 + \--- org.hamcrest:hamcrest-core:1.3 + + testCompileOnly - Compile only dependencies for source set 'test'. (n) + No dependencies + + testImplementation - Implementation only dependencies for source set 'test'. (n) + \--- junit:junit:4.13.2 (n) + + testResultsElementsForTest - Directory containing binary results of running tests for the test Test Suite's test target. (n) + No dependencies + + testRuntimeClasspath - Runtime classpath of source set 'test'. + +--- com.google.guava:guava:30.1.1-jre + | +--- com.google.guava:failureaccess:1.0.1 + | +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava + | +--- com.google.code.findbugs:jsr305:3.0.2 + | +--- org.checkerframework:checker-qual:3.8.0 + | +--- com.google.errorprone:error_prone_annotations:2.5.1 + | \--- com.google.j2objc:j2objc-annotations:1.3 + \--- junit:junit:4.13.2 + \--- org.hamcrest:hamcrest-core:1.3 + + testRuntimeOnly - Runtime only dependencies for source set 'test'. (n) + No dependencies + + (n) - Not resolved (configuration is not meant to be resolved) + + A web-based, searchable dependency report is available by adding the --scan option. + "}); + + assert_eq!( + result.unwrap().1, + GradleDependencyReport { + entries: BTreeMap::from([ + (String::from("annotationProcessor"), vec![]), + (String::from("apiElements"), vec![]), + (String::from("archives"), vec![]), + ( + String::from("compileClasspath"), + vec![Dependency { + group_id: String::from("com.google.guava"), + artifact_id: String::from("guava"), + package_version: Some(String::from("30.1.1-jre")), + resolved_package_version: None, + suffix: None, + dependencies: vec![ + Dependency { + group_id: String::from("com.google.guava"), + artifact_id: String::from("failureaccess"), + package_version: Some(String::from("1.0.1")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }, + Dependency { + group_id: String::from("com.google.guava"), + artifact_id: String::from("listenablefuture"), + package_version: Some(String::from( + "9999.0-empty-to-avoid-conflict-with-guava" + )), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }, + Dependency { + group_id: String::from("com.google.code.findbugs"), + artifact_id: String::from("jsr305"), + package_version: Some(String::from("3.0.2")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }, + Dependency { + group_id: String::from("org.checkerframework"), + artifact_id: String::from("checker-qual"), + package_version: Some(String::from("3.8.0")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }, + Dependency { + group_id: String::from("com.google.errorprone"), + artifact_id: String::from("error_prone_annotations"), + package_version: Some(String::from("2.5.1")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }, + Dependency { + group_id: String::from("com.google.j2objc"), + artifact_id: String::from("j2objc-annotations"), + package_version: Some(String::from("1.3")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + } + ] + }] + ), + (String::from("compileOnly"), vec![]), + (String::from("default"), vec![]), + ( + String::from("implementation"), + vec![Dependency { + group_id: String::from("com.google.guava"), + artifact_id: String::from("guava"), + package_version: Some(String::from("30.1.1-jre")), + resolved_package_version: None, + suffix: Some(NotResolved), + dependencies: vec![] + }] + ), + (String::from("mainSourceElements"), vec![]), + ( + String::from("runtimeClasspath"), + vec![Dependency { + group_id: String::from("com.google.guava"), + artifact_id: String::from("guava"), + package_version: Some(String::from("30.1.1-jre")), + resolved_package_version: None, + suffix: None, + dependencies: vec![ + Dependency { + group_id: String::from("com.google.guava"), + artifact_id: String::from("failureaccess"), + package_version: Some(String::from("1.0.1")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }, + Dependency { + group_id: String::from("com.google.guava"), + artifact_id: String::from("listenablefuture"), + package_version: Some(String::from( + "9999.0-empty-to-avoid-conflict-with-guava" + )), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }, + Dependency { + group_id: String::from("com.google.code.findbugs"), + artifact_id: String::from("jsr305"), + package_version: Some(String::from("3.0.2")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }, + Dependency { + group_id: String::from("org.checkerframework"), + artifact_id: String::from("checker-qual"), + package_version: Some(String::from("3.8.0")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }, + Dependency { + group_id: String::from("com.google.errorprone"), + artifact_id: String::from("error_prone_annotations"), + package_version: Some(String::from("2.5.1")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }, + Dependency { + group_id: String::from("com.google.j2objc"), + artifact_id: String::from("j2objc-annotations"), + package_version: Some(String::from("1.3")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + } + ] + }] + ), + (String::from("runtimeElements"), vec![]), + (String::from("runtimeOnly"), vec![]), + (String::from("testAnnotationProcessor"), vec![]), + ( + String::from("testCompileClasspath"), + vec![ + Dependency { + group_id: String::from("com.google.guava"), + artifact_id: String::from("guava"), + package_version: Some(String::from("30.1.1-jre")), + resolved_package_version: None, + suffix: None, + dependencies: vec![ + Dependency { + group_id: String::from("com.google.guava"), + artifact_id: String::from("failureaccess"), + package_version: Some(String::from("1.0.1")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }, + Dependency { + group_id: String::from("com.google.guava"), + artifact_id: String::from("listenablefuture"), + package_version: Some(String::from( + "9999.0-empty-to-avoid-conflict-with-guava" + )), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }, + Dependency { + group_id: String::from("com.google.code.findbugs"), + artifact_id: String::from("jsr305"), + package_version: Some(String::from("3.0.2")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }, + Dependency { + group_id: String::from("org.checkerframework"), + artifact_id: String::from("checker-qual"), + package_version: Some(String::from("3.8.0")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }, + Dependency { + group_id: String::from("com.google.errorprone"), + artifact_id: String::from("error_prone_annotations"), + package_version: Some(String::from("2.5.1")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }, + Dependency { + group_id: String::from("com.google.j2objc"), + artifact_id: String::from("j2objc-annotations"), + package_version: Some(String::from("1.3")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + } + ] + }, + Dependency { + group_id: String::from("junit"), + artifact_id: String::from("junit"), + package_version: Some(String::from("4.13.2")), + resolved_package_version: None, + suffix: None, + dependencies: vec![Dependency { + group_id: String::from("org.hamcrest"), + artifact_id: String::from("hamcrest-core"), + package_version: Some(String::from("1.3")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }] + } + ] + ), + (String::from("testCompileOnly"), vec![]), + ( + String::from("testImplementation"), + vec![Dependency { + group_id: String::from("junit"), + artifact_id: String::from("junit"), + package_version: Some(String::from("4.13.2")), + resolved_package_version: None, + suffix: Some(NotResolved), + dependencies: vec![] + }] + ), + (String::from("testResultsElementsForTest"), vec![]), + ( + String::from("testRuntimeClasspath"), + vec![ + Dependency { + group_id: String::from("com.google.guava"), + artifact_id: String::from("guava"), + package_version: Some(String::from("30.1.1-jre")), + resolved_package_version: None, + suffix: None, + dependencies: vec![ + Dependency { + group_id: String::from("com.google.guava"), + artifact_id: String::from("failureaccess"), + package_version: Some(String::from("1.0.1")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }, + Dependency { + group_id: String::from("com.google.guava"), + artifact_id: String::from("listenablefuture"), + package_version: Some(String::from( + "9999.0-empty-to-avoid-conflict-with-guava" + )), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }, + Dependency { + group_id: String::from("com.google.code.findbugs"), + artifact_id: String::from("jsr305"), + package_version: Some(String::from("3.0.2")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }, + Dependency { + group_id: String::from("org.checkerframework"), + artifact_id: String::from("checker-qual"), + package_version: Some(String::from("3.8.0")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }, + Dependency { + group_id: String::from("com.google.errorprone"), + artifact_id: String::from("error_prone_annotations"), + package_version: Some(String::from("2.5.1")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }, + Dependency { + group_id: String::from("com.google.j2objc"), + artifact_id: String::from("j2objc-annotations"), + package_version: Some(String::from("1.3")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + } + ] + }, + Dependency { + group_id: String::from("junit"), + artifact_id: String::from("junit"), + package_version: Some(String::from("4.13.2")), + resolved_package_version: None, + suffix: None, + dependencies: vec![Dependency { + group_id: String::from("org.hamcrest"), + artifact_id: String::from("hamcrest-core"), + package_version: Some(String::from("1.3")), + resolved_package_version: None, + suffix: None, + dependencies: vec![] + }] + } + ] + ), + (String::from("testRuntimeOnly"), vec![]), + ]) + } + ); + } + } +} diff --git a/buildpacks/gradle/src/gradle_command/mod.rs b/buildpacks/gradle/src/gradle_command/mod.rs new file mode 100644 index 00000000..1351fef4 --- /dev/null +++ b/buildpacks/gradle/src/gradle_command/mod.rs @@ -0,0 +1,65 @@ +mod daemon; +mod dependency_report; +mod tasks; + +pub(crate) use daemon::start as start_daemon; +pub(crate) use daemon::stop as stop_daemon; +pub(crate) use dependency_report::{dependency_report, GradleDependencyReport}; +pub(crate) use tasks::tasks; + +use std::process::Command; + +#[derive(Debug)] +pub(crate) enum GradleCommandError

{ + Io(std::io::Error), + UnexpectedExitStatus { + status: std::process::ExitStatus, + stdout: String, + stderr: String, + }, + Parse(P), +} + +impl

GradleCommandError

{ + pub(crate) fn map_parse_error(self, f: F) -> GradleCommandError + where + F: Fn(P) -> T, + { + match self { + GradleCommandError::Parse(p) => GradleCommandError::Parse(f(p)), + GradleCommandError::Io(io_error) => GradleCommandError::Io(io_error), + GradleCommandError::UnexpectedExitStatus { + status, + stdout, + stderr, + } => GradleCommandError::UnexpectedExitStatus { + status, + stdout, + stderr, + }, + } + } +} + +pub(crate) fn run_gradle_command( + command: &mut Command, + parser: F, +) -> Result> +where + F: FnOnce(&str, &str) -> Result, +{ + let output = command.output().map_err(GradleCommandError::Io)?; + + let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); + let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); + + if output.status.success() { + parser(&stdout, &stderr).map_err(GradleCommandError::Parse) + } else { + Err(GradleCommandError::UnexpectedExitStatus { + status: output.status, + stdout, + stderr, + }) + } +} diff --git a/buildpacks/gradle/src/gradle_command/tasks.rs b/buildpacks/gradle/src/gradle_command/tasks.rs new file mode 100644 index 00000000..3e3513a6 --- /dev/null +++ b/buildpacks/gradle/src/gradle_command/tasks.rs @@ -0,0 +1,189 @@ +use crate::gradle_command::{run_gradle_command, GradleCommandError}; +use libcnb::Env; +use std::path::Path; +use std::process::Command; + +pub(crate) fn tasks( + current_dir: &Path, + env: &Env, +) -> Result>> { + run_gradle_command( + Command::new(current_dir.join("gradlew")) + .current_dir(current_dir) + .envs(env) + .args(["--quiet", "tasks"]), + |stdout, _stderr| { + parser::parse(stdout) + .map(|groups| Tasks { groups }) + .map_err(|error| nom::error::Error { + input: error.input.to_string(), + code: error.code, + }) + }, + ) +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) struct Tasks { + pub(crate) groups: Vec, +} + +impl Tasks { + pub(crate) fn names(&self) -> Vec { + self.groups + .iter() + .flat_map(|task_group| &task_group.tasks) + .map(|task| task.name.clone()) + .collect() + } + + pub(crate) fn has_task(&self, s: &str) -> bool { + self.names().iter().any(|task_name| task_name == s) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) struct TaskGroup { + pub(crate) heading: String, + pub(crate) tasks: Vec, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) struct Task { + pub(crate) name: String, + pub(crate) description: String, +} + +mod parser { + use super::Task; + use super::TaskGroup; + use nom::bytes::complete::tag; + use nom::character::complete::{alphanumeric1, char, line_ending, not_line_ending}; + use nom::combinator::{map, verify}; + use nom::multi::{count, many0, many1, many_till}; + use nom::sequence::{terminated, tuple}; + use nom::{Finish, IResult}; + + pub(crate) fn parse(input: &str) -> Result, nom::error::Error<&str>> { + many1(map(many_till(any_line, task_group), |(_, out)| out))(input) + .finish() + .map(|(_remaining, parsed)| parsed) + } + + fn task_group(input: &str) -> IResult<&str, TaskGroup> { + map( + tuple((task_group_heading, many0(task_line))), + |(heading, lines)| TaskGroup { + heading, + tasks: lines, + }, + )(input) + } + + fn task_group_heading(input: &str) -> IResult<&str, String> { + let (input, line) = verify(any_line, |line: &str| line.ends_with("tasks"))(input)?; + let (input, _) = terminated(count(char('-'), line.len()), line_ending)(input)?; + + Ok((input, line)) + } + + fn task_line(input: &str) -> IResult<&str, Task> { + map( + tuple(( + map(terminated(alphanumeric1, tag(" - ")), String::from), + map(terminated(not_line_ending, line_ending), String::from), + )), + |(name, description)| Task { name, description }, + )(input) + } + + fn any_line(input: &str) -> IResult<&str, String> { + map(terminated(not_line_ending, line_ending), String::from)(input) + } + + #[cfg(test)] + mod test { + use super::{Task, TaskGroup}; + use indoc::indoc; + + #[test] + fn foo() { + let input = indoc! {" + + ------------------------------------------------------------ + Tasks runnable from root project 'demo' + ------------------------------------------------------------ + + Application tasks + ----------------- + bootRun - Runs this project as a Spring Boot application. + + Build tasks + ----------- + assemble - Assembles the outputs of this project. + bootBuildImage - Builds an OCI image of the application using the output of the bootJar task + bootJar - Assembles an executable jar archive containing the main classes and their dependencies. + build - Assembles and tests this project. + buildDependents - Assembles and tests this project and all projects that depend on it. + buildNeeded - Assembles and tests this project and all projects it depends on. + classes - Assembles main classes. + clean - Deletes the build directory. + jar - Assembles a jar archive containing the main classes. + resolveMainClassName - Resolves the name of the application's main class. + testClasses - Assembles test classes. + + Build Setup tasks + ----------------- + init - Initializes a new Gradle build. + wrapper - Generates Gradle wrapper files. + + Documentation tasks + ------------------- + javadoc - Generates Javadoc API documentation for the main source code. + + Help tasks + ---------- + buildEnvironment - Displays all buildscript dependencies declared in root project 'demo'. + dependencies - Displays all dependencies declared in root project 'demo'. + dependencyInsight - Displays the insight into a specific dependency in root project 'demo'. + dependencyManagement - Displays the dependency management declared in root project 'demo'. + help - Displays a help message. + javaToolchains - Displays the detected java toolchains. + kotlinDslAccessorsReport - Prints the Kotlin code for accessing the currently available project extensions and conventions. + outgoingVariants - Displays the outgoing variants of root project 'demo'. + projects - Displays the sub-projects of root project 'demo'. + properties - Displays the properties of root project 'demo'. + resolvableConfigurations - Displays the configurations that can be resolved in root project 'demo'. + tasks - Displays the tasks runnable from root project 'demo'. + + Verification tasks + ------------------ + check - Runs all checks. + test - Runs the test suite. + + Rules + ----- + Pattern: clean: Cleans the output files of a task. + Pattern: build: Assembles the artifacts of a configuration. + + To see all tasks and more detail, run gradlew tasks --all + + To see more detail about a task, run gradlew help --task + "}; + + let result = super::parse(input).unwrap(); + + assert_eq!( + result, + vec![ + TaskGroup { heading: String::from("Application tasks"), tasks: vec![Task { name: String::from("bootRun"), description: String::from("Runs this project as a Spring Boot application.") }] }, + TaskGroup { heading: String::from("Build tasks"), tasks: vec![Task { name: String::from("assemble"), description: String::from("Assembles the outputs of this project.") }, Task { name: String::from("bootBuildImage"), description: String::from("Builds an OCI image of the application using the output of the bootJar task") }, Task { name: String::from("bootJar"), description: String::from("Assembles an executable jar archive containing the main classes and their dependencies.") }, Task { name: String::from("build"), description: String::from("Assembles and tests this project.") }, Task { name: String::from("buildDependents"), description: String::from("Assembles and tests this project and all projects that depend on it.") }, Task { name: String::from("buildNeeded"), description: String::from("Assembles and tests this project and all projects it depends on.") }, Task { name: String::from("classes"), description: String::from("Assembles main classes.") }, Task { name: String::from("clean"), description: String::from("Deletes the build directory.") }, Task { name: String::from("jar"), description: String::from("Assembles a jar archive containing the main classes.") }, Task { name: String::from("resolveMainClassName"), description: String::from("Resolves the name of the application's main class.") }, Task { name: String::from("testClasses"), description: String::from("Assembles test classes.") }] }, + TaskGroup { heading: String::from("Build Setup tasks"), tasks: vec![Task { name: String::from("init"), description: String::from("Initializes a new Gradle build.") }, Task { name: String::from("wrapper"), description: String::from("Generates Gradle wrapper files.") }] }, + TaskGroup { heading: String::from("Documentation tasks"), tasks: vec![Task { name: String::from("javadoc"), description: String::from("Generates Javadoc API documentation for the main source code.") }] }, + TaskGroup { heading: String::from("Help tasks"), tasks: vec![Task { name: String::from("buildEnvironment"), description: String::from("Displays all buildscript dependencies declared in root project 'demo'.") }, Task { name: String::from("dependencies"), description: String::from("Displays all dependencies declared in root project 'demo'.") }, Task { name: String::from("dependencyInsight"), description: String::from("Displays the insight into a specific dependency in root project 'demo'.") }, Task { name: String::from("dependencyManagement"), description: String::from("Displays the dependency management declared in root project 'demo'.") }, Task { name: String::from("help"), description: String::from("Displays a help message.") }, Task { name: String::from("javaToolchains"), description: String::from("Displays the detected java toolchains.") }, Task { name: String::from("kotlinDslAccessorsReport"), description: String::from("Prints the Kotlin code for accessing the currently available project extensions and conventions.") }, Task { name: String::from("outgoingVariants"), description: String::from("Displays the outgoing variants of root project 'demo'.") }, Task { name: String::from("projects"), description: String::from("Displays the sub-projects of root project 'demo'.") }, Task { name: String::from("properties"), description: String::from("Displays the properties of root project 'demo'.") }, Task { name: String::from("resolvableConfigurations"), description: String::from("Displays the configurations that can be resolved in root project 'demo'.") }, Task { name: String::from("tasks"), description: String::from("Displays the tasks runnable from root project 'demo'.") }] }, + TaskGroup { heading: String::from("Verification tasks"), tasks: vec![Task { name: String::from("check"), description: String::from("Runs all checks.") }, Task { name: String::from("test"), description: String::from("Runs the test suite.") }] } + ] + ); + } + } +} diff --git a/buildpacks/gradle/src/layers/gradle_home.rs b/buildpacks/gradle/src/layers/gradle_home.rs new file mode 100644 index 00000000..5dd3c55d --- /dev/null +++ b/buildpacks/gradle/src/layers/gradle_home.rs @@ -0,0 +1,90 @@ +use crate::{GradleBuildpack, GradleBuildpackError, GRADLE_TASK_NAME_HEROKU_START_DAEMON}; +use indoc::{formatdoc, indoc}; +use libcnb::build::BuildContext; +use libcnb::data::layer_content_metadata::LayerTypes; +use libcnb::generic::GenericMetadata; +use libcnb::layer::{ExistingLayerStrategy, Layer, LayerData, LayerResult, LayerResultBuilder}; +use libcnb::layer_env::{LayerEnv, ModificationBehavior, Scope}; +use libcnb::Buildpack; +use std::fs; +use std::path::Path; + +pub(crate) struct GradleHomeLayer; + +impl Layer for GradleHomeLayer { + type Buildpack = GradleBuildpack; + type Metadata = GenericMetadata; + + fn types(&self) -> LayerTypes { + LayerTypes { + launch: true, + build: true, + cache: true, + } + } + + fn create( + &self, + _context: &BuildContext, + layer_path: &Path, + ) -> Result, ::Error> { + // https://docs.gradle.org/8.3/userguide/build_environment.html#sec:gradle_configuration_properties + fs::write( + layer_path.join("gradle.properties"), + indoc! {" + org.gradle.welcome=never + org.gradle.caching=true + "}, + ) + .map_err(GradleBuildpackError::WriteGradlePropertiesError)?; + + // https://docs.gradle.org/8.3/userguide/init_scripts.html + fs::write( + layer_path.join("init.gradle.kts"), + formatdoc! {" + allprojects {{ + tasks.register(\"{task_name}\") + }}", + task_name = GRADLE_TASK_NAME_HEROKU_START_DAEMON + }, + ) + .map_err(GradleBuildpackError::WriteGradleInitScriptError)?; + + LayerResultBuilder::new(None) + .env(LayerEnv::new().chainable_insert( + Scope::All, + ModificationBehavior::Override, + "GRADLE_USER_HOME", + layer_path, + )) + .build() + } + + fn existing_layer_strategy( + &self, + _context: &BuildContext, + _layer_data: &LayerData, + ) -> Result::Error> { + Ok(ExistingLayerStrategy::Update) + } + + fn update( + &self, + _context: &BuildContext, + layer_data: &LayerData, + ) -> Result, ::Error> { + // Remove daemon metadata from the cached directory. Among other things, it contains a list + // of PIDs from previous runs that will clutter up the output and aren't meaningful with + // containerized builds anyway. + let daemon_dir_path = layer_data.path.join("daemon"); + if daemon_dir_path.is_dir() { + // We explicitly ignore potential errors since not being able to remove this directory + // should not fail the build as it's mostly for output cosmetics only. + let _ignored_result = fs::remove_dir_all(daemon_dir_path); + } + + LayerResultBuilder::new(layer_data.content_metadata.metadata.clone()) + .env(layer_data.env.clone()) + .build() + } +} diff --git a/buildpacks/gradle/src/layers/mod.rs b/buildpacks/gradle/src/layers/mod.rs new file mode 100644 index 00000000..a80d8911 --- /dev/null +++ b/buildpacks/gradle/src/layers/mod.rs @@ -0,0 +1 @@ +pub(crate) mod gradle_home; diff --git a/buildpacks/gradle/src/main.rs b/buildpacks/gradle/src/main.rs new file mode 100644 index 00000000..04fc2bb0 --- /dev/null +++ b/buildpacks/gradle/src/main.rs @@ -0,0 +1,155 @@ +// Enable rustc and Clippy lints that are disabled by default. +// https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html#unused-crate-dependencies +#![warn(unused_crate_dependencies)] +// https://rust-lang.github.io/rust-clippy/stable/index.html +#![warn(clippy::pedantic)] +// This lint is too noisy and enforces a style that reduces readability in many cases. +#![allow(clippy::module_name_repetitions)] + +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::gradle_command::GradleCommandError; +use crate::layers::gradle_home::GradleHomeLayer; +use crate::GradleBuildpackError::{GradleWrapperIoError, GradleWrapperStatusError}; +use buildpacks_jvm_shared as shared; +#[cfg(test)] +use buildpacks_jvm_shared_test as _; +use libcnb::build::{BuildContext, BuildResult, BuildResultBuilder}; +use libcnb::data::build_plan::BuildPlanBuilder; +use libcnb::data::layer_name; +use libcnb::detect::{DetectContext, DetectResult, DetectResultBuilder}; +use libcnb::generic::GenericPlatform; +use libcnb::{buildpack_main, Buildpack, Env}; +#[cfg(test)] +use libcnb_test as _; +use libherokubuildpack::command::CommandExt; +use libherokubuildpack::log::log_header; +use serde::Deserialize; +use std::io::{stderr, stdout}; +use std::process::{Command, ExitStatus}; + +mod config; +mod detect; +mod errors; +mod framework; +mod gradle_command; +mod layers; + +pub(crate) struct GradleBuildpack; + +#[derive(Debug)] +pub(crate) enum GradleBuildpackError { + GradleWrapperNotFound, + GradleWrapperIoError(std::io::Error), + GradleWrapperStatusError(ExitStatus), + GetTasksError(GradleCommandError<()>), + GetDependencyReportError(GradleCommandError<()>), + WriteGradlePropertiesError(std::io::Error), + WriteGradleInitScriptError(std::io::Error), + GradleWrapperExecutableBitMissing(std::io::Error), + StartGradleDaemonError(GradleCommandError<()>), + BuildTaskUnknown, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct GradleBuildpackMetadata {} + +impl Buildpack for GradleBuildpack { + type Platform = GenericPlatform; + type Metadata = GradleBuildpackMetadata; + type Error = GradleBuildpackError; + + fn detect(&self, context: DetectContext) -> libcnb::Result { + if is_gradle_project_directory(&context.app_dir) { + DetectResultBuilder::pass() + .build_plan( + BuildPlanBuilder::new() + .requires("jdk") + .provides("jvm-application") + .requires("jvm-application") + .build(), + ) + .build() + } else { + DetectResultBuilder::fail().build() + } + } + + fn build(&self, context: BuildContext) -> libcnb::Result { + log_header("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::GradleWrapperExecutableBitMissing)?; + + let mut gradle_env = Env::from_current(); + shared::env::extend_build_env( + context.handle_layer(layer_name!("home"), GradleHomeLayer)?, + &mut gradle_env, + ); + + log_header("Starting Gradle Daemon"); + gradle_command::start_daemon(&gradle_wrapper_executable_path, &gradle_env) + .map_err(GradleBuildpackError::StartGradleDaemonError)?; + + let project_tasks = gradle_command::tasks(&context.app_dir, &gradle_env) + .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 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", + Framework::Ratpack => "installDist", + }) + }) + .ok_or(GradleBuildpackError::BuildTaskUnknown)?; + + log_header("Running build task"); + + 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(GradleWrapperIoError)?; + + if !output.status.success() { + Err(GradleWrapperStatusError(output.status))?; + } + + if let Err(err) = gradle_command::stop_daemon(&gradle_wrapper_executable_path, &gradle_env) + { + eprintln!("Could not stop gradle daemon: {err:?}"); + } + + BuildResultBuilder::new().build() + } + + fn on_error(&self, error: libcnb::Error) { + libherokubuildpack::error::on_error(on_error_gradle_buildpack, error); + } +} + +buildpack_main!(GradleBuildpack); + +impl From for libcnb::Error { + fn from(e: GradleBuildpackError) -> Self { + libcnb::Error::BuildpackError(e) + } +} + +pub(crate) const GRADLE_TASK_NAME_HEROKU_START_DAEMON: &str = "heroku_buildpack_start_daemon"; diff --git a/buildpacks/gradle/test-apps/heroku-gradle-getting-started b/buildpacks/gradle/test-apps/heroku-gradle-getting-started new file mode 160000 index 00000000..d4184a48 --- /dev/null +++ b/buildpacks/gradle/test-apps/heroku-gradle-getting-started @@ -0,0 +1 @@ +Subproject commit d4184a48e32efeb2b2bc6923b2b487ab7ea61c9e diff --git a/buildpacks/gradle/tests/integration/main.rs b/buildpacks/gradle/tests/integration/main.rs new file mode 100644 index 00000000..bf2f6f03 --- /dev/null +++ b/buildpacks/gradle/tests/integration/main.rs @@ -0,0 +1,21 @@ +//! Bundle all integration tests into one binary to: +//! - Reduce compile times +//! - Reduce required disk space +//! - Increase parallelism +//! +//! See: https://matklad.github.io/2021/02/27/delete-cargo-integration-tests.html#Implications + +use libcnb_test::BuildpackReference; + +mod smoke; +mod ux; + +pub(crate) fn default_buildpacks() -> Vec { + vec![ + BuildpackReference::Other(String::from("heroku/jvm")), + BuildpackReference::Crate, + // Using an explicit version from Docker Hub to prevent failures when there + // are multiple Procfile buildpack versions in the builder image. + BuildpackReference::Other(String::from("docker://docker.io/heroku/procfile-cnb:2.0.1")), + ] +} diff --git a/buildpacks/gradle/tests/integration/smoke.rs b/buildpacks/gradle/tests/integration/smoke.rs new file mode 100644 index 00000000..ea7b155a --- /dev/null +++ b/buildpacks/gradle/tests/integration/smoke.rs @@ -0,0 +1,20 @@ +//! Smoke tests that ensure a set of basic apps build successfully and the resulting container +//! exposes the HTTP interface of that app as expected. They also re-build the app and assert the +//! resulting container again to ensure that potential caching logic in the buildpack does not +//! break subsequent builds. +//! +//! These tests are strictly happy-path tests and do not assert any output of the buildpack. + +use crate::default_buildpacks; +use buildpacks_jvm_shared_test::{smoke_test, DEFAULT_INTEGRATION_TEST_BUILDER}; + +#[test] +#[ignore = "integration test"] +fn smoke_test_getting_started_guide() { + smoke_test( + DEFAULT_INTEGRATION_TEST_BUILDER, + "test-apps/heroku-gradle-getting-started", + default_buildpacks(), + "Getting Started with Gradle on Heroku", + ); +} diff --git a/buildpacks/gradle/tests/integration/ux.rs b/buildpacks/gradle/tests/integration/ux.rs new file mode 100644 index 00000000..2763020d --- /dev/null +++ b/buildpacks/gradle/tests/integration/ux.rs @@ -0,0 +1,36 @@ +use crate::default_buildpacks; +use buildpacks_jvm_shared::system_properties::write_system_properties; +use buildpacks_jvm_shared_test::DEFAULT_INTEGRATION_TEST_BUILDER; +use libcnb_test::{BuildConfig, PackResult, TestRunner}; +use std::collections::HashMap; + +/// The buildpack requires (unless otherwise configured) that the application build defines a +/// `stage` task. That task is not a default sbt task but is usually added by sbt-native-packager. +/// +/// To guide new users that might not be aware that they need a `stage` task, we need to output a +/// descriptive message that explains the issue instead of only relying on sbt telling the user +/// that the `stage` task could not be found. +#[test] +#[ignore = "integration test"] +fn test_unsupported_java_version() { + let build_config = BuildConfig::new( + DEFAULT_INTEGRATION_TEST_BUILDER, + "test-apps/heroku-gradle-getting-started", + ) + .buildpacks(default_buildpacks()) + .expected_pack_result(PackResult::Failure) + .app_dir_preprocessor(|dir| { + write_system_properties( + &dir, + &HashMap::from([(String::from("java.runtime.version"), String::from("7"))]), + ) + .unwrap() + }) + .to_owned(); + + TestRunner::default().build(&build_config, |context| { + println!("{}", context.pack_stdout); + println!("{}", context.pack_stderr); + // + }); +} diff --git a/meta-buildpacks/java/buildpack.toml b/meta-buildpacks/java/buildpack.toml index b3292534..96de3c59 100644 --- a/meta-buildpacks/java/buildpack.toml +++ b/meta-buildpacks/java/buildpack.toml @@ -26,6 +26,21 @@ id = "heroku/procfile" version = "2.0.1" optional = true +[[order]] + +[[order.group]] +id = "heroku/jvm" +version = "3.0.0" + +[[order.group]] +id = "heroku/gradle" +version = "3.0.0" + +[[order.group]] +id = "heroku/procfile" +version = "2.0.1" +optional = true + [metadata] [metadata.release] [metadata.release.image] diff --git a/meta-buildpacks/java/package.toml b/meta-buildpacks/java/package.toml index e57b6cbc..1e271ee2 100644 --- a/meta-buildpacks/java/package.toml +++ b/meta-buildpacks/java/package.toml @@ -7,5 +7,8 @@ uri = "libcnb:heroku/jvm" [[dependencies]] uri = "libcnb:heroku/maven" +[[dependencies]] +uri = "libcnb:heroku/gradle" + [[dependencies]] uri = "docker://docker.io/heroku/procfile-cnb@sha256:ea7219d4bb50196b4f292c9aae397b17255c59a243d7408535d2a03a5cd2b040" diff --git a/shared/src/fs.rs b/shared/src/fs.rs index ae247e4c..261a7e55 100644 --- a/shared/src/fs.rs +++ b/shared/src/fs.rs @@ -1,3 +1,5 @@ +use std::borrow::Borrow; +use std::fs::Permissions; use std::path::{Path, PathBuf}; /// Returns an iterator over the contents of the given directory. @@ -18,3 +20,25 @@ where .and_then(Iterator::collect::>>) .map(|dir_entries| dir_entries.into_iter().map(|dir_entry| dir_entry.path())) } + +#[cfg(unix)] +pub fn is_executable>(path: P) -> bool { + use std::os::unix::fs::PermissionsExt; + + path.as_ref() + .metadata() + .map(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0) + .ok() + .unwrap_or_default() +} + +#[cfg(unix)] +#[allow(clippy::missing_errors_doc)] +pub fn set_executable>(path: P) -> std::io::Result<()> { + use std::os::unix::fs::PermissionsExt; + + let permissions = path.as_ref().metadata()?.permissions(); + let new_permissions = Permissions::from_mode(permissions.mode() | 0o111); + + std::fs::set_permissions(path.borrow(), new_permissions) +} diff --git a/shared/src/system_properties.rs b/shared/src/system_properties.rs index 206f0d6f..b6bac8d9 100644 --- a/shared/src/system_properties.rs +++ b/shared/src/system_properties.rs @@ -11,7 +11,7 @@ use std::path::Path; pub fn read_system_properties( app_dir: &Path, ) -> Result, ReadSystemPropertiesError> { - none_on_not_found(fs::File::open(app_dir.join("system.properties"))) + none_on_not_found(fs::File::open(app_dir.join(SYSTEM_PROPERTIES_FILE_NAME))) .map_err(ReadSystemPropertiesError::IoError) .and_then(|optional_file| { optional_file @@ -22,8 +22,31 @@ pub fn read_system_properties( }) } +/// Writes all given properties to the `system.properties` file in the app's directory. +// Implicit hasher is allowed since the properties crate only works with the default one. +#[allow(clippy::missing_errors_doc, clippy::implicit_hasher)] +pub fn write_system_properties( + app_dir: &Path, + properties: &HashMap, +) -> Result<(), WriteSystemPropertiesError> { + fs::File::create(app_dir.join(SYSTEM_PROPERTIES_FILE_NAME)) + .map_err(WriteSystemPropertiesError::IoError) + .and_then(|file| { + java_properties::write(file, properties) + .map_err(WriteSystemPropertiesError::SerializationError) + }) +} + #[derive(Debug)] pub enum ReadSystemPropertiesError { IoError(std::io::Error), ParseError(java_properties::PropertiesError), } + +#[derive(Debug)] +pub enum WriteSystemPropertiesError { + IoError(std::io::Error), + SerializationError(java_properties::PropertiesError), +} + +const SYSTEM_PROPERTIES_FILE_NAME: &str = "system.properties";