Repository: railwayapp/nixpacks
Pull request #348 (2022-07-22 08:32:08)

Add caching and ring support to Clojure provider
Lein is the CLI tool to work with Clojure apps (Plus more abilities it can do)
Ring is the ExpressJS of Clojure, it allows apps to be served over HTTP
lein-rign is a plugin to enable better developer experience during building & releasing Ring apps 

This PR detects usage of `lein-rign` to be able to build & release it properly 

Also, caching support has been added to cache files between builds 


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 31ebb60..afe0842 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -125,9 +125,9 @@ dependencies = [
 
 [[package]]
 name = "darling"
-version = "0.14.1"
+version = "0.13.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02"
+checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
 dependencies = [
  "darling_core",
  "darling_macro",
@@ -135,9 +135,9 @@ dependencies = [
 
 [[package]]
 name = "darling_core"
-version = "0.14.1"
+version = "0.13.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f"
+checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610"
 dependencies = [
  "fnv",
  "ident_case",
@@ -149,9 +149,9 @@ dependencies = [
 
 [[package]]
 name = "darling_macro"
-version = "0.14.1"
+version = "0.13.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5"
+checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
 dependencies = [
  "darling_core",
  "quote",
@@ -433,9 +433,9 @@ dependencies = [
 
 [[package]]
 name = "serde_with"
-version = "2.0.0"
+version = "1.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89df7a26519371a3cce44fbb914c2819c84d9b897890987fa3ab096491cc0ea8"
+checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff"
 dependencies = [
  "serde",
  "serde_with_macros",
@@ -443,9 +443,9 @@ dependencies = [
 
 [[package]]
 name = "serde_with_macros"
-version = "2.0.0"
+version = "1.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de337f322382fcdfbb21a014f7c224ee041a23785651db67b9827403178f698f"
+checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082"
 dependencies = [
  "darling",
  "proc-macro2",
diff --git a/Cargo.toml b/Cargo.toml
index aff2f1b..48099d1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,7 +30,7 @@ regex = "1.6.0"
 serde = { version = "1.0.140", default-features = false }
 serde_json = "1.0.82"
 serde_yaml = "0.8.26"
-serde_with = { features = ["macros"], default-features = false, version = "2.0.0" }
+serde_with = { features = ["macros"], default-features = false, version = "1.14.0" }
 tempdir = "0.3.7"
 toml = "0.5.9"
 uuid = { version = "1.1.2", features = ["v4"], default-features = false }
diff --git a/docs/pages/docs/providers/clojure.md b/docs/pages/docs/providers/clojure.md
index 27414d7..dd03ab9 100644
--- a/docs/pages/docs/providers/clojure.md
+++ b/docs/pages/docs/providers/clojure.md
@@ -21,13 +21,18 @@ The version can be overriden by
 
 ## Build
 
+If `lein-ring` plugin detected
+
+```lein ring uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then mv /app/target/uberjar/standalone.jar /app/target/*standalone.jar; fi
 ```
-lein uberjar
+
+Default
+```lein uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then mv /app/target/uberjar/standalone.jar /app/target/*standalone.jar; fi
 ```
 
 
 ## Start
 
 ```
-java $JAVA_OPTS -jar target/uberjar/*standalone.jar
+java $JAVA_OPTS -jar /app/target/*standalone.jar
 ```
\ No newline at end of file
diff --git a/examples/clojure-ring-app/project.clj b/examples/clojure-ring-app/project.clj
new file mode 100644
index 0000000..f80373d
--- /dev/null
+++ b/examples/clojure-ring-app/project.clj
@@ -0,0 +1,12 @@
+(defproject hello-ring "0.1.0-SNAPSHOT"
+  :description "FIXME: write description"
+  :url "http://example.com/FIXME"
+  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
+            :url "https://www.eclipse.org/legal/epl-2.0/"}
+  :plugins [[lein-ring "0.12.5"]]
+  :ring {:handler hello-ring.core/handler}
+  :dependencies [[org.clojure/clojure "1.10.3"]
+                 [ring/ring-core "1.9.5"]
+                 [ring/ring-jetty-adapter "1.9.5"]]
+  :repl-options {:init-ns hello-ring.core}
+  :main hello-ring.core)
diff --git a/examples/clojure-ring-app/src/hello_ring/core.clj b/examples/clojure-ring-app/src/hello_ring/core.clj
new file mode 100644
index 0000000..a851716
--- /dev/null
+++ b/examples/clojure-ring-app/src/hello_ring/core.clj
@@ -0,0 +1,10 @@
+(ns hello-ring.core  
+  (:require [ring.adapter.jetty :as jetty]))
+
+(defn handler [request]
+  {:status 200
+   :headers {"Content-Type" "text/plain"}
+   :body "Hello Clojure, Hello Ring!"})
+
+(defn -main []
+  (jetty/run-jetty handler {:port 3000}))
\ No newline at end of file
diff --git a/src/nixpacks/builder/docker.rs b/src/nixpacks/builder/docker.rs
index 7319fc2..c6c1360 100644
--- a/src/nixpacks/builder/docker.rs
+++ b/src/nixpacks/builder/docker.rs
@@ -372,12 +372,14 @@ impl DockerBuilder {
 }
 
 fn get_cache_mount(cache_key: &Option<String>, cache_directories: &Option<Vec<String>>) -> String {
-    let sanitized_cache_key = cache_key.clone().map(sanitize_cache_key);
-
-    match (sanitized_cache_key, cache_directories) {
+    match (cache_key, cache_directories) {
         (Some(cache_key), Some(cache_directories)) => cache_directories
             .iter()
-            .map(|dir| format!("--mount=type=cache,id={cache_key}-{dir},target={dir}"))
+            .map(|dir| {
+                let sanitized_dir = dir.replace('~', "/root");
+                let sanitized_key = sanitize_cache_key(format!("{cache_key}-{sanitized_dir}"));
+                format!("--mount=type=cache,id={sanitized_key},target={sanitized_dir}")
+            })
             .collect::<Vec<String>>()
             .join(" "),
         _ => "".to_string(),
diff --git a/src/nixpacks/cache.rs b/src/nixpacks/cache.rs
index f9f8a4f..4eeba0e 100644
--- a/src/nixpacks/cache.rs
+++ b/src/nixpacks/cache.rs
@@ -1,5 +1,12 @@
 pub fn sanitize_cache_key(cache_key: String) -> String {
-    cache_key.replace(' ', "-")
+    cache_key
+        .chars()
+        .filter(|x| !matches!(x, '.')) // remove dot from the string
+        .map(|x| match x {
+            ' ' | '/' => '-',
+            _ => x,
+        })
+        .collect()
 }
 
 #[cfg(test)]
@@ -13,5 +20,6 @@ mod tests {
             sanitize_cache_key("s p a c e s".to_string()),
             "s-p-a-c-e-s".to_string()
         );
+        assert_eq!(sanitize_cache_key("/.m2".to_string()), "-m2".to_string());
     }
 }
diff --git a/src/providers/clojure.rs b/src/providers/clojure.rs
index e4024ca..0154315 100644
--- a/src/providers/clojure.rs
+++ b/src/providers/clojure.rs
@@ -3,12 +3,14 @@ use crate::nixpacks::{
     app::App,
     environment::Environment,
     nix::pkg::Pkg,
-    phase::{BuildPhase, SetupPhase, StartPhase},
+    phase::{BuildPhase, InstallPhase, SetupPhase, StartPhase},
 };
 use anyhow::Result;
 use regex::Regex;
 
 const DEFAULT_JDK_PKG_NAME: &'static &str = &"jdk8";
+const LEIN_CACHE_DIR: &'static &str = &"~/.m2";
+
 pub struct ClojureProvider {}
 
 impl Provider for ClojureProvider {
@@ -20,21 +22,44 @@ impl Provider for ClojureProvider {
         Ok(app.includes_file("project.clj"))
     }
 
-    fn setup(&self, _app: &App, _env: &Environment) -> Result<Option<SetupPhase>> {
+    fn install(&self, _app: &App, _env: &Environment) -> Result<Option<InstallPhase>> {
+        let mut install_phase = InstallPhase::new("".to_string());
+        install_phase.add_cache_directory(LEIN_CACHE_DIR.to_string());
+        Ok(Some(install_phase))
+    }
+
+    fn setup(&self, app: &App, env: &Environment) -> Result<Option<SetupPhase>> {
         Ok(Some(SetupPhase::new(vec![
             Pkg::new("leiningen"),
-            Pkg::new("jdk8"),
+            ClojureProvider::get_nix_jdk_package(app, env)?,
         ])))
     }
 
-    fn build(&self, _app: &App, _env: &Environment) -> Result<Option<BuildPhase>> {
-        Ok(Some(BuildPhase::new("lein uberjar".to_string())))
+    fn build(&self, app: &App, _env: &Environment) -> Result<Option<BuildPhase>> {
+        let has_lein_ring_plugin = app
+            .read_file("project.clj")?
+            .to_lowercase()
+            .contains("[lein-ring ");
+
+        let build_cmd = match has_lein_ring_plugin {
+            true => "lein ring uberjar",
+            false => "lein uberjar",
+        };
+
+        // 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";
+
+        Ok(Some(BuildPhase::new(format!(
+            "{}; {}",
+            build_cmd.to_string(),
+            move_file_cmd
+        ))))
     }
 
     fn start(&self, _app: &App, _env: &Environment) -> Result<Option<StartPhase>> {
-        Ok(Some(StartPhase::new(
-            "java $JAVA_OPTS -jar target/uberjar/*standalone.jar".to_string(),
-        )))
+        let start_cmd = "java $JAVA_OPTS -jar /app/target/*standalone.jar";
+
+        Ok(Some(StartPhase::new(start_cmd.to_string())))
     }
 }
 


Test patch:
diff --git a/tests/docker_run_tests.rs b/tests/docker_run_tests.rs
index a75dee8..22ce241 100644
--- a/tests/docker_run_tests.rs
+++ b/tests/docker_run_tests.rs
@@ -557,3 +557,10 @@ fn test_clojure() {
     let output = run_image(name, None);
     assert_eq!(output, "Hello, World From Clojure!");
 }
+
+#[test]
+fn test_clojure_ring_app() {
+    let name = simple_build("./examples/clojure-ring-app");
+    let output = run_image(name, None);
+    assert_eq!(output, "Started server on port 3000");
+}
diff --git a/tests/generate_plan_tests.rs b/tests/generate_plan_tests.rs
index a90dbbe..d0d09a6 100644
--- a/tests/generate_plan_tests.rs
+++ b/tests/generate_plan_tests.rs
@@ -943,17 +943,47 @@ fn test_ruby_sinatra() -> Result<()> {
 #[test]
 fn test_clojure() -> Result<()> {
     let plan = simple_gen_plan("./examples/clojure");
+    let move_file_cmd = "if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi";
+
+    assert_eq!(
+        plan.setup.unwrap().pkgs,
+        vec![Pkg::new("leiningen"), Pkg::new("jdk8"),]
+    );
+    assert_eq!(
+        plan.build.unwrap().cmds,
+        Some(vec![format!(
+            "{}; {}",
+            "lein uberjar".to_string(),
+            move_file_cmd
+        )])
+    );
+    assert_eq!(
+        plan.start.unwrap().cmd,
+        Some("java $JAVA_OPTS -jar /app/target/*standalone.jar".to_string())
+    );
+    Ok(())
+}
+
+#[test]
+fn test_clojure_ring_app() -> Result<()> {
+    let plan = simple_gen_plan("./examples/clojure-ring-app");
+    let move_file_cmd = "if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi";
+
     assert_eq!(
         plan.setup.unwrap().pkgs,
         vec![Pkg::new("leiningen"), Pkg::new("jdk8"),]
     );
     assert_eq!(
         plan.build.unwrap().cmds,
-        Some(vec!["lein uberjar".to_string()])
+        Some(vec![format!(
+            "{}; {}",
+            "lein ring uberjar".to_string(),
+            move_file_cmd
+        )])
     );
     assert_eq!(
         plan.start.unwrap().cmd,
-        Some("java $JAVA_OPTS -jar target/uberjar/*standalone.jar".to_string())
+        Some("java $JAVA_OPTS -jar /app/target/*standalone.jar".to_string())
     );
     Ok(())
 }