Repository: railwayapp/nixpacks (rust)

Issue:
Configurable .NET Version
### Feature request

Real senario: I need to deploy .NET 5, but by default Nixpacks builds with .NET 6. I can't change `dotnet-sdk` to `dotnet-sdk_5` right now. There's `NIXPACKS_PKGS` but that's specifying *additional* packages.

### Motivation

I want to deploy .NET 5.

### Contribution

_No response_

Pull request description:
feat(csharp): allow configurable sdk version
<!-- Thanks for sending a pull request! -->
<!-- Please add a useful description explaining what this PR does -->

This PR allows the dotnet SDK version for C# projects to be provided by setting the `NIXPACKS_CSHARP_SDK_VERSION` environment variable or by including a `global.json` file in your project. If no version is specified, the existing default SDK will be used.

Tests were added for all three conditions.

Closes #323 

<!-- PR Checklist  -->
- [x] Tests are added/updated if needed
- [x] Docs are updated if needed 


Patch:
diff --git a/.cargo/config.toml b/.cargo/config.toml
index 81814c1..a4ea077 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -1,4 +1,4 @@
 [alias]
-generate_plan_tests = "test --package nixpacks --lib --test generate_plan_tests"
+generate-plan-tests = "test --package nixpacks --lib --test generate_plan_tests"
 snapshot = "insta test --review -- --test generate_plan_tests"
 lint-fix = "clippy --fix --allow-dirty --allow-staged --all-targets --all-features -- -D warnings"
diff --git a/docs/pages/docs/providers/csharp.md b/docs/pages/docs/providers/csharp.md
index 902063e..889e341 100644
--- a/docs/pages/docs/providers/csharp.md
+++ b/docs/pages/docs/providers/csharp.md
@@ -6,6 +6,11 @@ title: C#
 
 CSharp is detected if any `*.csproj*` files are found.
 
+The SDK version can be overridden by
+
+- Setting the `NIXPACKS_CSHARP_SDK_VERSION` environment variable
+- Setting the version in a `global.json` file
+
 ## Install
 
 ```
diff --git a/examples/csharp-api/global.json b/examples/csharp-api/global.json
new file mode 100644
index 0000000..86f1e0c
--- /dev/null
+++ b/examples/csharp-api/global.json
@@ -0,0 +1,5 @@
+{
+  "sdk": {
+    "version": "7.0.0"
+  }
+}
diff --git a/src/providers/csharp.rs b/src/providers/csharp.rs
index 21dfed6..90fc0b6 100644
--- a/src/providers/csharp.rs
+++ b/src/providers/csharp.rs
@@ -9,6 +9,17 @@ use crate::nixpacks::{
     },
 };
 use anyhow::{Context, Result};
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Default, Debug)]
+pub struct CSharpSdk {
+    pub version: Option<String>,
+}
+
+#[derive(Serialize, Deserialize, Default, Debug)]
+pub struct CSharpGlobalJson {
+    pub sdk: Option<CSharpSdk>,
+}
 
 pub struct CSharpProvider {}
 
@@ -23,8 +34,9 @@ impl Provider for CSharpProvider {
         Ok(!app.find_files("*.csproj")?.is_empty())
     }
 
-    fn get_build_plan(&self, app: &App, _env: &Environment) -> Result<Option<BuildPlan>> {
-        let setup = Phase::setup(Some(vec![Pkg::new("dotnet-sdk")]));
+    fn get_build_plan(&self, app: &App, env: &Environment) -> Result<Option<BuildPlan>> {
+        let sdk = CSharpProvider::get_sdk_version(app, env);
+        let setup = Phase::setup(Some(vec![Pkg::new(sdk?.as_str())]));
         let install = Phase::install(Some("dotnet restore".to_string()));
         let build = Phase::build(Some(format!(
             "dotnet publish --no-restore -c Release -o {ARTIFACT_DIR}"
@@ -57,3 +69,82 @@ impl Provider for CSharpProvider {
         Ok(Some(plan))
     }
 }
+
+impl CSharpProvider {
+    fn get_sdk_version(app: &App, env: &Environment) -> Result<String> {
+        // First check for an SDK version environment variable
+        if let Some(version) = env.get_config_variable("CSHARP_SDK_VERSION") {
+            if let Some((major, minor)) = &version[0..3].split_once('.') {
+                return Ok(format!("dotnet-sdk_{major}_{minor}"));
+            }
+        }
+
+        // Then check for a global.json and see if we can get the sdk version from there
+        if app.includes_file("global.json") {
+            let global_json: CSharpGlobalJson = app.read_json("global.json")?;
+
+            if let Some(sdk) = global_json.sdk {
+                if let Some(version) = sdk.version {
+                    if let Some((major, minor)) = &version[0..3].split_once('.') {
+                        return Ok(format!("dotnet-sdk_{major}_{minor}"));
+                    }
+                }
+            }
+        }
+
+        // Fall back to default sdk
+        Ok("dotnet-sdk".to_string())
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use crate::nixpacks::{app::App, environment::Environment};
+    use std::collections::BTreeMap;
+
+    #[test]
+    fn test_no_version() -> Result<()> {
+        let expected_sdk_name = "dotnet-sdk";
+        assert_eq!(
+            CSharpProvider::get_sdk_version(
+                &App::new("./examples/csharp-cli")?,
+                &Environment::default()
+            )?,
+            expected_sdk_name
+        );
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_global_json() -> Result<()> {
+        let expected_sdk_name = "dotnet-sdk_7_0";
+        assert_eq!(
+            CSharpProvider::get_sdk_version(
+                &App::new("./examples/csharp-api")?,
+                &Environment::default()
+            )?,
+            expected_sdk_name
+        );
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_version_from_environment_variable() -> Result<()> {
+        let expected_sdk_name = "dotnet-sdk_6_0";
+        assert_eq!(
+            CSharpProvider::get_sdk_version(
+                &App::new("./examples/csharp-cli")?,
+                &Environment::new(BTreeMap::from([(
+                    "NIXPACKS_CSHARP_SDK_VERSION".to_string(),
+                    "6.0.0".to_string()
+                )]))
+            )?,
+            expected_sdk_name
+        );
+
+        Ok(())
+    }
+}


Test patch:
diff --git a/tests/snapshots/generate_plan_tests__csharp_api.snap b/tests/snapshots/generate_plan_tests__csharp_api.snap
index 1077ce9..47c14ee 100644
--- a/tests/snapshots/generate_plan_tests__csharp_api.snap
+++ b/tests/snapshots/generate_plan_tests__csharp_api.snap
@@ -33,7 +33,7 @@ expression: plan
     "setup": {
       "name": "setup",
       "nixPkgs": [
-        "dotnet-sdk"
+        "dotnet-sdk_7_0"
       ],
       "nixOverlays": [],
       "nixpkgsArchive": "[archive]"