Repository: railwayapp/nixpacks
Pull request #628 (2022-10-20 16:32:42)

Include no files in ruby setup phase
This PR includes no app source files in the setup phase when Ruby is being installed. This should allow us to take advantage of the Docker layer cache and not re-install Ruby everytime the source code changes.

Fixes https://github.com/railwayapp/nixpacks/issues/624


Patch:
diff --git a/src/nixpacks/plan/mod.rs b/src/nixpacks/plan/mod.rs
index e5076bc..8591759 100644
--- a/src/nixpacks/plan/mod.rs
+++ b/src/nixpacks/plan/mod.rs
@@ -190,19 +190,13 @@ impl BuildPlan {
         let mut uses_setup = false;
 
         if let Some(pkg_string) = env.get_config_variable("PKGS") {
-            let mut pkgs = pkg_string
-                .split(' ')
-                .map(std::string::ToString::to_string)
-                .collect::<Vec<_>>();
+            let mut pkgs = split_env_string(pkg_string.as_str());
             pkgs.push("...".to_string());
             setup.nix_pkgs = Some(pkgs);
             uses_setup = true;
         }
         if let Some(apt_string) = env.get_config_variable("APT_PKGS") {
-            let mut apts = apt_string
-                .split(' ')
-                .map(std::string::ToString::to_string)
-                .collect::<Vec<_>>();
+            let mut apts = split_env_string(apt_string.as_str());
             apts.push("...".to_string());
             setup.apt_pkgs = Some(apts);
             uses_setup = true;
@@ -223,13 +217,27 @@ impl BuildPlan {
 
         // Install
         if let Some(cmd_string) = env.get_config_variable("INSTALL_CMD") {
-            let install = Phase::install(Some(cmd_string));
+            let mut install = Phase::install(Some(cmd_string));
+
+            if let Some(cache_dirs) = env.get_config_variable("INSTALL_CACHE_DIRS") {
+                split_env_string(cache_dirs.as_str())
+                    .iter()
+                    .for_each(|dir| install.add_cache_directory(dir));
+            }
+
             phases.push(install);
         }
 
         // Build
         if let Some(cmd_string) = env.get_config_variable("BUILD_CMD") {
-            let build = Phase::build(Some(cmd_string));
+            let mut build = Phase::build(Some(cmd_string));
+
+            if let Some(cache_dirs) = env.get_config_variable("BUILD_CACHE_DIRS") {
+                split_env_string(cache_dirs.as_str())
+                    .iter()
+                    .for_each(|dir| build.add_cache_directory(dir));
+            }
+
             phases.push(build);
         }
 
@@ -292,10 +300,58 @@ impl topological_sort::TopItem for (String, Phase) {
     }
 }
 
+fn split_env_string(s: &str) -> Vec<String> {
+    s.split([' ', ','])
+        .map(std::string::ToString::to_string)
+        .filter(|s| !s.is_empty())
+        .collect::<Vec<_>>()
+}
+
 #[cfg(test)]
 mod test {
     use super::*;
 
+    #[test]
+    fn get_plan_from_environment() {
+        let env = Environment::from_envs(vec![
+            "NIXPACKS_PKGS=cowsay sl",
+            "NIXPACKS_APT_PKGS=foo,bar",
+            "NIXPACKS_LIBS=my-lib",
+            "NIXPACKS_INSTALL_CMD=yarn install",
+            "NIXPACKS_INSTALL_CACHE_DIRS=install/cache/dir",
+            "NIXPACKS_BUILD_CMD=yarn build",
+            "NIXPACKS_BUILD_CACHE_DIRS=build/cache/dir",
+            "NIXPACKS_START_CMD=yarn start",
+        ])
+        .unwrap();
+        let env_plan = BuildPlan::from_environment(&env);
+
+        let result = BuildPlan::from_toml(
+            r#"
+            [phases.setup]
+            nixPkgs = ["cowsay", "sl", "..."]
+            aptPkgs = ["foo", "bar", "..."]
+            nixLibs = ["my-lib", "..."]
+
+            [phases.install]
+            cmds = ["yarn install"]
+            cacheDirectories = ["install/cache/dir"]
+            dependsOn = ["setup"]
+
+            [phases.build]
+            cmds = ["yarn build"]
+            cacheDirectories = ["build/cache/dir"]
+            dependsOn = ["install"]
+
+            [start]
+            cmd = "yarn start"
+            "#,
+        )
+        .unwrap();
+
+        assert_eq!(result, env_plan);
+    }
+
     #[test]
     fn test_get_phases_with_dependencies() {
         let setup = Phase::new("setup");
@@ -311,7 +367,6 @@ mod test {
 
         let plan = BuildPlan::new(&vec![setup, install, build, another], None);
 
-        // let phases = topological_sort(plan.get_phases_with_dependencies("build")).unwrap();
         let build_phase = plan.get_phases_with_dependencies("build");
         let phases = build_phase.values();
 
@@ -341,4 +396,16 @@ mod test {
         );
         assert!(plan.get_phase("setup").unwrap().nixpkgs_archive.is_some());
     }
+
+    #[test]
+    fn test_split_env_string() {
+        assert_eq!(
+            split_env_string("nodejs yarn"),
+            vec!["nodejs".to_string(), "yarn".to_string()]
+        );
+        assert_eq!(
+            split_env_string("nodejs, yarn"),
+            vec!["nodejs".to_string(), "yarn".to_string()]
+        );
+    }
 }
diff --git a/src/providers/ruby.rs b/src/providers/ruby.rs
index 38f042d..adbe190 100644
--- a/src/providers/ruby.rs
+++ b/src/providers/ruby.rs
@@ -60,6 +60,9 @@ impl RubyProvider {
         let mut setup = Phase::setup(None);
         setup.add_apt_pkgs(vec!["procps".to_string()]);
 
+        // Don't re-install ruby if the code has changed
+        setup.only_include_files = Some(Vec::new());
+
         if self.uses_postgres(app)? {
             setup.add_apt_pkgs(vec!["libpq-dev".to_string()]);
         }


Test patch:
diff --git a/tests/snapshots/generate_plan_tests__config_from_environment_variables.snap b/tests/snapshots/generate_plan_tests__config_from_environment_variables.snap
index 1c7e4b0..94a5693 100644
--- a/tests/snapshots/generate_plan_tests__config_from_environment_variables.snap
+++ b/tests/snapshots/generate_plan_tests__config_from_environment_variables.snap
@@ -22,6 +22,10 @@ expression: plan
       ],
       "cmds": [
         "build"
+      ],
+      "cacheDirectories": [
+        "/build",
+        "barbaz"
       ]
     },
     "install": {
@@ -31,6 +35,10 @@ expression: plan
       ],
       "cmds": [
         "install"
+      ],
+      "cacheDirectories": [
+        "/tmp",
+        "foobar"
       ]
     },
     "setup": {
diff --git a/tests/snapshots/generate_plan_tests__ruby.snap b/tests/snapshots/generate_plan_tests__ruby.snap
index 0f59156..c60653d 100644
--- a/tests/snapshots/generate_plan_tests__ruby.snap
+++ b/tests/snapshots/generate_plan_tests__ruby.snap
@@ -40,7 +40,8 @@ expression: plan
         "rvm --default use ruby-3.1.2",
         "gem install bundler:2.3.7",
         "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
-      ]
+      ],
+      "onlyIncludeFiles": []
     }
   },
   "start": {
diff --git a/tests/snapshots/generate_plan_tests__ruby_execjs.snap b/tests/snapshots/generate_plan_tests__ruby_execjs.snap
index a5348b7..0614030 100644
--- a/tests/snapshots/generate_plan_tests__ruby_execjs.snap
+++ b/tests/snapshots/generate_plan_tests__ruby_execjs.snap
@@ -63,7 +63,8 @@ expression: plan
         "rvm --default use ruby-3.1.2",
         "gem install bundler:2.3.7",
         "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
-      ]
+      ],
+      "onlyIncludeFiles": []
     }
   },
   "start": {
diff --git a/tests/snapshots/generate_plan_tests__ruby_local_deps.snap b/tests/snapshots/generate_plan_tests__ruby_local_deps.snap
index 3d222c8..3f8acad 100644
--- a/tests/snapshots/generate_plan_tests__ruby_local_deps.snap
+++ b/tests/snapshots/generate_plan_tests__ruby_local_deps.snap
@@ -40,7 +40,8 @@ expression: plan
         "rvm --default use ruby-3.1.2",
         "gem install bundler:2.3.7",
         "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
-      ]
+      ],
+      "onlyIncludeFiles": []
     }
   },
   "start": {
diff --git a/tests/snapshots/generate_plan_tests__ruby_rails_postgres.snap b/tests/snapshots/generate_plan_tests__ruby_rails_postgres.snap
index 14ba927..a59c8ad 100644
--- a/tests/snapshots/generate_plan_tests__ruby_rails_postgres.snap
+++ b/tests/snapshots/generate_plan_tests__ruby_rails_postgres.snap
@@ -51,7 +51,8 @@ expression: plan
         "rvm --default use 3.1.2",
         "gem install bundler:2.3.7",
         "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
-      ]
+      ],
+      "onlyIncludeFiles": []
     }
   },
   "start": {
diff --git a/tests/snapshots/generate_plan_tests__ruby_sinatra.snap b/tests/snapshots/generate_plan_tests__ruby_sinatra.snap
index 73a4dd1..5e36ab7 100644
--- a/tests/snapshots/generate_plan_tests__ruby_sinatra.snap
+++ b/tests/snapshots/generate_plan_tests__ruby_sinatra.snap
@@ -40,7 +40,8 @@ expression: plan
         "rvm --default use ruby-3.1.2",
         "gem install bundler:2.3.7",
         "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
-      ]
+      ],
+      "onlyIncludeFiles": []
     }
   },
   "start": {
diff --git a/tests/snapshots/generate_plan_tests__ruby_with_node.snap b/tests/snapshots/generate_plan_tests__ruby_with_node.snap
index c96f5af..4eda5ce 100644
--- a/tests/snapshots/generate_plan_tests__ruby_with_node.snap
+++ b/tests/snapshots/generate_plan_tests__ruby_with_node.snap
@@ -66,7 +66,8 @@ expression: plan
         "rvm --default use ruby-3.1.2",
         "gem install bundler:2.3.7",
         "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
-      ]
+      ],
+      "onlyIncludeFiles": []
     }
   },
   "start": {