Repository: railwayapp/nixpacks (rust)

Issue:
Spring Boot Application Fails to Build Correctly
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Describe the bug

Hi and thanks a lot for your awesome service.

I try to build and run a simple spring boot 3.0.0 app.
Worked until a couple of hours perfectly on railway.app.
Now I see the following error:

```
#11 3.627
#11 3.627 Welcome to Gradle 7.4.2!
#11 3.627
#11 3.628 Here are the highlights of this release:
#11 3.628  - Aggregated test and JaCoCo reports
#11 3.628  - Marking additional test source directories as tests in IntelliJ
#11 3.628  - Support for Adoptium JDKs in Java toolchains
#11 3.628
#11 3.628 For more details see https://docs.gradle.org/7.4.2/release-notes.html
#11 3.628
#11 3.726 Starting a Gradle Daemon (subsequent builds will be faster)
 
#11 6.926
#11 6.926 FAILURE: Build failed with an exception.
#11 6.926
#11 6.926 * Where:
#11 6.927 Settings file '/app/settings.gradle'
#11 6.927
#11 6.927 * What went wrong:
#11 6.927 Could not compile settings file '/app/settings.gradle'.
#11 6.927 > startup failed:
#11 6.927   General error during conversion: Unsupported class file major version 63
#11 6.927
#11 6.927   java.lang.IllegalArgumentException: Unsupported class file major version 63
#11 6.927   	at groovyjarjarasm.asm.ClassReader.<init>(ClassReader.java:199)
#11 6.927   	at groovyjarjarasm.asm.ClassReader.<init>(ClassReader.java:180)
#11 6.927   	at groovyjarjarasm.asm.ClassReader.<init>(ClassReader.java:166)
#11 6.928   	at groovyjarjarasm.asm.ClassReader.<init>(ClassReader.java:287)
#11 6.928   	at org.codehaus.groovy.ast.decompiled.AsmDecompiler.parseClass(AsmDecompiler.java:81)
 
#11 6.928   	at org.codehaus.groovy.control.ClassNodeResolver.findDecompiled(ClassNodeResolver.java:251)
#11 6.928   	at org.codehaus.groovy.control.ClassNodeResolver.tryAsLoaderClassOrScript(ClassNodeResolver.java:189)
#11 6.928   	at org.codehaus.groovy.control.ClassNodeResolver.findClassNode(ClassNodeResolver.java:169)
#11 6.928   	at org.codehaus.groovy.control.ClassNodeResolver.resolveName(ClassNodeResolver.java:125)
#11 6.928   	at org.codehaus.groovy.ast.decompiled.AsmReferenceResolver.resolveClassNullable(AsmReferenceResolver.java:57)
#11 6.928   	at org.codehaus.groovy.ast.decompiled.AsmReferenceResolver.resolveClass(AsmReferenceResolver.java:44)
#11 6.928   	at org.codehaus.groovy.ast.decompiled.TypeSignatureParser.visitEnd(TypeSignatureParser.java:110)
#11 6.928   	at groovyjarjarasm.asm.signature.SignatureReader.parseType(SignatureReader.java:206)
#11 6.929   	at groovyjarjarasm.asm.signature.SignatureReader.accept(SignatureReader.java:124)
#11 6.929   	at org.codehaus.groovy.ast.decompiled.ClassSignatureParser.parseClassSignature(ClassSignatureParser.java:74)
#11 6.929   	at org.codehaus.groovy.ast.decompiled.ClassSignatureParser.configureClass(ClassSignatureParser.java:32)
#11 6.929   	at org.codehaus.groovy.ast.decompiled.DecompiledClassNode.lazyInitSupers(DecompiledClassNode.java:185)
#11 6.929   	at org.codehaus.groovy.ast.decompiled.DecompiledClassNode.isUsingGenerics(DecompiledClassNode.java:86)
```
This looks familiar to the issue in the comments described in [issue-504](https://github.com/railwayapp/nixpacks/issues/504)



### To reproduce

Steps to reproduce

1. Spring-boot application (I guess build with gradle using buildpacks) - as example https://github.com/Promotos/sir-bookmark-backend
2. Try to build on railway.app

### Expected behavior

Expected would be a successful build

### Environment

- spring-boot app 3.0
- JDK 17
- Gradle

```
==============
Using Nixpacks
==============
Inline cache enabled
 
╔═══════════════════════════════ Nixpacks v1.0.0 ══════════════════════════════╗
║ setup      │ jdk, gradle                                                     ║
║──────────────────────────────────────────────────────────────────────────────║
║ build      │ chmod +x gradlew                                                ║
║            │ ./gradlew build -x check                                        ║
║──────────────────────────────────────────────────────────────────────────────║
║ start      │ java $JAVA_OPTS -jar -Dserver.port=$PORT                        ║
║            │ -Dspring.datasource.url=jdbc:postgresql:/xxxx                                    ║
║            │ -Dspring.datasource.username=xxxx║
║            │ -Dspring.datasource.password=xxxx build/libs/sir-  ║
║            │ bookmark-backend-0.0.1-SNAPSHOT.jar                             ║
╚══════════════════════════════════════════════════════════════════════════════╝
``` 

Pull request:
Java provider default jdk17 and custom versions
This PR 
- Updates the default JDK version for the java provider to version 17
- Allows customizing the JDK and Gradle versions with the `NIXPACKS_JDK_VERSION` and `NIXPACKS_GRADLE_VERSION` environment variables

Fixes #724 

Patch:
diff --git a/docs/pages/docs/providers/java.md b/docs/pages/docs/providers/java.md
index f1d6492..613159b 100644
--- a/docs/pages/docs/providers/java.md
+++ b/docs/pages/docs/providers/java.md
@@ -6,51 +6,72 @@ title: Java
 
 Java is detected if a `pom.[xml|atom|clj|groovy|rb|scala|yaml|yml]` or `gradlew` file is found.
 
-## Install
+## Setup
 
-```
-Skipped
-```
+### JDK
+
+The following major JDK versions are available
+
+- `19`
+- `17` (Default)
+- `11`
+- `8`
+
+The version can be overriden by setting the `NIXPACKS_JDK_VERSION` environment variable.
+
+### Gradle
+
+The following major Gradle verions are available
+
+- `7` (Default)
+- `6`
+- `5`
+- `4`
+
+The version can be overriden by setting the `NIXPACKS_GRADLE_VERSION` environment variable.
 
 ## Build
 
-If Maven is found: 
+If Maven is found:
+
 ```
 /bin/maven -DoutputFile=target/mvn-dependency-list.log -B -DskipTests clean dependency:list install
 ```
 
 If Gradle is found:
+
 ```
 ./gradlew build
 ```
 
 ## Start
 
-If Maven is found: 
+If Maven is found:
+
 ```
 java $JAVA_OPTS -jar target/*jar
 ```
 
+If Maven and Wildfly Swarm is found:
 
-If Maven and Wildfly Swarm is found: 
 ```
 java -Dswarm.http.port=$PORT $JAVA_OPTS -jar target/*jar
 ```
 
+If Maven and Spring Boot is found:
 
-If Maven and Spring Boot is found: 
 ```
 java -Dserver.port=$PORT $JAVA_OPTS -jar target/*jar
 ```
 
-
 If Gradle is found:
+
 ```
 java $JAVA_OPTS -jar build/libs/*.jar
 ```
 
-
 If Gradle and Spring Boot is found:
+
 ```
 java $JAVA_OPTS -jar -Dserver.port=$PORT build/libs/*.jar
 ```
diff --git a/src/providers/java.rs b/src/providers/java.rs
index 96b1b58..f389199 100644
--- a/src/providers/java.rs
+++ b/src/providers/java.rs
@@ -8,11 +8,14 @@ use crate::nixpacks::{
         BuildPlan,
     },
 };
-use anyhow::Result;
-use regex::{Match, Regex};
+use anyhow::{bail, Result};
+use regex::Regex;
 
 pub struct JavaProvider {}
 
+const DEFAULT_JDK_VERSION: u32 = 17;
+const DEFAULT_GRADLE_VERSION: u32 = 7;
+
 impl Provider for JavaProvider {
     fn name(&self) -> &str {
         "java"
@@ -30,11 +33,10 @@ impl Provider for JavaProvider {
             || app.includes_file("gradlew"))
     }
 
-    fn get_build_plan(&self, app: &App, _env: &Environment) -> Result<Option<BuildPlan>> {
-        let mut setup: Phase;
-        let mut build = if self.is_using_gradle(app) {
-            let pkgs = self.get_jdk_and_gradle_pkgs(app)?;
-            setup = Phase::setup(Some(pkgs));
+    fn get_build_plan(&self, app: &App, env: &Environment) -> Result<Option<BuildPlan>> {
+        let (setup, build) = if self.is_using_gradle(app) {
+            let pkgs = self.get_jdk_and_gradle_pkgs(app, env)?;
+            let setup = Phase::setup(Some(pkgs));
 
             let mut build = Phase::build(None);
             let gradle_exe = self.get_gradle_exe(app);
@@ -46,19 +48,26 @@ impl Provider for JavaProvider {
 
             build.add_cmd(format!("{gradle_exe} build -x check"));
             build.add_cache_directory("/root/.gradle");
-            build
+            build.depends_on_phase("setup");
+
+            (setup, build)
         } else {
-            setup = Phase::setup(Some(vec![Pkg::new("jdk")]));
-            setup.add_nix_pkgs(&[Pkg::new("maven")]);
+            let jdk_version = self.get_jdk_version(app, env)?;
+            let jdk_pkg = self.get_jdk_pkg(jdk_version)?;
+
+            let setup = Phase::setup(Some(vec![jdk_pkg, Pkg::new("maven")]));
+
             let mvn_exe = self.get_maven_exe(app);
             let mut build = Phase::build(Some(format!("{mvn_exe} -DoutputFile=target/mvn-dependency-list.log -B -DskipTests clean dependency:list install", 
                 mvn_exe=mvn_exe
             )));
             build.add_cache_directory(".m2/repository");
-            build
+            build.depends_on_phase("setup");
+
+            (setup, build)
         };
+
         let start = StartPhase::new(self.get_start_cmd(app)?);
-        build.depends_on = Some(vec!["setup".to_string()]);
 
         let plan = BuildPlan::new(&vec![setup, build], Some(start));
         Ok(Some(plan))
@@ -143,18 +152,69 @@ impl JavaProvider {
         }
     }
 
-    pub fn get_jdk_and_gradle_pkgs(&self, app: &App) -> Result<Vec<Pkg>> {
-        fn as_default(v: Option<Match>) -> &str {
-            match v {
-                Some(m) => m.as_str(),
-                None => "_",
+    pub fn get_jdk_and_gradle_pkgs(&self, app: &App, env: &Environment) -> Result<Vec<Pkg>> {
+        let gradle_version = self.get_gradle_version(app, env)?;
+        let jdk_version = self.get_jdk_version(app, env)?;
+
+        let pkgs = vec![
+            self.get_jdk_pkg(jdk_version)?,
+            self.get_gradle_pkg(gradle_version)?,
+        ];
+        Ok(pkgs)
+    }
+
+    fn get_jdk_pkg(&self, jdk_version: u32) -> Result<Pkg> {
+        let pkg = match jdk_version {
+            19 => Pkg::new("jdk"),
+            17 => Pkg::new("jdk17"),
+            11 => Pkg::new("jdk11"),
+            8 => Pkg::new("jdk8"),
+            _ => bail!("Unsupported JDK version: {}", jdk_version),
+        };
+
+        Ok(pkg)
+    }
+
+    fn get_gradle_pkg(&self, gradle_version: u32) -> Result<Pkg> {
+        let pkg = match gradle_version {
+            7 => Pkg::new("gradle"),
+            6 => Pkg::new("gradle_6"),
+            5 => Pkg::new("gradle_5"),
+            4 => Pkg::new("gradle_4"),
+            _ => bail!("Unsupported Gradle version: {}", gradle_version),
+        };
+
+        Ok(pkg)
+    }
+
+    fn get_jdk_version(&self, app: &App, env: &Environment) -> Result<u32> {
+        // If the JDK version is manually specified, use that
+        if let Some(jdk_version) = env.get_config_variable("JDK_VERSION") {
+            return Ok(jdk_version.parse::<u32>()?);
+        }
+
+        if self.is_using_gradle(app) {
+            let gradle_version = self.get_gradle_version(app, env)?;
+
+            // Return a JDK version based on the gradle version
+            if gradle_version == 6 {
+                return Ok(11);
+            } else if gradle_version <= 5 {
+                return Ok(8);
             }
         }
 
-        let default_pkgs = vec![Pkg::new("jdk"), Pkg::new("gradle")];
+        Ok(DEFAULT_JDK_VERSION)
+    }
+
+    fn get_gradle_version(&self, app: &App, env: &Environment) -> Result<u32> {
+        // If the Gradle version is manually specified, use that
+        if let Some(gradle_version) = env.get_config_variable("GRADLE_VERSION") {
+            return Ok(gradle_version.parse::<u32>()?);
+        }
 
         if !app.includes_file("gradle/wrapper/gradle-wrapper.properties") {
-            return Ok(default_pkgs);
+            return Ok(DEFAULT_GRADLE_VERSION);
         }
 
         let file_content = app.read_file("gradle/wrapper/gradle-wrapper.properties")?;
@@ -164,35 +224,97 @@ impl JavaProvider {
 
         // If it's still none, return default
         if custom_version.is_none() {
-            return Ok(default_pkgs);
+            return Ok(DEFAULT_GRADLE_VERSION);
         }
 
         let custom_version = custom_version.unwrap();
         let matches = Regex::new(r#"^(?:[\sa-zA-Z-"']*)(\d*)(?:\.*)(\d*)(?:\.*\d*)(?:["']?)$"#)?
             .captures(custom_version.as_str().trim());
 
-        // If no matches, just use default
-        if matches.is_none() {
-            return Ok(default_pkgs);
-        }
-        let matches = matches.unwrap();
-        let parsed_version = as_default(matches.get(1));
+        let parsed_version = match matches {
+            Some(m) => match m.get(1) {
+                Some(v) => v.as_str(),
+                None => return Ok(DEFAULT_GRADLE_VERSION),
+            },
+            None => return Ok(DEFAULT_GRADLE_VERSION),
+        };
 
-        if parsed_version == "_" {
-            return Ok(default_pkgs);
-        }
+        let int_version = parsed_version.parse::<u32>().unwrap_or_default();
+        Ok(int_version)
+    }
+}
 
-        let int_version = parsed_version.parse::<i32>().unwrap_or_default();
-        let pkgs = if int_version == 6 {
-            vec![Pkg::new("jdk11"), Pkg::new("gradle_6")]
-        } else if int_version == 5 {
-            vec![Pkg::new("jdk8"), Pkg::new("gradle_5")]
-        } else if int_version < 5 {
-            vec![Pkg::new("jdk8"), Pkg::new("gradle_4")]
-        } else {
-            default_pkgs
-        };
+#[cfg(test)]
+mod tests {
+    use super::*;
 
-        Ok(pkgs)
+    #[test]
+    fn test_get_jdk_pkg() {
+        let java = JavaProvider {};
+
+        assert_eq!(
+            Pkg::new("jdk17"),
+            java.get_jdk_pkg(
+                java.get_jdk_version(
+                    &App::new("examples/java-gradle-hello-world").unwrap(),
+                    &Environment::from_envs(vec![]).unwrap(),
+                )
+                .unwrap()
+            )
+            .unwrap()
+        );
+
+        assert_eq!(
+            Pkg::new("jdk8"),
+            java.get_jdk_pkg(
+                java.get_jdk_version(
+                    &App::new("examples/java-gradle-hello-world").unwrap(),
+                    &Environment::from_envs(vec!["NIXPACKS_JDK_VERSION=8"]).unwrap(),
+                )
+                .unwrap()
+            )
+            .unwrap()
+        );
+
+        assert_eq!(
+            Pkg::new("jdk11"),
+            java.get_jdk_pkg(
+                java.get_jdk_version(
+                    &App::new("examples/java-gradle-hello-world").unwrap(),
+                    &Environment::from_envs(vec!["NIXPACKS_GRADLE_VERSION=6"]).unwrap(),
+                )
+                .unwrap()
+            )
+            .unwrap()
+        );
+    }
+
+    #[test]
+    fn test_get_gradle_pkg() {
+        let java = JavaProvider {};
+
+        assert_eq!(
+            Pkg::new("gradle"),
+            java.get_gradle_pkg(
+                java.get_gradle_version(
+                    &App::new("examples/java-gradle-hello-world").unwrap(),
+                    &Environment::from_envs(vec![]).unwrap(),
+                )
+                .unwrap()
+            )
+            .unwrap()
+        );
+
+        assert_eq!(
+            Pkg::new("gradle_5"),
+            java.get_gradle_pkg(
+                java.get_gradle_version(
+                    &App::new("examples/java-gradle-hello-world").unwrap(),
+                    &Environment::from_envs(vec!["NIXPACKS_GRADLE_VERSION=5"]).unwrap(),
+                )
+                .unwrap()
+            )
+            .unwrap()
+        );
     }
 }


Test patch:
diff --git a/tests/snapshots/generate_plan_tests__java_gradle_hello_world.snap b/tests/snapshots/generate_plan_tests__java_gradle_hello_world.snap
index 7857656..2a1a2ea 100644
--- a/tests/snapshots/generate_plan_tests__java_gradle_hello_world.snap
+++ b/tests/snapshots/generate_plan_tests__java_gradle_hello_world.snap
@@ -12,6 +12,7 @@ expression: plan
     "build": {
       "name": "build",
       "dependsOn": [
+        "install",
         "setup"
       ],
       "cmds": [
@@ -24,7 +25,7 @@ expression: plan
     "setup": {
       "name": "setup",
       "nixPkgs": [
-        "jdk",
+        "jdk17",
         "gradle"
       ],
       "nixOverlays": [],
diff --git a/tests/snapshots/generate_plan_tests__java_maven.snap b/tests/snapshots/generate_plan_tests__java_maven.snap
index 214dcdd..19b10d4 100644
--- a/tests/snapshots/generate_plan_tests__java_maven.snap
+++ b/tests/snapshots/generate_plan_tests__java_maven.snap
@@ -12,6 +12,7 @@ expression: plan
     "build": {
       "name": "build",
       "dependsOn": [
+        "install",
         "setup"
       ],
       "cmds": [
@@ -24,7 +25,7 @@ expression: plan
     "setup": {
       "name": "setup",
       "nixPkgs": [
-        "jdk",
+        "jdk17",
         "maven"
       ],
       "nixOverlays": [],
diff --git a/tests/snapshots/generate_plan_tests__java_maven_wrapper.snap b/tests/snapshots/generate_plan_tests__java_maven_wrapper.snap
index 626d865..7babc52 100644
--- a/tests/snapshots/generate_plan_tests__java_maven_wrapper.snap
+++ b/tests/snapshots/generate_plan_tests__java_maven_wrapper.snap
@@ -12,6 +12,7 @@ expression: plan
     "build": {
       "name": "build",
       "dependsOn": [
+        "install",
         "setup"
       ],
       "cmds": [
@@ -24,7 +25,7 @@ expression: plan
     "setup": {
       "name": "setup",
       "nixPkgs": [
-        "jdk",
+        "jdk17",
         "maven"
       ],
       "nixOverlays": [],
diff --git a/tests/snapshots/generate_plan_tests__java_spring_boot.snap b/tests/snapshots/generate_plan_tests__java_spring_boot.snap
index 536412b..17327db 100644
--- a/tests/snapshots/generate_plan_tests__java_spring_boot.snap
+++ b/tests/snapshots/generate_plan_tests__java_spring_boot.snap
@@ -12,6 +12,7 @@ expression: plan
     "build": {
       "name": "build",
       "dependsOn": [
+        "install",
         "setup"
       ],
       "cmds": [