Repository: railwayapp/nixpacks
Pull request #491 (2022-09-01 01:20:10)

Read Elixir version from mix.exs and allow user to override 
<!-- Thanks for sending a pull request! -->
<!-- Please add a useful description explaining what this PR does -->

Currently, we choose latest version for any Elixir project
For old projects that utilizes deprecated code (in latest Elixir), this would break their build.

This PR parses `mix.exs` to read the right Elixir version beside also enable users to override Elixir version chosen by Nixpacks
By setting `NIXPACKS_ELIXIR_VERSION` environment variable or writing the desired version to the `.elixir-version` file

<!-- PR Checklist  -->
<!-- - [ ] Tests are added/updated if needed  -->
<!-- - [ ] Docs are updated if needed  -->



Patch:
diff --git a/docs/pages/docs/providers/clojure.md b/docs/pages/docs/providers/clojure.md
index dd03ab9..5e25739 100644
--- a/docs/pages/docs/providers/clojure.md
+++ b/docs/pages/docs/providers/clojure.md
@@ -23,11 +23,13 @@ The version can be overriden by
 
 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 ring uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then mv /app/target/uberjar/standalone.jar /app/target/*standalone.jar; fi
 ```
 
 Default
-```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/uberjar/*standalone.jar ]; then mv /app/target/uberjar/standalone.jar /app/target/*standalone.jar; fi
 ```
 
 
diff --git a/docs/pages/docs/providers/elixir.md b/docs/pages/docs/providers/elixir.md
index b1aaed2..33f022b 100644
--- a/docs/pages/docs/providers/elixir.md
+++ b/docs/pages/docs/providers/elixir.md
@@ -7,7 +7,24 @@ title: Elixir
 Elixir is detected if a `mix.exs` file is found.
 
 ## Setup
+The following Elixir versions are available
+
+- `latest`  (Default)
+- `1.12`
+- `1.11`
+- `1.10`
+- `1.9`
+
+The version can be overriden by
+
+- Setting the `NIXPACKS_ELIXIR_VERSION` environment variable
+- Setting the version in a `.elixir-version` file
+
+
+
 ```
+MIX_ENV=prod 
+
 mix local.hex --force
 mix local.rebar --force
 mix deps.get --only prod
diff --git a/src/providers/elixir.rs b/src/providers/elixir.rs
index 4c366ed..98b2670 100644
--- a/src/providers/elixir.rs
+++ b/src/providers/elixir.rs
@@ -1,7 +1,7 @@
 use super::Provider;
 use crate::nixpacks::{
     app::App,
-    environment::Environment,
+    environment::{Environment, EnvironmentVariables},
     nix::pkg::Pkg,
     plan::{
         phase::{Phase, StartPhase},
@@ -9,6 +9,8 @@ use crate::nixpacks::{
     },
 };
 use anyhow::Result;
+use regex::{Match, Regex};
+const DEFAULT_ELIXIR_PKG_NAME: &str = "elixir";
 
 pub struct ElixirProvider {}
 
@@ -21,10 +23,17 @@ impl Provider for ElixirProvider {
         Ok(app.includes_file("mix.exs"))
     }
 
-    fn get_build_plan(&self, _app: &App, _env: &Environment) -> Result<Option<BuildPlan>> {
+    fn get_build_plan(&self, app: &App, env: &Environment) -> Result<Option<BuildPlan>> {
         let mut plan = BuildPlan::default();
 
-        let setup_phase = Phase::setup(Some(vec![Pkg::new("elixir")]));
+        let elixir_pkg = ElixirProvider::get_nix_elixir_package(app, env)?;
+
+        plan.add_variables(EnvironmentVariables::from([(
+            "MIX_ENV".to_string(),
+            "prod".to_string(),
+        )]));
+
+        let setup_phase = Phase::setup(Some(vec![elixir_pkg]));
         plan.add_phase(setup_phase);
 
         // Install Phase
@@ -48,3 +57,59 @@ impl Provider for ElixirProvider {
         Ok(Some(plan))
     }
 }
+
+impl ElixirProvider {
+    fn get_nix_elixir_package(app: &App, env: &Environment) -> Result<Pkg> {
+        fn as_default(v: Option<Match>) -> &str {
+            match v {
+                Some(m) => m.as_str(),
+                None => "_",
+            }
+        }
+
+        let mix_exs_content = app.read_file("mix.exs")?;
+        let custom_version = env.get_config_variable("ELIXIR_VERSION");
+
+        let mix_elixir_version_regex = Regex::new(r#"(elixir:[\s].*[> ])([0-9|\.]*)"#)?;
+
+        // If not from env variable, get it from the .elixir-version file then try to parse from mix.exs
+        let custom_version = if custom_version.is_some() {
+            custom_version
+        } else if custom_version.is_none() && app.includes_file(".elixir-version") {
+            Some(app.read_file(".elixir-version")?)
+        } else {
+            mix_elixir_version_regex
+                .captures(&mix_exs_content)
+                .map(|c| c.get(2).unwrap().as_str().to_owned())
+        };
+
+        // If it's still none, return default
+        if custom_version.is_none() {
+            return Ok(Pkg::new(DEFAULT_ELIXIR_PKG_NAME));
+        }
+        let custom_version = custom_version.unwrap();
+
+        // Regex for reading Elixir versions (e.g. 1.8 or 1.12)
+        let elixir_version_regex =
+            Regex::new(r#"^(?:[\sa-zA-Z-"']*)(\d*)(?:\.*)(\d*)(?:\.*\d*)(?:["']?)$"#)?;
+
+        // Capture matches
+        let matches = elixir_version_regex.captures(custom_version.as_str().trim());
+
+        // If no matches, just use default
+        if matches.is_none() {
+            return Ok(Pkg::new(DEFAULT_ELIXIR_PKG_NAME));
+        }
+        let matches = matches.unwrap();
+        let parsed_version = (as_default(matches.get(1)), as_default(matches.get(2)));
+
+        // Match major and minor versions
+        match parsed_version {
+            ("1", "9") => Ok(Pkg::new("elixir_1_9")),
+            ("1", "10") => Ok(Pkg::new("elixir_1_10")),
+            ("1", "11") => Ok(Pkg::new("elixir_1_11")),
+            ("1", "12") => Ok(Pkg::new("elixir_1_12")),
+            _ => Ok(Pkg::new(DEFAULT_ELIXIR_PKG_NAME)),
+        }
+    }
+}


Test patch:
diff --git a/tests/snapshots/generate_plan_tests__elixir_no_ecto.snap b/tests/snapshots/generate_plan_tests__elixir_no_ecto.snap
index 5315912..c1ed2c1 100644
--- a/tests/snapshots/generate_plan_tests__elixir_no_ecto.snap
+++ b/tests/snapshots/generate_plan_tests__elixir_no_ecto.snap
@@ -5,6 +5,9 @@ expression: plan
 {
   "nixpacksVersion": "[version]",
   "buildImage": "[build_image]",
+  "variables": {
+    "MIX_ENV": "prod"
+  },
   "phases": [
     {
       "name": "install",
@@ -21,7 +24,7 @@ expression: plan
       "name": "setup",
       "nixPackages": [
         {
-          "name": "elixir"
+          "name": "elixir_1_12"
         }
       ]
     },