Repository: railwayapp/nixpacks
Pull request #617 (2022-10-14 14:18:16)

Install nodejs only once for the Ruby provider
Fixes https://github.com/railwayapp/nixpacks/issues/616

This PR
- Uses the node provider to install NodeJS if the `execjs` gem is detected
- Move the installation of various deps to the setup phase instead of the install phase
- Do not run `npm i` if there is no `package.json` file detected
- Adds a test for ruby with execjs


Patch:
diff --git a/examples/ruby-execjs/Gemfile b/examples/ruby-execjs/Gemfile
new file mode 100644
index 0000000..2f6fcf4
--- /dev/null
+++ b/examples/ruby-execjs/Gemfile
@@ -0,0 +1,4 @@
+
+source 'https://rubygems.org'
+
+gem 'execjs', '~> 2.8.1'
diff --git a/examples/ruby-execjs/Gemfile.lock b/examples/ruby-execjs/Gemfile.lock
new file mode 100644
index 0000000..463f43c
--- /dev/null
+++ b/examples/ruby-execjs/Gemfile.lock
@@ -0,0 +1,14 @@
+GEM
+  remote: https://rubygems.org/
+  specs:
+
+PLATFORMS
+  arm64-darwin-21
+
+DEPENDENCIES
+
+RUBY VERSION
+   ruby 3.1.2p20
+
+BUNDLED WITH
+   2.3.7
diff --git a/examples/ruby-execjs/Procfile b/examples/ruby-execjs/Procfile
new file mode 100644
index 0000000..82cb606
--- /dev/null
+++ b/examples/ruby-execjs/Procfile
@@ -0,0 +1,1 @@
+web: ruby app.rb
\ No newline at end of file
diff --git a/examples/ruby-execjs/app.rb b/examples/ruby-execjs/app.rb
new file mode 100644
index 0000000..9955b60
--- /dev/null
+++ b/examples/ruby-execjs/app.rb
@@ -0,0 +1,4 @@
+require "execjs"
+
+value = ExecJS.eval "'hello from execjs'.toUpperCase()"
+puts value
diff --git a/src/providers/node/mod.rs b/src/providers/node/mod.rs
index 3bd24f2..e0fa20b 100644
--- a/src/providers/node/mod.rs
+++ b/src/providers/node/mod.rs
@@ -78,7 +78,7 @@ impl Provider for NodeProvider {
         }
 
         // Install
-        let mut install = Phase::install(Some(NodeProvider::get_install_command(app)));
+        let mut install = Phase::install(NodeProvider::get_install_command(app));
         install.add_cache_directory(NodeProvider::get_package_manager_cache_dir(app));
         install.add_path("/app/node_modules/.bin".to_string());
 
@@ -232,7 +232,11 @@ impl NodeProvider {
         pkg_manager.to_string()
     }
 
-    pub fn get_install_command(app: &App) -> String {
+    pub fn get_install_command(app: &App) -> Option<String> {
+        if !app.includes_file("package.json") {
+            return None;
+        }
+
         let mut install_cmd = "npm i";
         let package_manager = NodeProvider::get_package_manager(app);
         if package_manager == "pnpm" {
@@ -248,7 +252,8 @@ impl NodeProvider {
         } else if app.includes_file("bun.lockb") {
             install_cmd = "bun i --no-save";
         }
-        install_cmd.to_string()
+
+        Some(install_cmd.to_string())
     }
 
     fn get_package_manager_cache_dir(app: &App) -> String {
diff --git a/src/providers/php/mod.rs b/src/providers/php/mod.rs
index 3764ec7..a4c48c0 100644
--- a/src/providers/php/mod.rs
+++ b/src/providers/php/mod.rs
@@ -81,8 +81,9 @@ impl PhpProvider {
         if app.includes_file("composer.json") {
             install.add_cmd("composer install".to_string());
         };
-        if app.includes_file("package.json") {
-            install.add_cmd(NodeProvider::get_install_command(app));
+
+        if let Some(node_install_cmd) = NodeProvider::get_install_command(app) {
+            install.add_cmd(node_install_cmd);
         }
 
         install
diff --git a/src/providers/ruby.rs b/src/providers/ruby.rs
index 9c228b9..38f042d 100644
--- a/src/providers/ruby.rs
+++ b/src/providers/ruby.rs
@@ -39,7 +39,7 @@ impl Provider for RubyProvider {
         );
 
         let node = NodeProvider::default();
-        if node.detect(app, env)? {
+        if node.detect(app, env)? || self.uses_gem_dep(app, "execjs") {
             let node_build_plan = node.get_build_plan(app, env)?;
             if let Some(node_build_plan) = node_build_plan {
                 // Include the install phase from the node provider
@@ -68,6 +68,15 @@ impl RubyProvider {
             setup.add_apt_pkgs(vec!["default-libmysqlclient-dev".to_string()]);
         }
 
+        if self.uses_gem_dep(app, "rmagick") {
+            setup.add_apt_pkgs(vec![String::from("libmagickwand-dev")]);
+            setup.add_nix_pkgs(&[Pkg::new("imagemagick")]);
+        }
+
+        if self.uses_gem_dep(app, "charlock_holmes") {
+            setup.add_apt_pkgs(vec![String::from("libicu-dev")]);
+        }
+
         setup.add_cmd(
             "curl -sSL https://get.rvm.io | bash -s stable && . /etc/profile.d/rvm.sh".to_string(),
         );
@@ -92,17 +101,6 @@ impl RubyProvider {
         install.add_path(format!("/usr/local/rvm/gems/{}/bin", ruby_version));
         install.add_path(format!("/usr/local/rvm/gems/{}@global/bin", ruby_version));
 
-        if self.uses_gem_dep(app, "execjs") {
-            install.add_nix_pkgs(&[Pkg::new("nodejs")]);
-        }
-        if self.uses_gem_dep(app, "rmagick") {
-            install.add_apt_pkgs(vec![String::from("libmagickwand-dev")]);
-            install.add_nix_pkgs(&[Pkg::new("imagemagick")]);
-        }
-        if self.uses_gem_dep(app, "charlock_holmes") {
-            install.add_apt_pkgs(vec![String::from("libicu-dev")]);
-        }
-
         Ok(Some(install))
     }
 


Test patch:
diff --git a/tests/docker_run_tests.rs b/tests/docker_run_tests.rs
index 30b9ab6..879267d 100644
--- a/tests/docker_run_tests.rs
+++ b/tests/docker_run_tests.rs
@@ -794,6 +794,13 @@ fn test_ruby_node() {
     assert!(output.contains("Hello from Ruby with Node"));
 }
 
+#[test]
+fn test_ruby_execjs() {
+    let name = simple_build("./examples/ruby-execjs/");
+    let output = run_image(&name, None);
+    assert!(output.contains("HELLO FROM EXECJS"));
+}
+
 #[test]
 fn test_ruby_local_deps() {
     let name = simple_build("./examples/ruby-local-deps/");
diff --git a/tests/snapshots/generate_plan_tests__ruby_execjs.snap b/tests/snapshots/generate_plan_tests__ruby_execjs.snap
new file mode 100644
index 0000000..a5348b7
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__ruby_execjs.snap
@@ -0,0 +1,72 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "providers": [],
+  "buildImage": "[build_image]",
+  "variables": {
+    "BUNDLE_GEMFILE": "/app/Gemfile",
+    "GEM_HOME": "/usr/local/rvm/gems/ruby-3.1.2",
+    "GEM_PATH": "/usr/local/rvm/gems/ruby-3.1.2:/usr/local/rvm/gems/ruby-3.1.2@global",
+    "NIXPACKS_METADATA": "Ruby"
+  },
+  "phases": {
+    "install": {
+      "name": "install",
+      "dependsOn": [
+        "setup"
+      ],
+      "cmds": [
+        "bundle install"
+      ],
+      "cacheDirectories": [
+        "/root/.bundle/cache"
+      ],
+      "paths": [
+        "/usr/local/rvm/rubies/ruby-3.1.2/bin",
+        "/usr/local/rvm/gems/ruby-3.1.2/bin",
+        "/usr/local/rvm/gems/ruby-3.1.2@global/bin"
+      ]
+    },
+    "node:install": {
+      "name": "node:install",
+      "dependsOn": [
+        "setup"
+      ],
+      "cacheDirectories": [
+        "/root/.npm"
+      ],
+      "paths": [
+        "/app/node_modules/.bin"
+      ]
+    },
+    "node:setup": {
+      "name": "node:setup",
+      "nixPkgs": [
+        "nodejs-16_x",
+        "npm-8_x"
+      ],
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      ],
+      "nixpkgsArchive": "[archive]"
+    },
+    "setup": {
+      "name": "setup",
+      "aptPkgs": [
+        "procps"
+      ],
+      "cmds": [
+        "curl -sSL https://get.rvm.io | bash -s stable && . /etc/profile.d/rvm.sh",
+        "rvm install ruby-3.1.2",
+        "rvm --default use ruby-3.1.2",
+        "gem install bundler:2.3.7",
+        "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
+      ]
+    }
+  },
+  "start": {
+    "cmd": "ruby app.rb"
+  }
+}