Repository: railwayapp/nixpacks (rust)

Issue:
Golang Version Provider
Golang Nixpack should support versioning, defined inside of go.mod

It should NOT use custom flagging like the Heroku one (e.g should parse the inline go version)

Pull request description:
add versioning support for golang provider
Added support for retrieving the Go version number from `go.mod` (if exists). Also added an example and tests for the version parsing method, the docker run output, and plan generation.

Note: I've also updated `examples/go-mod`, as it was pointing to a Go version that's not available on nix unstable.

Closes #148 

Patch:
diff --git a/examples/go-custom-version/go.mod b/examples/go-custom-version/go.mod
new file mode 100644
index 0000000..c7db24b
--- /dev/null
+++ b/examples/go-custom-version/go.mod
@@ -0,0 +1,3 @@
+module versiontest
+
+go 1.18
diff --git a/examples/go-custom-version/main.go b/examples/go-custom-version/main.go
new file mode 100644
index 0000000..9774657
--- /dev/null
+++ b/examples/go-custom-version/main.go
@@ -0,0 +1,10 @@
+package main
+
+import (
+	"fmt"
+	"runtime"
+)
+
+func main() {
+	fmt.Printf("Hello from %s!\n", runtime.Version())
+}
diff --git a/examples/go-mod/go.mod b/examples/go-mod/go.mod
index b63c81c..4ec7e2e 100644
--- a/examples/go-mod/go.mod
+++ b/examples/go-mod/go.mod
@@ -1,6 +1,6 @@
 module gin
 
-go 1.14
+go 1.17
 
 require (
 	github.com/gin-gonic/gin v1.7.7
diff --git a/src/providers/go.rs b/src/providers/go.rs
index 29bfd44..5caa611 100644
--- a/src/providers/go.rs
+++ b/src/providers/go.rs
@@ -5,11 +5,13 @@ use crate::nixpacks::{
     nix::Pkg,
     phase::{BuildPhase, InstallPhase, SetupPhase, StartPhase},
 };
-use anyhow::Result;
+use anyhow::{bail, Result};
 
 pub struct GolangProvider {}
 
 pub const BINARY_NAME: &'static &str = &"out";
+const AVAILABLE_GO_VERSIONS: &[(&str, &str)] = &[("1.17", "go"), ("1.18", "go_1_18")];
+pub const DEFAULT_GO_PKG_NAME: &'static &str = &"go";
 
 impl Provider for GolangProvider {
     fn name(&self) -> &str {
@@ -21,7 +23,10 @@ impl Provider for GolangProvider {
     }
 
     fn setup(&self, _app: &App, _env: &Environment) -> Result<Option<SetupPhase>> {
-        Ok(Some(SetupPhase::new(vec![Pkg::new("go")])))
+        let go_mod = self.read_go_mod_if_exists(_app)?;
+        let nix_pkg = GolangProvider::get_nix_golang_pkg(go_mod)?;
+
+        Ok(Some(SetupPhase::new(vec![Pkg::new(&nix_pkg)])))
     }
 
     fn install(&self, app: &App, _env: &Environment) -> Result<Option<InstallPhase>> {
@@ -72,3 +77,79 @@ impl Provider for GolangProvider {
         )])))
     }
 }
+
+impl GolangProvider {
+    pub fn read_go_mod_if_exists(&self, app: &App) -> Result<Option<String>> {
+        if app.includes_file("go.mod") {
+            Ok(Some(app.read_file("go.mod")?))
+        } else {
+            Ok(None)
+        }
+    }
+
+    pub fn get_nix_golang_pkg(go_mod_contents: Option<String>) -> Result<String> {
+        if go_mod_contents.is_some() {
+            let mut lines = go_mod_contents.as_ref().unwrap().lines();
+            let go_version_line = lines.find(|line| line.trim().starts_with("go"));
+
+            if let Some(go_version_line) = go_version_line {
+                let go_version = go_version_line.trim().split_whitespace().nth(1).unwrap();
+
+                if let Some(nix_pkg) = version_number_to_pkg(go_version)? {
+                    return Ok(nix_pkg);
+                }
+            }
+        }
+
+        Ok(DEFAULT_GO_PKG_NAME.to_string())
+    }
+}
+
+fn version_number_to_pkg(version: &str) -> Result<Option<String>> {
+    let matched_version = AVAILABLE_GO_VERSIONS.iter().find(|(v, _)| v == &version);
+
+    match matched_version {
+        Some((_, pkg)) => Ok(Some(pkg.to_string())),
+        None => bail!("Go version {} is not available", version),
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn test_no_go_mod() -> Result<()> {
+        assert_eq!(
+            GolangProvider::get_nix_golang_pkg(None)?,
+            DEFAULT_GO_PKG_NAME.to_string()
+        );
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_with_go_mod() -> Result<()> {
+        let go_mod_contents = r#"
+            go 1.18
+        "#;
+
+        assert_eq!(
+            GolangProvider::get_nix_golang_pkg(Some(go_mod_contents.to_string()))?,
+            "go_1_18".to_string()
+        );
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_invalid_version() -> Result<()> {
+        let go_mod_contents = r#"
+            go 1.8
+        "#;
+
+        assert!(GolangProvider::get_nix_golang_pkg(Some(go_mod_contents.to_string())).is_err());
+
+        Ok(())
+    }
+}


Test patch:
diff --git a/tests/docker_run_tests.rs b/tests/docker_run_tests.rs
index 6fd13e3..355dcea 100644
--- a/tests/docker_run_tests.rs
+++ b/tests/docker_run_tests.rs
@@ -207,6 +207,13 @@ fn test_go() {
     assert!(output.contains("Hello from Go"));
 }
 
+#[test]
+fn test_go_custom_version() {
+    let name = simple_build("./examples/go-custom-version");
+    let output = run_image(name);
+    assert!(output.contains("Hello from go1.18"));
+}
+
 #[test]
 fn test_haskell_stack() {
     let name = simple_build("./examples/haskell-stack");
diff --git a/tests/generate_plan_tests.rs b/tests/generate_plan_tests.rs
index a13ce91..f7395ab 100644
--- a/tests/generate_plan_tests.rs
+++ b/tests/generate_plan_tests.rs
@@ -218,6 +218,21 @@ fn test_go_mod() -> Result<()> {
     Ok(())
 }
 
+#[test]
+fn test_go_custom_version() -> Result<()> {
+    let plan = gen_plan(
+        "./examples/go-custom-version",
+        Vec::new(),
+        None,
+        None,
+        Vec::new(),
+        false,
+    )?;
+    assert_eq!(plan.setup.unwrap().pkgs, vec![Pkg::new("go_1_18")]);
+
+    Ok(())
+}
+
 #[test]
 fn test_deno() -> Result<()> {
     let plan = gen_plan("./examples/deno", Vec::new(), None, None, Vec::new(), false)?;