Repository: railwayapp/nixpacks
Pull request #718 (2022-12-15 08:04:29)

Add traits Send And Sync to the Provider type
This adds support for running `create_docker_image` inside of tokio threads. 


Patch:
diff --git a/.github/workflows/publish_debian.yml b/.github/workflows/publish_debian.yml
index d910a09..d97a0ac 100644
--- a/.github/workflows/publish_debian.yml
+++ b/.github/workflows/publish_debian.yml
@@ -68,7 +68,7 @@ jobs:
           commit-message: Bump base image
           branch: debian-${{ steps.date.outputs.date }}
           delete-branch: true
-          title: Bump base image to `debian-${{ steps.date.outputs.date }}`
+          title: Bump Debian base image to `debian-${{ steps.date.outputs.date }}`
           labels: release/patch
           
       - name: Login to Docker Hub
diff --git a/.github/workflows/publish_ubuntu.yml b/.github/workflows/publish_ubuntu.yml
index bf3d5e6..810ef85 100644
--- a/.github/workflows/publish_ubuntu.yml
+++ b/.github/workflows/publish_ubuntu.yml
@@ -61,9 +61,9 @@ jobs:
           base: main
           token: ${{ secrets.COMMITTER_TOKEN }}
           commit-message: Bump base image
-          branch: debian-${{ steps.date.outputs.date }}
+          branch: ubuntu-${{ steps.date.outputs.date }}
           delete-branch: true
-          title: Bump base image to `debian-${{ steps.date.outputs.date }}`
+          title: Bump Ubuntu base image to `ubuntu-${{ steps.date.outputs.date }}`
           labels: release/patch
           
       - name: Login to Docker Hub
diff --git a/Cargo.lock b/Cargo.lock
index fe8252a..27dd36b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -947,7 +947,7 @@ dependencies = [
 
 [[package]]
 name = "nixpacks"
-version = "0.16.0"
+version = "1.0.2"
 dependencies = [
  "actix-web",
  "anyhow",
@@ -970,6 +970,7 @@ dependencies = [
  "rand 0.8.5",
  "regex",
  "sanitize-filename",
+ "semver",
  "serde",
  "serde_json",
  "serde_with",
diff --git a/Cargo.toml b/Cargo.toml
index 070a77d..d5795d2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "nixpacks"
-version = "0.16.0"
+version = "1.0.2"
 edition = "2021"
 license = "MIT"
 authors = ["Railway <contact@railway.app>"]
@@ -57,6 +57,7 @@ futures = "0.3"
 portpicker = "0.1.1"
 tokio = { version = "1.21.2", features = ["full"] }
 async-trait = "0.1.58"
+semver = "1.0.14"
 
 [dev-dependencies]
 dotenv-parser = "0.1.3"
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/docs/pages/docs/providers/node.md b/docs/pages/docs/providers/node.md
index 07b3159..1248c47 100644
--- a/docs/pages/docs/providers/node.md
+++ b/docs/pages/docs/providers/node.md
@@ -84,6 +84,8 @@ For example, To install a specific version of PNPM, add a `packageManager` key t
 }
 ```
 
+Corepack will only be used on Node 16 and above.
+
 ## Bun Support
 
 We support Bun, but due to Bun being in alpha, it is unstable and very experimental.
diff --git a/docs/pages/docs/providers/php.md b/docs/pages/docs/providers/php.md
index f93d30f..88113c2 100644
--- a/docs/pages/docs/providers/php.md
+++ b/docs/pages/docs/providers/php.md
@@ -11,7 +11,8 @@ Php is detected if a `composer.json` OR `index.php` file is found.
 The following PHP versions are available
 
 - `8.0`
-- `8.1` (Default)
+- `8.1`
+- `8.2` (Default)
 
 The version is automatically detected by parsing your `composer.json` file.
 
diff --git a/examples/go-cgo-enabled/nixpacks.toml b/examples/go-cgo-enabled/nixpacks.toml
new file mode 100644
index 0000000..7d01e54
--- /dev/null
+++ b/examples/go-cgo-enabled/nixpacks.toml
@@ -0,0 +1,2 @@
+[variables]
+CGO_ENABLED = "1"
diff --git a/examples/ruby-2/.ruby-version b/examples/ruby-2/.ruby-version
new file mode 100644
index 0000000..338a5b5
--- /dev/null
+++ b/examples/ruby-2/.ruby-version
@@ -0,0 +1,1 @@
+2.6.6
diff --git a/examples/ruby-2/Gemfile b/examples/ruby-2/Gemfile
new file mode 100644
index 0000000..e5fe2ae
--- /dev/null
+++ b/examples/ruby-2/Gemfile
@@ -0,0 +1,2 @@
+
+source 'https://rubygems.org'
diff --git a/examples/ruby-2/Gemfile.lock b/examples/ruby-2/Gemfile.lock
new file mode 100644
index 0000000..9742a29
--- /dev/null
+++ b/examples/ruby-2/Gemfile.lock
@@ -0,0 +1,11 @@
+GEM
+  remote: https://rubygems.org/
+  specs:
+
+PLATFORMS
+  arm64-darwin-21
+
+DEPENDENCIES
+
+BUNDLED WITH
+   2.3.7
diff --git a/examples/ruby-2/Procfile b/examples/ruby-2/Procfile
new file mode 100644
index 0000000..82cb606
--- /dev/null
+++ b/examples/ruby-2/Procfile
@@ -0,0 +1,1 @@
+web: ruby app.rb
\ No newline at end of file
diff --git a/examples/ruby-2/app.rb b/examples/ruby-2/app.rb
new file mode 100644
index 0000000..28fedfe
--- /dev/null
+++ b/examples/ruby-2/app.rb
@@ -0,0 +1,1 @@
+puts "Hello from Ruby 2"
diff --git a/src/lib.rs b/src/lib.rs
index 9ca714b..d5ca931 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -47,7 +47,7 @@ mod chain;
 pub mod nixpacks;
 pub mod providers;
 
-pub fn get_providers() -> &'static [&'static dyn Provider] {
+pub fn get_providers() -> &'static [&'static (dyn Provider)] {
     &[
         &CrystalProvider {},
         &CSharpProvider {},
diff --git a/src/nixpacks/app.rs b/src/nixpacks/app.rs
index 8932955..ee1c08e 100644
--- a/src/nixpacks/app.rs
+++ b/src/nixpacks/app.rs
@@ -212,7 +212,7 @@ impl App {
 
     /// Get the path in the container to an asset defined in `static_assets`.
     pub fn asset_path(&self, name: &str) -> String {
-        format!("{}{}", ASSETS_DIR, name)
+        format!("{ASSETS_DIR}{name}")
     }
 }
 
diff --git a/src/nixpacks/builder/docker/docker_image_builder.rs b/src/nixpacks/builder/docker/docker_image_builder.rs
index a5b2ffb..386ad54 100644
--- a/src/nixpacks/builder/docker/docker_image_builder.rs
+++ b/src/nixpacks/builder/docker/docker_image_builder.rs
@@ -64,7 +64,7 @@ impl ImageBuilder for DockerImageBuilder {
 
         // If printing the Dockerfile, don't write anything to disk
         if self.options.print_dockerfile {
-            println!("{}", dockerfile);
+            println!("{dockerfile}");
             return Ok(());
         }
 
@@ -86,7 +86,7 @@ impl ImageBuilder for DockerImageBuilder {
 
             self.logger.log_section("Successfully Built!");
             println!("\nRun:");
-            println!("  docker run -it {}", name);
+            println!("  docker run -it {name}");
 
             if self.options.incremental_cache_image.is_some() {
                 incremental_cache.create_image(
@@ -161,7 +161,7 @@ impl DockerImageBuilder {
         for (name, value) in &plan.variables.clone().unwrap_or_default() {
             docker_build_cmd
                 .arg("--build-arg")
-                .arg(format!("{}={}", name, value));
+                .arg(format!("{name}={value}"));
         }
 
         // Add user defined tags and labels to the image
diff --git a/src/nixpacks/builder/docker/dockerfile_generation.rs b/src/nixpacks/builder/docker/dockerfile_generation.rs
index 502a212..bb95709 100644
--- a/src/nixpacks/builder/docker/dockerfile_generation.rs
+++ b/src/nixpacks/builder/docker/dockerfile_generation.rs
@@ -5,7 +5,7 @@ use crate::nixpacks::{
     app,
     environment::Environment,
     images::DEFAULT_BASE_IMAGE,
-    nix::{create_nix_expressions_for_phases, nix_file_names_for_phases},
+    nix::{create_nix_expressions_for_phases, nix_file_names_for_phases, setup_files_for_phases},
     plan::{
         phase::{Phase, StartPhase},
         BuildPlan,
@@ -105,6 +105,9 @@ impl DockerfileGenerator for BuildPlan {
     ) -> Result<String> {
         let plan = self;
 
+        let setup_files = setup_files_for_phases(&plan.phases.clone().unwrap_or_default());
+        let setup_copy_cmds = utils::get_copy_commands(&setup_files, APP_DIR).join("\n");
+
         let nix_file_names = nix_file_names_for_phases(&plan.phases.clone().unwrap_or_default());
 
         let mut nix_install_cmds: Vec<String> = Vec::new();
@@ -161,7 +164,7 @@ impl DockerfileGenerator for BuildPlan {
             let rel_assets_slash_path = rel_assets_path
                 .to_slash()
                 .context("Failed to convert nix file path to slash path.")?;
-            format!("COPY {} {}", rel_assets_slash_path, app::ASSETS_DIR)
+            format!("COPY {rel_assets_slash_path} {}", app::ASSETS_DIR)
         };
 
         let phases = plan.get_sorted_phases()?;
@@ -198,6 +201,7 @@ impl DockerfileGenerator for BuildPlan {
             ENTRYPOINT [\"/bin/bash\", \"-l\", \"-c\"]
             WORKDIR {APP_DIR}
 
+            {setup_copy_cmds}
             {nix_install_cmds}
             {apt_pkgs_str}
             {assets_copy_cmd}
@@ -209,6 +213,7 @@ impl DockerfileGenerator for BuildPlan {
         ", 
         base_image=base_image,
         APP_DIR=APP_DIR,
+        setup_copy_cmds=setup_copy_cmds,
         nix_install_cmds=nix_install_cmds,
         apt_pkgs_str=apt_pkgs_str,
         assets_copy_cmd=assets_copy_cmd,
@@ -259,11 +264,11 @@ impl BuildPlan {
                     let path = Path::new(&static_assets_path).join(name);
                     let parent = path.parent().unwrap();
                     fs::create_dir_all(parent)
-                        .context(format!("Creating parent directory for {}", name))?;
+                        .context(format!("Creating parent directory for {name}"))?;
                     let mut file =
-                        File::create(path).context(format!("Creating asset file for {}", name))?;
+                        File::create(path).context(format!("Creating asset file for {name}"))?;
                     file.write_all(content.as_bytes())
-                        .context(format!("Writing asset {}", name))?;
+                        .context(format!("Writing asset {name}"))?;
                 }
             }
         }
@@ -355,7 +360,7 @@ impl DockerfileGenerator for Phase {
         let (build_path, run_path) = if let Some(paths) = &phase.paths {
             let joined_paths = paths.join(":");
             (
-                format!("ENV PATH {}:$PATH", joined_paths),
+                format!("ENV PATH {joined_paths}:$PATH"),
                 format!(
                     "RUN printf '\\nPATH={}:$PATH' >> /root/.profile",
                     joined_paths
@@ -393,18 +398,18 @@ impl DockerfileGenerator for Phase {
             ]
             .concat()
             .iter()
-            .map(|s| format!("RUN {}", s))
+            .map(|s| format!("RUN {s}"))
             .collect::<Vec<_>>()
             .join("\n");
 
-            format!("{}\n{}", cache_copy_in_command, run_commands)
+            format!("{cache_copy_in_command}\n{run_commands}")
         } else {
             phase
                 .cmds
                 .clone()
                 .unwrap_or_default()
                 .iter()
-                .map(|s| format!("RUN {} {}", cache_mount, s))
+                .map(|s| format!("RUN {cache_mount} {s}"))
                 .collect::<Vec<_>>()
                 .join("\n")
         };
diff --git a/src/nixpacks/builder/docker/file_server.rs b/src/nixpacks/builder/docker/file_server.rs
index 6794b3d..1f42c20 100644
--- a/src/nixpacks/builder/docker/file_server.rs
+++ b/src/nixpacks/builder/docker/file_server.rs
@@ -39,14 +39,14 @@ impl FileServer {
             access_token: Uuid::new_v4().to_string(),
             listen_to_ip: NIXPACKS_SERVER_LISTEN_TO_IP.to_string(),
             port,
-            upload_url: format!("http://{}:{}/upload/", NIXPACKS_SERVER_HOST, port),
+            upload_url: format!("http://{NIXPACKS_SERVER_HOST}:{port}/upload/"),
         };
 
         let server_config = config.clone();
         thread::spawn(move || {
             let server_future = FileServer::run_app(server_config);
             if let Err(e) = rt::System::new().block_on(server_future) {
-                println!("File server error: {}", e);
+                println!("File server error: {e}");
             }
         });
 
diff --git a/src/nixpacks/builder/docker/incremental_cache.rs b/src/nixpacks/builder/docker/incremental_cache.rs
index 30727ae..79635c9 100644
--- a/src/nixpacks/builder/docker/incremental_cache.rs
+++ b/src/nixpacks/builder/docker/incremental_cache.rs
@@ -130,7 +130,7 @@ impl IncrementalCache {
                     .split('/')
                     .into_iter()
                     .filter(|c| !c.is_empty())
-                    .map(|c| format!("{}?", c))
+                    .map(|c| format!("{c}?"))
                     .collect::<Vec<_>>()
                     .join("/");
 
@@ -163,7 +163,7 @@ impl IncrementalCache {
                         "if [ -d \"{sanitized_dir}\" ]; then curl -v -T {} {} --header \"t:{}\" --retry 3 --retry-all-errors; fi;",
                         compressed_file_name, server_config.upload_url, server_config.access_token,
                     ),
-                    format!("if [ -d \"{sanitized_dir}\" ]; then rm -rf {}; fi", sanitized_dir),
+                    format!("if [ -d \"{sanitized_dir}\" ]; then rm -rf {sanitized_dir}; fi"),
                 ]
             })
             .collect::<Vec<String>>()
diff --git a/src/nixpacks/builder/docker/utils.rs b/src/nixpacks/builder/docker/utils.rs
index b41c1f1..877c74f 100644
--- a/src/nixpacks/builder/docker/utils.rs
+++ b/src/nixpacks/builder/docker/utils.rs
@@ -11,9 +11,9 @@ pub fn get_cache_mount(
             .iter()
             .map(|dir| {
                 let mut sanitized_dir = dir.replace('~', "/root");
-                let sanitized_key = sanitize_cache_key(&format!("{}-{}", cache_key, sanitized_dir));
+                let sanitized_key = sanitize_cache_key(&format!("{cache_key}-{sanitized_dir}"));
                 if !sanitized_dir.starts_with('/') {
-                    sanitized_dir = format!("/app/{}", sanitized_dir);
+                    sanitized_dir = format!("/app/{sanitized_dir}");
                 }
                 format!(
                     "--mount=type=cache,id={},target={}",
@@ -46,7 +46,7 @@ pub fn get_copy_commands(files: &[String], app_dir: &str) -> Vec<String> {
 
 pub fn get_copy_from_commands(from: &str, files: &[String], app_dir: &str) -> Vec<String> {
     if files.is_empty() {
-        vec![format!("COPY --from=0 {} {}", app_dir, app_dir)]
+        vec![format!("COPY --from=0 {app_dir} {app_dir}")]
     } else {
         files
             .iter()
@@ -65,7 +65,7 @@ pub fn get_copy_from_commands(from: &str, files: &[String], app_dir: &str) -> Ve
 pub fn get_exec_command(command: &str) -> String {
     let params = command.replace('\"', "\\\"");
 
-    format!("CMD [\"{}\"]", params)
+    format!("CMD [\"{params}\"]")
 }
 
 #[cfg(test)]
diff --git a/src/nixpacks/environment.rs b/src/nixpacks/environment.rs
index 701144b..8a3d9b9 100644
--- a/src/nixpacks/environment.rs
+++ b/src/nixpacks/environment.rs
@@ -44,7 +44,7 @@ impl Environment {
     }
 
     pub fn get_config_variable(&self, name: &str) -> Option<String> {
-        self.get_variable(format!("NIXPACKS_{}", name).as_str())
+        self.get_variable(format!("NIXPACKS_{name}").as_str())
             .map(|var| var.replace('\n', ""))
     }
 
@@ -67,6 +67,12 @@ impl Environment {
     pub fn clone_variables(env: &Environment) -> EnvironmentVariables {
         env.variables.clone()
     }
+
+    pub fn append_variables(env: &Environment, variables: EnvironmentVariables) -> Environment {
+        let mut new_env = Environment::new(Environment::clone_variables(env));
+        new_env.variables.extend(variables);
+        new_env
+    }
 }
 
 #[cfg(test)]
diff --git a/src/nixpacks/images.rs b/src/nixpacks/images.rs
index b84b852..23221d7 100644
--- a/src/nixpacks/images.rs
+++ b/src/nixpacks/images.rs
@@ -1,2 +1,5 @@
-pub const DEFAULT_BASE_IMAGE: &str = "ghcr.io/railwayapp/nixpacks:debian-1670889918";
-pub const DEBIAN_SLIM_IMAGE: &str = "debian:bullseye-slim";
+pub const UBUNTU_BASE_IMAGE: &str = "ghcr.io/railwayapp/nixpacks:ubuntu-1671044363";
+pub const DEBIAN_BASE_IMAGE: &str = "ghcr.io/railwayapp/nixpacks:debian-1671494681";
+pub const DEFAULT_BASE_IMAGE: &str = UBUNTU_BASE_IMAGE;
+
+pub const STANDALONE_IMAGE: &str = "ubuntu:jammy";
diff --git a/src/nixpacks/logger.rs b/src/nixpacks/logger.rs
index bf9dfc1..70027f3 100644
--- a/src/nixpacks/logger.rs
+++ b/src/nixpacks/logger.rs
@@ -12,7 +12,7 @@ impl Logger {
     }
 
     pub fn log_step(&self, msg: &str) {
-        println!("=> {}", msg);
+        println!("=> {msg}");
     }
 }
 
diff --git a/src/nixpacks/nix/mod.rs b/src/nixpacks/nix/mod.rs
index 376ff49..b949611 100644
--- a/src/nixpacks/nix/mod.rs
+++ b/src/nixpacks/nix/mod.rs
@@ -6,9 +6,9 @@ use crate::nixpacks::plan::phase::{Phase, Phases};
 pub mod pkg;
 
 // This line is automatically updated.
-// Last Modified: 2022-09-12 17:11:54 UTC+0000
-// https://github.com/NixOS/nixpkgs/commit/a0b7e70db7a55088d3de0cc370a59f9fbcc906c3
-pub const NIXPKGS_ARCHIVE: &str = "a0b7e70db7a55088d3de0cc370a59f9fbcc906c3";
+// Last Modified: 2022-12-19 17:04:12 UTC+0000
+// https://github.com/NixOS/nixpkgs/commit/bb31220cca6d044baa6dc2715b07497a2a7c4bc7
+pub const NIXPKGS_ARCHIVE: &str = "bb31220cca6d044baa6dc2715b07497a2a7c4bc7";
 
 #[derive(Eq, PartialEq, Default, Debug, Clone)]
 struct NixGroup {
@@ -16,6 +16,7 @@ struct NixGroup {
     pkgs: Vec<String>,
     libs: Vec<String>,
     overlays: Vec<String>,
+    files: Vec<String>,
 }
 
 fn group_nix_packages_by_archive(phases: &[Phase]) -> Vec<NixGroup> {
@@ -29,6 +30,7 @@ fn group_nix_packages_by_archive(phases: &[Phase]) -> Vec<NixGroup> {
             pkgs: phase.nix_pkgs.clone().unwrap_or_default(),
             libs: phase.nix_libs.clone().unwrap_or_default(),
             overlays: phase.nix_overlays.clone().unwrap_or_default(),
+            files: phase.only_include_files.clone().unwrap_or_default(),
         });
 
     for g in groups {
@@ -37,6 +39,7 @@ fn group_nix_packages_by_archive(phases: &[Phase]) -> Vec<NixGroup> {
                 group.pkgs.extend(g.pkgs);
                 group.libs.extend(g.libs);
                 group.overlays.extend(g.overlays);
+                group.files.extend(g.files);
             }
             None => {
                 archive_to_packages.insert(g.archive.clone(), g);
@@ -75,9 +78,23 @@ pub fn nix_file_names_for_phases(phases: &Phases) -> Vec<String> {
     archives.iter().map(nix_file_name).collect()
 }
 
+pub fn setup_files_for_phases(phases: &Phases) -> Vec<String> {
+    let groups = group_nix_packages_by_archive(
+        &phases
+            .values()
+            .map(std::clone::Clone::clone)
+            .collect::<Vec<_>>(),
+    );
+
+    groups.iter().fold(Vec::new(), |mut acc, g| {
+        acc.extend(g.files.clone());
+        acc
+    })
+}
+
 fn nix_file_name(archive: &Option<String>) -> String {
     match archive {
-        Some(archive) => format!("nixpkgs-{}.nix", archive),
+        Some(archive) => format!("nixpkgs-{archive}.nix"),
         None => "nixpkgs.nix".to_string(),
     }
 }
@@ -99,7 +116,7 @@ fn nix_expression_for_group(group: &NixGroup) -> String {
     let overlays_string = group
         .overlays
         .iter()
-        .map(|url| format!("(import (builtins.fetchTarball \"{}\"))", url))
+        .map(|url| format!("(import (builtins.fetchTarball \"{url}\"))"))
         .collect::<Vec<String>>()
         .join("\n");
 
@@ -110,16 +127,17 @@ fn nix_expression_for_group(group: &NixGroup) -> String {
 
     // If the openssl library is added, set the OPENSSL_DIR and OPENSSL_LIB_DIR environment variables
     // In the future, we will probably want a generic way for providers to set variables based off Nix package locations
-    let openssl_dirs = if libs.contains("openssl") {
-        formatdoc! {"
-          export OPENSSL_DIR=\"${{openssl.dev}}\"
-          export OPENSSL_LIB_DIR=\"${{openssl.out}}/lib\"
+    let openssl_dirs =
+        if let Some(openssl_lib) = group.libs.iter().find(|lib| lib.contains("openssl")) {
+            formatdoc! {"
+          export OPENSSL_DIR=\"${{{openssl_lib}.dev}}\"
+          export OPENSSL_LIB_DIR=\"${{{openssl_lib}.out}}/lib\"
         "}
-    } else {
-        String::new()
-    };
+        } else {
+            String::new()
+        };
 
-    let name = format!("{}-env", archive);
+    let name = format!("{archive}-env");
     let nix_expression = formatdoc! {"
             {{ }}:
 
@@ -162,6 +180,7 @@ mod tests {
     fn test_group_nix_packages_by_archive() {
         let mut setup1 = Phase::setup(Some(vec![Pkg::new("foo"), Pkg::new("bar")]));
         setup1.add_pkgs_libs(vec!["lib1".to_string()]);
+        setup1.add_file_dependency("test-file".to_string());
 
         let mut setup2 = Phase::setup(Some(vec![Pkg::new("hello"), Pkg::new("world")]));
         setup2.nixpkgs_archive = Some("archive2".to_string());
@@ -176,7 +195,8 @@ mod tests {
                 archive: None,
                 pkgs: vec!["foo".to_string(), "bar".to_string(), "baz".to_string()],
                 libs: vec!["lib1".to_string()],
-                overlays: vec![]
+                overlays: vec![],
+                files: vec!["test-file".to_string()]
             }
         );
         assert_eq!(
@@ -185,7 +205,8 @@ mod tests {
                 archive: Some("archive2".to_string()),
                 pkgs: vec!["hello".to_string(), "world".to_string()],
                 libs: vec![],
-                overlays: vec![]
+                overlays: vec![],
+                files: vec![]
             }
         );
     }
diff --git a/src/nixpacks/nix/pkg.rs b/src/nixpacks/nix/pkg.rs
index 936054c..0cd8026 100644
--- a/src/nixpacks/nix/pkg.rs
+++ b/src/nixpacks/nix/pkg.rs
@@ -23,10 +23,10 @@ impl Pkg {
             Some(overrides) => {
                 let override_string = overrides
                     .iter()
-                    .map(|(name, value)| format!("{} = {};", name, value))
+                    .map(|(name, value)| format!("{name} = {value};"))
                     .collect::<Vec<_>>()
                     .join(" ");
-                format!("({}.override {{ {} }})", self.name, override_string)
+                format!("({}.override {{ {override_string} }})", self.name)
             }
             None => self.name.clone(),
         }
@@ -55,10 +55,10 @@ impl Pkg {
             Some(overrides) => {
                 let override_string = overrides
                     .iter()
-                    .map(|(name, value)| format!("{} = {}", name, value))
+                    .map(|(name, value)| format!("{name} = {value}"))
                     .collect::<Vec<_>>()
                     .join(", ");
-                format!("{} {{ {} }}", self.name, override_string)
+                format!("{} {{ {override_string} }}", self.name)
             }
             None => self.name.clone(),
         }
diff --git a/src/nixpacks/plan/generator.rs b/src/nixpacks/plan/generator.rs
index 121619c..b0080c3 100644
--- a/src/nixpacks/plan/generator.rs
+++ b/src/nixpacks/plan/generator.rs
@@ -25,7 +25,7 @@ pub struct GeneratePlanOptions {
 }
 
 pub struct NixpacksBuildPlanGenerator<'a> {
-    providers: &'a [&'a dyn Provider],
+    providers: &'a [&'a (dyn Provider)],
     config: GeneratePlanOptions,
 }
 
@@ -47,7 +47,7 @@ impl<'a> PlanGenerator for NixpacksBuildPlanGenerator<'a> {
 
 impl NixpacksBuildPlanGenerator<'_> {
     pub fn new<'a>(
-        providers: &'a [&'a dyn Provider],
+        providers: &'a [&'a (dyn Provider)],
         config: GeneratePlanOptions,
     ) -> NixpacksBuildPlanGenerator<'a> {
         NixpacksBuildPlanGenerator { providers, config }
@@ -57,6 +57,12 @@ impl NixpacksBuildPlanGenerator<'_> {
     fn get_build_plan(&self, app: &App, env: &Environment) -> Result<BuildPlan> {
         let plan_before_providers = self.get_plan_before_providers(app, env)?;
 
+        // Add the variables from the nixpacks.toml to environment
+        let env = &Environment::append_variables(
+            env,
+            plan_before_providers.variables.clone().unwrap_or_default(),
+        );
+
         let provider_plan =
             self.get_plan_from_providers(app, env, plan_before_providers.providers.clone())?;
 
@@ -71,7 +77,7 @@ impl NixpacksBuildPlanGenerator<'_> {
             plan.add_variables(Environment::clone_variables(env));
         }
 
-        plan.pin();
+        plan.pin(env.is_config_variable_truthy("DEBIAN"));
 
         Ok(plan)
     }
@@ -195,7 +201,7 @@ impl NixpacksBuildPlanGenerator<'_> {
                 let ext = filename.extension().unwrap_or_default();
 
                 let contents = app.read_file(file_path.as_str()).with_context(|| {
-                    format!("Failed to read Nixpacks config file `{}`", file_path)
+                    format!("Failed to read Nixpacks config file `{file_path}`")
                 })?;
                 let plan = if ext == "toml" {
                     BuildPlan::from_toml(&contents)
@@ -206,7 +212,7 @@ impl NixpacksBuildPlanGenerator<'_> {
                 };
 
                 Some(plan.with_context(|| {
-                    format!("Failed to parse Nixpacks config file `{}`", file_path)
+                    format!("Failed to parse Nixpacks config file `{file_path}`")
                 })?)
             } else {
                 None
diff --git a/src/nixpacks/plan/mod.rs b/src/nixpacks/plan/mod.rs
index 5cd62e7..03fa5b5 100644
--- a/src/nixpacks/plan/mod.rs
+++ b/src/nixpacks/plan/mod.rs
@@ -3,7 +3,7 @@ use self::{
     phase::{Phase, Phases, StartPhase},
     topological_sort::topological_sort,
 };
-use super::images::DEFAULT_BASE_IMAGE;
+use super::images::{DEBIAN_BASE_IMAGE, UBUNTU_BASE_IMAGE};
 use crate::nixpacks::{
     app::{App, StaticAssets},
     environment::{Environment, EnvironmentVariables},
@@ -180,7 +180,7 @@ impl BuildPlan {
             self.add_phase(phase);
         }
 
-        format!("{}:{}", prefix, phase_name)
+        format!("{prefix}:{phase_name}")
     }
 
     pub fn add_dependency_between_phases(&mut self, dependant: &str, dependency: &str) {
@@ -268,10 +268,15 @@ impl BuildPlan {
         BuildPlan::new(&phases, start)
     }
 
-    pub fn pin(&mut self) {
+    pub fn pin(&mut self, use_debian: bool) {
         self.providers = Some(Vec::new());
         if self.build_image.is_none() {
-            self.build_image = Some(DEFAULT_BASE_IMAGE.to_string());
+            let base_image = if use_debian {
+                DEBIAN_BASE_IMAGE
+            } else {
+                UBUNTU_BASE_IMAGE
+            };
+            self.build_image = Some(base_image.to_string());
         }
 
         self.resolve_phase_names();
@@ -410,7 +415,7 @@ mod test {
         )
         .unwrap();
 
-        plan.pin();
+        plan.pin(false);
         assert_eq!(
             plan.get_phase("setup").unwrap().nix_pkgs,
             Some(vec!["nodejs".to_string(), "yarn".to_string()])
diff --git a/src/nixpacks/plan/phase.rs b/src/nixpacks/plan/phase.rs
index d3dc93c..7d17166 100644
--- a/src/nixpacks/plan/phase.rs
+++ b/src/nixpacks/plan/phase.rs
@@ -1,5 +1,5 @@
 use crate::nixpacks::{
-    images::{DEBIAN_SLIM_IMAGE, DEFAULT_BASE_IMAGE},
+    images::{DEFAULT_BASE_IMAGE, STANDALONE_IMAGE},
     nix::{pkg::Pkg, NIXPKGS_ARCHIVE},
 };
 use serde::{Deserialize, Serialize};
@@ -66,7 +66,7 @@ impl Phase {
     }
 
     pub fn prefix_name(&mut self, prefix: &str) {
-        self.name = Some(format!("{}:{}", prefix, self.get_name()));
+        self.name = Some(format!("{prefix}:{}", self.get_name()));
     }
 
     pub fn prefix_dependencies(&mut self, prefix: &str) {
@@ -75,7 +75,7 @@ impl Phase {
                 depends_on
                     .clone()
                     .iter()
-                    .map(|name| format!("{}:{}", prefix, name))
+                    .map(|name| format!("{prefix}:{name}"))
                     .collect::<Vec<_>>(),
             );
         }
@@ -219,7 +219,7 @@ impl StartPhase {
     }
 
     pub fn run_in_slim_image(&mut self) {
-        self.run_image = Some(DEBIAN_SLIM_IMAGE.to_string());
+        self.run_image = Some(STANDALONE_IMAGE.to_string());
     }
 
     pub fn add_file_dependency<S: Into<String>>(&mut self, file: S) {
diff --git a/src/nixpacks/plan/pretty_print.rs b/src/nixpacks/plan/pretty_print.rs
index c1ba644..1bf547b 100644
--- a/src/nixpacks/plan/pretty_print.rs
+++ b/src/nixpacks/plan/pretty_print.rs
@@ -11,7 +11,7 @@ const MAX_BOX_WIDTH: usize = 80;
 
 impl BuildPlan {
     pub fn get_build_string(&self) -> Result<String> {
-        let title_str = format!(" Nixpacks v{} ", NIX_PACKS_VERSION);
+        let title_str = format!(" Nixpacks v{NIX_PACKS_VERSION} ");
         let title_width = console::measure_text_width(title_str.as_str());
 
         let phase_contents = self
@@ -59,13 +59,9 @@ impl BuildPlan {
         let middle_padding_width = console::measure_text_width(middle_padding.as_str());
         let middle_padding = middle_padding.cyan().dimmed().to_string();
 
-        let box_width = std::cmp::min(
-            MAX_BOX_WIDTH,
-            std::cmp::max(
-                MIN_BOX_WIDTH,
-                (edge_width * 2) + first_column_width + middle_padding_width + max_right_content,
-            ),
-        );
+        let box_width =
+            ((edge_width * 2) + first_column_width + middle_padding_width + max_right_content)
+                .clamp(MIN_BOX_WIDTH, MAX_BOX_WIDTH);
 
         let second_column_width =
             box_width - (edge_width * 2) - first_column_width - middle_padding_width;
@@ -123,7 +119,7 @@ impl BuildPlan {
                 )
             })
             .collect::<Vec<_>>()
-            .join(format!("\n{}\n", hor_sep).as_str());
+            .join(format!("\n{hor_sep}\n").as_str());
 
         let start_row = print_row(
             "start",
diff --git a/src/providers/clojure.rs b/src/providers/clojure.rs
index b61ce1e..33f4a91 100644
--- a/src/providers/clojure.rs
+++ b/src/providers/clojure.rs
@@ -42,8 +42,8 @@ impl Provider for ClojureProvider {
         };
 
         // based on project config, uberjar can be created under ./target/uberjar or ./target, This ensure file will be found on the same place whatevery the project config is
-        let move_file_cmd = "if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi";
-        let mut build = Phase::build(Some(format!("{}; {}", build_cmd, move_file_cmd)));
+        let move_file_cmd = "if [ -f /app/target/default+uberjar/*standalone.jar ]; then mv /app/target/default+uberjar/*standalone.jar /app/target/*standalone.jar; fi";
+        let mut build = Phase::build(Some(format!("{build_cmd}; {move_file_cmd}")));
         build.depends_on_phase("setup");
 
         let start = StartPhase::new("bash -c \"java $JAVA_OPTS -jar /app/target/*standalone.jar\"");
diff --git a/src/providers/cobol.rs b/src/providers/cobol.rs
index 60f8e53..680c4e9 100644
--- a/src/providers/cobol.rs
+++ b/src/providers/cobol.rs
@@ -54,7 +54,7 @@ impl Provider for CobolProvider {
         )));
         build.depends_on_phase("setup");
 
-        let start = StartPhase::new(format!("./{}", file_name));
+        let start = StartPhase::new(format!("./{file_name}"));
 
         let plan = BuildPlan::new(&vec![setup, build], Some(start));
         Ok(Some(plan))
diff --git a/src/providers/csharp.rs b/src/providers/csharp.rs
index 908c216..4013d5f 100644
--- a/src/providers/csharp.rs
+++ b/src/providers/csharp.rs
@@ -37,7 +37,7 @@ impl Provider for CSharpProvider {
             .context("Invalid file_name")?
             .to_str()
             .context("Invalid project_name")?;
-        let start = StartPhase::new(format!("./{}/{}", ARTIFACT_DIR, project_name));
+        let start = StartPhase::new(format!("./{ARTIFACT_DIR}/{project_name}"));
 
         let mut plan = BuildPlan::new(&vec![setup, install, build], Some(start));
         plan.add_variables(EnvironmentVariables::from([
diff --git a/src/providers/fsharp.rs b/src/providers/fsharp.rs
index d66a254..d56df79 100644
--- a/src/providers/fsharp.rs
+++ b/src/providers/fsharp.rs
@@ -37,7 +37,7 @@ impl Provider for FSharpProvider {
             .context("Invalid file_name")?
             .to_str()
             .context("Invalid project_name")?;
-        let start = StartPhase::new(format!("./{}/{}", ARTIFACT_DIR, project_name));
+        let start = StartPhase::new(format!("./{ARTIFACT_DIR}/{project_name}"));
 
         let mut plan = BuildPlan::new(&vec![setup, install, build], Some(start));
         plan.add_variables(EnvironmentVariables::from([
diff --git a/src/providers/go.rs b/src/providers/go.rs
index 25c80a9..41516af 100644
--- a/src/providers/go.rs
+++ b/src/providers/go.rs
@@ -41,9 +41,9 @@ impl Provider for GolangProvider {
         }
 
         let mut build = if app.includes_file("go.mod") {
-            Phase::build(Some(format!("go build -o {}", BINARY_NAME)))
+            Phase::build(Some(format!("go build -o {BINARY_NAME}")))
         } else if app.includes_file("main.go") {
-            Phase::build(Some(format!("go build -o {} main.go", BINARY_NAME)))
+            Phase::build(Some(format!("go build -o {BINARY_NAME} main.go")))
         } else {
             Phase::build(None)
         };
@@ -54,7 +54,7 @@ impl Provider for GolangProvider {
         let has_go_files = app.has_match("**/*.go");
 
         if has_go_files {
-            let mut start = StartPhase::new(format!("./{}", BINARY_NAME));
+            let mut start = StartPhase::new(format!("./{BINARY_NAME}"));
             let cgo = env.get_variable("CGO_ENABLED").unwrap_or("0");
 
             // Only run in a new image if CGO_ENABLED=0 (default)
diff --git a/src/providers/haskell.rs b/src/providers/haskell.rs
index a5ba893..36a4ed0 100644
--- a/src/providers/haskell.rs
+++ b/src/providers/haskell.rs
@@ -65,7 +65,7 @@ impl Provider for HaskellStackProvider {
             .get(0)
             .ok_or_else(|| anyhow::anyhow!("Failed to get executable name"))?;
 
-        let start = StartPhase::new(format!("/root/.local/bin/{}", name));
+        let start = StartPhase::new(format!("/root/.local/bin/{name}"));
 
         let plan = BuildPlan::new(&vec![setup, install, build], Some(start));
 
diff --git a/src/providers/java.rs b/src/providers/java.rs
index 842ec03..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);
@@ -44,21 +46,28 @@ impl Provider for JavaProvider {
                 build.add_cmd("chmod +x gradlew");
             }
 
-            build.add_cmd(format!("{} build -x check", gradle_exe));
+            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()
+        );
     }
 }
diff --git a/src/providers/mod.rs b/src/providers/mod.rs
index c289e4a..629eba7 100644
--- a/src/providers/mod.rs
+++ b/src/providers/mod.rs
@@ -22,7 +22,7 @@ pub mod staticfile;
 pub mod swift;
 pub mod zig;
 
-pub trait Provider {
+pub trait Provider: Send + Sync {
     fn name(&self) -> &str;
     fn detect(&self, _app: &App, _env: &Environment) -> Result<bool> {
         Ok(false)
diff --git a/src/providers/node/mod.rs b/src/providers/node/mod.rs
index bbe7f9e..c99fbd3 100644
--- a/src/providers/node/mod.rs
+++ b/src/providers/node/mod.rs
@@ -96,7 +96,7 @@ impl Provider for NodeProvider {
         }
 
         // Install
-        let corepack = NodeProvider::uses_corepack(app)?;
+        let corepack = NodeProvider::uses_corepack(app, env)?;
         let mut install = Phase::install(if corepack {
             Some("npm install -g corepack && corepack enable".to_string())
         } else {
@@ -130,7 +130,7 @@ impl Provider for NodeProvider {
             build.add_cache_directory(if dir.is_empty() {
                 next_cache_dir.to_string()
             } else {
-                format!("{}/{}", dir, next_cache_dir)
+                format!("{dir}/{next_cache_dir}")
             });
         }
 
@@ -167,12 +167,16 @@ impl NodeProvider {
         Ok(false)
     }
 
-    pub fn uses_corepack(app: &App) -> Result<bool> {
+    pub fn uses_corepack(app: &App, env: &Environment) -> Result<bool> {
         let package_json: PackageJson = app.read_json("package.json").unwrap_or_default();
+        let node_pkg = NodeProvider::get_nix_node_pkg(&package_json, app, env)?;
 
-        if let Some(package_manager) = package_json.package_manager {
-            // The field is not null.
+        // Corepack is not supported for Node 14
+        if node_pkg.name.contains("14") {
+            return Ok(false);
+        }
 
+        if let Some(package_manager) = package_json.package_manager {
             if package_manager.starts_with("npm") {
                 // Fall back to just using the system npm version.
                 return Ok(false);
@@ -199,7 +203,7 @@ impl NodeProvider {
 
         if NodeProvider::has_script(app, "build")? {
             let pkg_manager = NodeProvider::get_package_manager(app);
-            Ok(Some(format!("{} run build", pkg_manager)))
+            Ok(Some(format!("{pkg_manager} run build")))
         } else {
             Ok(None)
         }
@@ -224,17 +228,17 @@ impl NodeProvider {
 
         let package_manager = NodeProvider::get_package_manager(app);
         if NodeProvider::has_script(app, "start")? {
-            return Ok(Some(format!("{} run start", package_manager)));
+            return Ok(Some(format!("{package_manager} run start")));
         }
 
         if let Some(main) = package_json.main {
             if app.includes_file(&main) {
-                return Ok(Some(format!("{} {}", executor, main)));
+                return Ok(Some(format!("{executor} {main}")));
             }
         }
 
         if app.includes_file("index.js") {
-            return Ok(Some(format!("{} index.js", executor)));
+            return Ok(Some(format!("{executor} index.js")));
         } else if app.includes_file("index.ts") && package_manager == "bun" {
             return Ok(Some("bun index.ts".to_string()));
         }
@@ -490,7 +494,7 @@ impl NodeProvider {
 
 fn version_number_to_pkg(version: u32) -> String {
     if AVAILABLE_NODE_VERSIONS.contains(&version) {
-        format!("nodejs-{}_x", version)
+        format!("nodejs-{version}_x")
     } else {
         DEFAULT_NODE_PKG_NAME.to_string()
     }
diff --git a/src/providers/node/nx.rs b/src/providers/node/nx.rs
index 998a861..33125d0 100644
--- a/src/providers/node/nx.rs
+++ b/src/providers/node/nx.rs
@@ -72,7 +72,7 @@ impl Nx {
     }
 
     pub fn get_nx_project_json_for_app(app: &App, nx_app_name: &String) -> Result<ProjectJson> {
-        let project_path = format!("./apps/{}/project.json", nx_app_name);
+        let project_path = format!("./apps/{nx_app_name}/project.json");
         app.read_json::<ProjectJson>(&project_path)
     }
 
@@ -86,12 +86,12 @@ impl Nx {
             }
         }
 
-        Ok(format!("dist/apps/{}", nx_app_name))
+        Ok(format!("dist/apps/{nx_app_name}"))
     }
 
     pub fn get_nx_build_cmd(app: &App, env: &Environment) -> Option<String> {
         Nx::get_nx_app_name(app, env)
-            .map(|nx_app_name| format!("npx nx run {}:build:production", nx_app_name))
+            .map(|nx_app_name| format!("npx nx run {nx_app_name}:build:production"))
     }
 
     pub fn get_nx_start_cmd(app: &App, env: &Environment) -> Result<Option<String>> {
@@ -106,14 +106,14 @@ impl Nx {
             if let Some(start_target) = project_json.targets.start {
                 if let Some(configurations) = start_target.configurations {
                     if configurations.production.is_some() {
-                        return Ok(Some(format!("npx nx run {}:start:production", nx_app_name)));
+                        return Ok(Some(format!("npx nx run {nx_app_name}:start:production")));
                     }
                 }
-                return Ok(Some(format!("npx nx run {}:start", nx_app_name)));
+                return Ok(Some(format!("npx nx run {nx_app_name}:start")));
             }
 
             if project_json.targets.build.executor == "@nrwl/next:build" {
-                return Ok(Some(format!("cd {} && npm run start", output_path)));
+                return Ok(Some(format!("cd {output_path} && npm run start")));
             }
 
             if let Some(options) = project_json.targets.build.options {
@@ -121,10 +121,10 @@ impl Nx {
                     let current_path = PathBuf::from(main_path);
                     let file_name = current_path.file_stem().unwrap().to_str().unwrap();
 
-                    return Ok(Some(format!("node {}/{}.js", output_path, file_name)));
+                    return Ok(Some(format!("node {output_path}/{file_name}.js")));
                 }
             }
-            return Ok(Some(format!("node {}/index.js", output_path)));
+            return Ok(Some(format!("node {output_path}/index.js")));
         }
 
         Ok(None)
diff --git a/src/providers/node/turborepo.rs b/src/providers/node/turborepo.rs
index e422a9b..2e22ed8 100644
--- a/src/providers/node/turborepo.rs
+++ b/src/providers/node/turborepo.rs
@@ -40,7 +40,7 @@ impl Turborepo {
 
     fn get_pipeline_cmd(cfg: &TurboJson, name: &str) -> Option<String> {
         if cfg.pipeline.contains_key(name) {
-            Some(format!("npx turbo run {}", name))
+            Some(format!("npx turbo run {name}"))
         } else {
             None
         }
@@ -59,7 +59,7 @@ impl Turborepo {
         if let Some(build_cmd) = Turborepo::get_build_cmd(&turbo_cfg) {
             return Ok(Some(build_cmd));
         } else if let Some(app_name) = Turborepo::get_app_name(env) {
-            return Ok(Some(format!("{} turbo run {}:build", dlx, app_name)));
+            return Ok(Some(format!("{dlx} turbo run {app_name}:build")));
         };
         Ok(None)
     }
@@ -94,7 +94,7 @@ impl Turborepo {
                     pkg_manager, name
                 )));
             }
-            println!("Warning: Turborepo app `{}` not found", name);
+            println!("Warning: Turborepo app `{name}` not found");
         }
         if let Some(start_pipeline) = Turborepo::get_start_cmd(&turbo_cfg) {
             return Ok(Some(start_pipeline));
diff --git a/src/providers/php/mod.rs b/src/providers/php/mod.rs
index 4399e78..64e696c 100644
--- a/src/providers/php/mod.rs
+++ b/src/providers/php/mod.rs
@@ -15,7 +15,7 @@ use crate::nixpacks::{
 use super::{node::NodeProvider, Provider};
 use anyhow::Result;
 
-const DEFAULT_PHP_VERSION: &str = "8.1";
+const DEFAULT_PHP_VERSION: &str = "8.2";
 
 pub struct PhpProvider;
 
@@ -63,7 +63,7 @@ impl PhpProvider {
         ];
         if let Ok(php_extensions) = PhpProvider::get_php_extensions(app) {
             for extension in php_extensions {
-                pkgs.push(Pkg::new(&format!("{}Extensions.{}", &php_pkg, extension)));
+                pkgs.push(Pkg::new(&format!("{}Extensions.{extension}", &php_pkg)));
             }
         }
 
@@ -144,6 +144,8 @@ impl PhpProvider {
                 "8.0".to_string()
             } else if v.contains("8.1") {
                 "8.1".to_string()
+            } else if v.contains("8.2") {
+                "8.2".to_string()
             } else if v.contains("7.4") {
                 "7.4".to_string()
             } else {
@@ -154,7 +156,7 @@ impl PhpProvider {
                 DEFAULT_PHP_VERSION.to_string()
             }
         } else {
-            println!("Warning: No PHP version specified, using PHP {}; see https://getcomposer.org/doc/04-schema.md#package-links for how to specify a PHP version.", DEFAULT_PHP_VERSION);
+            println!("Warning: No PHP version specified, using PHP {DEFAULT_PHP_VERSION}; see https://getcomposer.org/doc/04-schema.md#package-links for how to specify a PHP version.");
             DEFAULT_PHP_VERSION.to_string()
         };
 
diff --git a/src/providers/python.rs b/src/providers/python.rs
index bcde627..86b8a9f 100644
--- a/src/providers/python.rs
+++ b/src/providers/python.rs
@@ -139,8 +139,8 @@ impl PythonProvider {
 
     fn install(&self, app: &App, _env: &Environment) -> Result<Option<Phase>> {
         let env_loc = "/opt/venv";
-        let create_env = format!("python -m venv {}", env_loc);
-        let activate_env = format!(". {}/bin/activate", env_loc);
+        let create_env = format!("python -m venv {env_loc}");
+        let activate_env = format!(". {env_loc}/bin/activate");
 
         if app.includes_file("requirements.txt") {
             let mut install_phase = Phase::install(Some(format!(
@@ -148,7 +148,7 @@ impl PythonProvider {
                 create_env, activate_env
             )));
 
-            install_phase.add_path(format!("{}/bin", env_loc));
+            install_phase.add_path(format!("{env_loc}/bin"));
             install_phase.add_cache_directory(PIP_CACHE_DIR.to_string());
 
             return Ok(Some(install_phase));
@@ -160,7 +160,7 @@ impl PythonProvider {
                     create_env, activate_env, install_poetry
                 )));
 
-                install_phase.add_path(format!("{}/bin", env_loc));
+                install_phase.add_path(format!("{env_loc}/bin"));
 
                 install_phase.add_cache_directory(PIP_CACHE_DIR.to_string());
 
@@ -172,7 +172,7 @@ impl PythonProvider {
             )));
 
             install_phase.add_file_dependency("pyproject.toml".to_string());
-            install_phase.add_path(format!("{}/bin", env_loc));
+            install_phase.add_path(format!("{env_loc}/bin"));
 
             install_phase.add_cache_directory(PIP_CACHE_DIR.to_string());
 
@@ -187,10 +187,10 @@ impl PythonProvider {
                 "PIPENV_VENV_IN_PROJECT=1 pipenv install --skip-lock"
             };
 
-            let cmd = format!("{} && {} && {}", create_env, activate_env, cmd);
+            let cmd = format!("{create_env} && {activate_env} && {cmd}");
             let mut install_phase = Phase::install(Some(cmd));
 
-            install_phase.add_path(format!("{}/bin", env_loc));
+            install_phase.add_path(format!("{env_loc}/bin"));
             install_phase.add_cache_directory(PIP_CACHE_DIR.to_string());
 
             return Ok(Some(install_phase));
@@ -214,7 +214,7 @@ impl PythonProvider {
                 if let Some(entry_point) = meta.entry_point {
                     return Ok(Some(StartPhase::new(match entry_point {
                         EntryPoint::Command(cmd) => cmd,
-                        EntryPoint::Module(module) => format!("python -m {}", module),
+                        EntryPoint::Module(module) => format!("python -m {module}"),
                     })));
                 }
             }
diff --git a/src/providers/ruby.rs b/src/providers/ruby.rs
index 08f44bd..4e9fac1 100644
--- a/src/providers/ruby.rs
+++ b/src/providers/ruby.rs
@@ -2,6 +2,7 @@ use super::{node::NodeProvider, Provider};
 use crate::nixpacks::{
     app::App,
     environment::{Environment, EnvironmentVariables},
+    images::DEBIAN_BASE_IMAGE,
     nix::pkg::Pkg,
     plan::{
         phase::{Phase, StartPhase},
@@ -10,6 +11,7 @@ use crate::nixpacks::{
 };
 use anyhow::{bail, Ok, Result};
 use regex::Regex;
+use semver::Version;
 
 pub struct RubyProvider {}
 
@@ -51,6 +53,11 @@ impl Provider for RubyProvider {
 
         plan.add_variables(self.get_environment_variables(app)?);
 
+        // Temporary fix to allow using older versions of ruby
+        if self.requires_openssl_1(app)? {
+            plan.build_image = Some(DEBIAN_BASE_IMAGE.to_string());
+        }
+
         Ok(Some(plan))
     }
 }
@@ -80,6 +87,8 @@ impl RubyProvider {
             setup.add_apt_pkgs(vec![String::from("libicu-dev")]);
         }
 
+        let ruby_version = self.get_ruby_version(app)?;
+
         setup.add_cmd(format!(
             "curl -sSL https://get.rvm.io | bash -s stable \
             && . /etc/profile.d/rvm.sh \
@@ -87,7 +96,7 @@ impl RubyProvider {
             && rvm --default use {ruby_version} \
             && gem install {bundler_version} \
             && rm -rf /usr/local/rvm/src",
-            ruby_version = self.get_ruby_version(app)?,
+            ruby_version = ruby_version,
             bundler_version = self.get_bundler_version(app)
         ));
 
@@ -110,9 +119,9 @@ impl RubyProvider {
 
         // Ensure that the ruby executable is in the PATH
         let ruby_version = self.get_ruby_version(app)?;
-        install.add_path(format!("/usr/local/rvm/rubies/{}/bin", ruby_version));
-        install.add_path(format!("/usr/local/rvm/gems/{}/bin", ruby_version));
-        install.add_path(format!("/usr/local/rvm/gems/{}@global/bin", ruby_version));
+        install.add_path(format!("/usr/local/rvm/rubies/{ruby_version}/bin"));
+        install.add_path(format!("/usr/local/rvm/gems/{ruby_version}/bin"));
+        install.add_path(format!("/usr/local/rvm/gems/{ruby_version}@global/bin"));
 
         Ok(Some(install))
     }
@@ -147,7 +156,7 @@ impl RubyProvider {
             ),
             (
                 "GEM_HOME".to_string(),
-                format!("/usr/local/rvm/gems/{}", ruby_version),
+                format!("/usr/local/rvm/gems/{ruby_version}"),
             ),
             ("MALLOC_ARENA_MAX".to_string(), "2".to_string()),
         ]);
@@ -215,6 +224,21 @@ impl RubyProvider {
         }
     }
 
+    fn requires_openssl_1(&self, app: &App) -> Result<bool> {
+        let ruby_version = self.get_ruby_version(app)?;
+        match Version::parse(ruby_version.trim_start_matches("ruby-")) {
+            std::result::Result::Ok(v) => {
+                // Version 3.1.0 and above work with openssl 3.0
+                if v.major >= 3 && v.minor >= 1 {
+                    Ok(false)
+                } else {
+                    Ok(true)
+                }
+            }
+            std::result::Result::Err(_) => Ok(false),
+        }
+    }
+
     fn is_rails_app(&self, app: &App) -> bool {
         app.includes_file("config/application.rb")
             && app
diff --git a/src/providers/rust.rs b/src/providers/rust.rs
index 747e484..37a6563 100644
--- a/src/providers/rust.rs
+++ b/src/providers/rust.rs
@@ -52,7 +52,7 @@ impl RustProvider {
         let mut rust_pkg: Pkg = RustProvider::get_rust_pkg(app, env)?;
 
         if let Some(target) = RustProvider::get_target(app, env)? {
-            rust_pkg = rust_pkg.set_override("targets", &format!("[\"{}\"]", target));
+            rust_pkg = rust_pkg.set_override("targets", &format!("[\"{target}\"]"));
         }
 
         let mut setup = Phase::setup(Some(vec![
@@ -91,7 +91,7 @@ impl RustProvider {
 
         if let Some(target) = RustProvider::get_target(app, env)? {
             if let Some(workspace) = RustProvider::resolve_cargo_workspace(app, env)? {
-                write!(build_cmd, " --package {} --target {}", workspace, target)?;
+                write!(build_cmd, " --package {workspace} --target {target}")?;
 
                 build.add_cmd(build_cmd);
                 build.add_cmd(format!(
@@ -100,7 +100,7 @@ impl RustProvider {
                     name = workspace
                 ));
             } else {
-                write!(build_cmd, " --target {}", target)?;
+                write!(build_cmd, " --target {target}")?;
 
                 if let Some(name) = RustProvider::get_app_name(app)? {
                     build.add_cmd(build_cmd);
@@ -112,12 +112,12 @@ impl RustProvider {
                 }
             }
         } else if let Some(workspace) = RustProvider::resolve_cargo_workspace(app, env)? {
-            write!(build_cmd, " --package {}", workspace)?;
+            write!(build_cmd, " --package {workspace}")?;
             build.add_cmd(build_cmd);
-            build.add_cmd(format!("cp target/release/{name} bin", name = workspace));
+            build.add_cmd(format!("cp target/release/{workspace} bin"));
         } else if let Some(name) = RustProvider::get_app_name(app)? {
             build.add_cmd(build_cmd);
-            build.add_cmd(format!("cp target/release/{name} bin", name = name));
+            build.add_cmd(format!("cp target/release/{name} bin"));
         }
 
         build.add_cache_directory(CARGO_GIT_CACHE_DIR.to_string());
@@ -134,24 +134,24 @@ impl RustProvider {
     fn get_start(app: &App, env: &Environment) -> Result<Option<StartPhase>> {
         if (RustProvider::get_target(app, env)?).is_some() {
             if let Some(workspace) = RustProvider::resolve_cargo_workspace(app, env)? {
-                let mut start = StartPhase::new(format!("./bin/{}", workspace));
+                let mut start = StartPhase::new(format!("./bin/{workspace}"));
                 start.run_in_slim_image();
-                start.add_file_dependency(format!("./bin/{}", workspace));
+                start.add_file_dependency(format!("./bin/{workspace}"));
 
                 Ok(Some(start))
             } else if let Some(name) = RustProvider::get_app_name(app)? {
-                let mut start = StartPhase::new(format!("./bin/{}", name));
+                let mut start = StartPhase::new(format!("./bin/{name}"));
                 start.run_in_slim_image();
-                start.add_file_dependency(format!("./bin/{}", name));
+                start.add_file_dependency(format!("./bin/{name}"));
 
                 Ok(Some(start))
             } else {
                 Ok(None)
             }
         } else if let Some(workspace) = RustProvider::resolve_cargo_workspace(app, env)? {
-            Ok(Some(StartPhase::new(format!("./bin/{}", workspace))))
+            Ok(Some(StartPhase::new(format!("./bin/{workspace}"))))
         } else if let Some(name) = RustProvider::get_app_name(app)? {
-            Ok(Some(StartPhase::new(format!("./bin/{}", name))))
+            Ok(Some(StartPhase::new(format!("./bin/{name}"))))
         } else {
             Ok(None)
         }
@@ -170,7 +170,7 @@ impl RustProvider {
 
     fn get_target(app: &App, env: &Environment) -> Result<Option<String>> {
         if RustProvider::should_use_musl(app, env)? {
-            Ok(Some(format!("{}-unknown-linux-musl", ARCH)))
+            Ok(Some(format!("{ARCH}-unknown-linux-musl")))
         } else {
             Ok(None)
         }
@@ -207,7 +207,7 @@ impl RustProvider {
 
         if let Some(toolchain_file) = RustProvider::get_rust_toolchain_file(app) {
             return Ok(Pkg::new(&format!(
-                "(rust-bin.fromRustupToolchainFile ./{})",
+                "(rust-bin.fromRustupToolchainFile ../{})",
                 toolchain_file
             )));
         }
@@ -219,7 +219,7 @@ impl RustProvider {
                     package.rust_version.map_or_else(
                         || Pkg::new(DEFAULT_RUST_PACKAGE),
                         |version| {
-                            Pkg::new(format!("rust-bin.stable.\"{}\".default", version).as_str())
+                            Pkg::new(format!("rust-bin.stable.\"{version}\".default").as_str())
                         },
                     )
                 },
@@ -284,9 +284,9 @@ impl RustProvider {
 
     fn find_binary_in_workspace(app: &App, workspace: &Workspace) -> Result<Option<String>> {
         let find_binary = |member: &str| -> Result<Option<String>> {
-            let mut manifest = app.read_toml::<Manifest>(&format!("{}/Cargo.toml", member))?;
+            let mut manifest = app.read_toml::<Manifest>(&format!("{member}/Cargo.toml"))?;
 
-            manifest.complete_from_path(&app.source.join(format!("{}/Cargo.toml", member)))?;
+            manifest.complete_from_path(&app.source.join(format!("{member}/Cargo.toml")))?;
 
             if let Some(package) = manifest.package {
                 if !manifest.bin.is_empty() || manifest.lib.is_none() {
@@ -373,7 +373,7 @@ mod test {
                 &App::new("./examples/rust-custom-toolchain")?,
                 &Environment::default()
             )?,
-            Pkg::new("(rust-bin.fromRustupToolchainFile ./rust-toolchain.toml)")
+            Pkg::new("(rust-bin.fromRustupToolchainFile ../rust-toolchain.toml)")
         );
 
         Ok(())
diff --git a/src/providers/staticfile.rs b/src/providers/staticfile.rs
index 58def64..f4064ce 100644
--- a/src/providers/staticfile.rs
+++ b/src/providers/staticfile.rs
@@ -101,7 +101,7 @@ impl StaticfileProvider {
         let status_code = staticfile.status_code.unwrap_or_default();
         let mut error_page = String::new();
         for (key, value) in status_code {
-            writeln!(error_page, "\terror_page {} {};", key, value)?;
+            writeln!(error_page, "\terror_page {key} {value};")?;
         }
 
         let nginx_conf = formatdoc! {"
diff --git a/src/providers/swift.rs b/src/providers/swift.rs
index d427a70..8fc5015 100644
--- a/src/providers/swift.rs
+++ b/src/providers/swift.rs
@@ -84,7 +84,7 @@ impl Provider for SwiftProvider {
         ));
 
         let name = SwiftProvider::get_executable_name(app)?;
-        let start = StartPhase::new(format!("./{}", name));
+        let start = StartPhase::new(format!("./{name}"));
 
         let plan = BuildPlan::new(&vec![setup, install, build], Some(start));
 
diff --git a/src/providers/zig.rs b/src/providers/zig.rs
index 717f6a0..053546d 100644
--- a/src/providers/zig.rs
+++ b/src/providers/zig.rs
@@ -37,13 +37,13 @@ impl Provider for ZigProvider {
             install.add_cmd("git submodule update --init".to_string());
         }
         if app.includes_file("gyro.zzz") {
-            let gyro_exe_path = format!("/gyro/gyro-{}-linux-{}/bin/gyro", GYRO_VERSION, ARCH);
+            let gyro_exe_path = format!("/gyro/gyro-{GYRO_VERSION}-linux-{ARCH}/bin/gyro");
             install.add_cmd(format!(
                 "mkdir /gyro && (wget -O- {} | tar -C /gyro -xzf -)",
                 ZigProvider::get_gyro_download_url()
             ));
-            install.add_cmd(format!("chmod +x {}", gyro_exe_path));
-            install.add_cmd(format!("{} fetch", gyro_exe_path));
+            install.add_cmd(format!("chmod +x {gyro_exe_path}"));
+            install.add_cmd(format!("{gyro_exe_path} fetch"));
         }
 
         let build = Phase::build(Some("zig build -Drelease-safe=true".to_string()));
@@ -70,7 +70,7 @@ impl ZigProvider {
                 GYRO_VERSION, GYRO_VERSION, ARCH
             )
         } else {
-            panic!("Gyro is not supported on your architecture ({}).", ARCH)
+            panic!("Gyro is not supported on your architecture ({ARCH}).")
         }
     }
 }


Test patch:
diff --git a/examples/go-cgo-enabled/test.env b/examples/go-cgo-enabled/test.env
deleted file mode 100644
index ed74cfa..0000000
--- a/examples/go-cgo-enabled/test.env
+++ /dev/null
@@ -1,3 +0,0 @@
-# Note: You do NOT need this for your project. This file is only used for testing purposes.
-
-ENVS="CGO_ENABLED=1"
diff --git a/tests/docker_run_tests.rs b/tests/docker_run_tests.rs
index 9b3b9bf..f5350eb 100644
--- a/tests/docker_run_tests.rs
+++ b/tests/docker_run_tests.rs
@@ -687,6 +687,13 @@ async fn test_rust_custom_version() {
     assert!(output.contains("cargo 1.56.0"));
 }
 
+#[tokio::test]
+async fn test_rust_toolchain_file() {
+    let name = simple_build("./examples/rust-custom-toolchain").await;
+    let output = run_image(&name, None).await;
+    assert!(output.contains("cargo 1.60.0-nightly"));
+}
+
 #[tokio::test]
 async fn test_rust_ring() {
     let name = simple_build("./examples/rust-ring").await;
diff --git a/tests/snapshots/generate_plan_tests__clojure.snap b/tests/snapshots/generate_plan_tests__clojure.snap
index 8ebe036..70e267c 100644
--- a/tests/snapshots/generate_plan_tests__clojure.snap
+++ b/tests/snapshots/generate_plan_tests__clojure.snap
@@ -16,7 +16,7 @@ expression: plan
         "setup"
       ],
       "cmds": [
-        "lein uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi"
+        "lein uberjar; if [ -f /app/target/default+uberjar/*standalone.jar ]; then mv /app/target/default+uberjar/*standalone.jar /app/target/*standalone.jar; fi"
       ]
     },
     "setup": {
diff --git a/tests/snapshots/generate_plan_tests__clojure_jdk11.snap b/tests/snapshots/generate_plan_tests__clojure_jdk11.snap
index 2860fe4..de854bb 100644
--- a/tests/snapshots/generate_plan_tests__clojure_jdk11.snap
+++ b/tests/snapshots/generate_plan_tests__clojure_jdk11.snap
@@ -16,7 +16,7 @@ expression: plan
         "setup"
       ],
       "cmds": [
-        "lein uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi"
+        "lein uberjar; if [ -f /app/target/default+uberjar/*standalone.jar ]; then mv /app/target/default+uberjar/*standalone.jar /app/target/*standalone.jar; fi"
       ]
     },
     "setup": {
diff --git a/tests/snapshots/generate_plan_tests__clojure_jdk_latest.snap b/tests/snapshots/generate_plan_tests__clojure_jdk_latest.snap
index a9f7c4e..3106e97 100644
--- a/tests/snapshots/generate_plan_tests__clojure_jdk_latest.snap
+++ b/tests/snapshots/generate_plan_tests__clojure_jdk_latest.snap
@@ -16,7 +16,7 @@ expression: plan
         "setup"
       ],
       "cmds": [
-        "lein uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi"
+        "lein uberjar; if [ -f /app/target/default+uberjar/*standalone.jar ]; then mv /app/target/default+uberjar/*standalone.jar /app/target/*standalone.jar; fi"
       ]
     },
     "setup": {
diff --git a/tests/snapshots/generate_plan_tests__clojure_ring_app.snap b/tests/snapshots/generate_plan_tests__clojure_ring_app.snap
index ee64ca8..801bc53 100644
--- a/tests/snapshots/generate_plan_tests__clojure_ring_app.snap
+++ b/tests/snapshots/generate_plan_tests__clojure_ring_app.snap
@@ -16,7 +16,7 @@ expression: plan
         "setup"
       ],
       "cmds": [
-        "lein ring uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi"
+        "lein ring uberjar; if [ -f /app/target/default+uberjar/*standalone.jar ]; then mv /app/target/default+uberjar/*standalone.jar /app/target/*standalone.jar; fi"
       ]
     },
     "setup": {
diff --git a/tests/snapshots/generate_plan_tests__go.snap b/tests/snapshots/generate_plan_tests__go.snap
index 62607d4..1067773 100644
--- a/tests/snapshots/generate_plan_tests__go.snap
+++ b/tests/snapshots/generate_plan_tests__go.snap
@@ -34,6 +34,6 @@ expression: plan
   },
   "start": {
     "cmd": "./out",
-    "runImage": "debian:bullseye-slim"
+    "runImage": "ubuntu:jammy"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__go_custom_version.snap b/tests/snapshots/generate_plan_tests__go_custom_version.snap
index 6e1e25c..ceb3020 100644
--- a/tests/snapshots/generate_plan_tests__go_custom_version.snap
+++ b/tests/snapshots/generate_plan_tests__go_custom_version.snap
@@ -46,6 +46,6 @@ expression: plan
   },
   "start": {
     "cmd": "./out",
-    "runImage": "debian:bullseye-slim"
+    "runImage": "ubuntu:jammy"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__go_gin.snap b/tests/snapshots/generate_plan_tests__go_gin.snap
index 6e1e25c..ceb3020 100644
--- a/tests/snapshots/generate_plan_tests__go_gin.snap
+++ b/tests/snapshots/generate_plan_tests__go_gin.snap
@@ -46,6 +46,6 @@ expression: plan
   },
   "start": {
     "cmd": "./out",
-    "runImage": "debian:bullseye-slim"
+    "runImage": "ubuntu:jammy"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__go_mod.snap b/tests/snapshots/generate_plan_tests__go_mod.snap
index b6d74f7..453f6bc 100644
--- a/tests/snapshots/generate_plan_tests__go_mod.snap
+++ b/tests/snapshots/generate_plan_tests__go_mod.snap
@@ -46,6 +46,6 @@ expression: plan
   },
   "start": {
     "cmd": "./out",
-    "runImage": "debian:bullseye-slim"
+    "runImage": "ubuntu:jammy"
   }
 }
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": [
diff --git a/tests/snapshots/generate_plan_tests__ruby_2.snap b/tests/snapshots/generate_plan_tests__ruby_2.snap
new file mode 100644
index 0000000..70c71c9
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__ruby_2.snap
@@ -0,0 +1,58 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "providers": [],
+  "buildImage": "[build_image]",
+  "variables": {
+    "BUNDLE_GEMFILE": "/app/Gemfile",
+    "GEM_HOME": "/usr/local/rvm/gems/2.6.6",
+    "GEM_PATH": "/usr/local/rvm/gems/2.6.6:/usr/local/rvm/gems/2.6.6@global",
+    "MALLOC_ARENA_MAX": "2",
+    "NIXPACKS_METADATA": "ruby"
+  },
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ]
+    },
+    "install": {
+      "name": "install",
+      "dependsOn": [
+        "setup"
+      ],
+      "cmds": [
+        "bundle install"
+      ],
+      "onlyIncludeFiles": [
+        "Gemfile",
+        "Gemfile.lock"
+      ],
+      "cacheDirectories": [
+        "/root/.bundle/cache"
+      ],
+      "paths": [
+        "/usr/local/rvm/rubies/2.6.6/bin",
+        "/usr/local/rvm/gems/2.6.6/bin",
+        "/usr/local/rvm/gems/2.6.6@global/bin"
+      ]
+    },
+    "setup": {
+      "name": "setup",
+      "aptPkgs": [
+        "procps"
+      ],
+      "cmds": [
+        "curl -sSL https://get.rvm.io | bash -s stable && . /etc/profile.d/rvm.sh && rvm install 2.6.6 && rvm --default use 2.6.6 && gem install bundler:2.3.7 && rm -rf /usr/local/rvm/src",
+        "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
+      ],
+      "onlyIncludeFiles": []
+    }
+  },
+  "start": {
+    "cmd": "ruby app.rb"
+  }
+}