diff --git a/source/Calamari.Scripting/DotnetScript/dotnet-script.1.6.0.zip b/source/Calamari.Scripting/DotnetScript/dotnet-script.1.6.0.zip
deleted file mode 100644
index 88cda61773..0000000000
Binary files a/source/Calamari.Scripting/DotnetScript/dotnet-script.1.6.0.zip and /dev/null differ
diff --git a/source/Calamari.Scripting/DotnetScript/dotnet-script.2.0.1.zip b/source/Calamari.Scripting/DotnetScript/dotnet-script.2.0.1.zip
new file mode 100644
index 0000000000..65024309a4
Binary files /dev/null and b/source/Calamari.Scripting/DotnetScript/dotnet-script.2.0.1.zip differ
diff --git a/source/Calamari.Scripting/DotnetScript/dotnet-script.runtimeconfig.json b/source/Calamari.Scripting/DotnetScript/dotnet-script.runtimeconfig.json
new file mode 100644
index 0000000000..5d68f1c6ee
--- /dev/null
+++ b/source/Calamari.Scripting/DotnetScript/dotnet-script.runtimeconfig.json
@@ -0,0 +1,14 @@
+{
+ "runtimeOptions": {
+ "tfm": "net8.0",
+ "rollForward": "Major",
+ "framework": {
+ "name": "Microsoft.NETCore.App",
+ "version": "8.0.0"
+ },
+ "configProperties": {
+ "System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
+ "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
+ }
+ }
+}
diff --git a/source/Calamari.Tests/Fixtures/DotnetScript/DotnetScriptRuntimeConfigFixture.cs b/source/Calamari.Tests/Fixtures/DotnetScript/DotnetScriptRuntimeConfigFixture.cs
new file mode 100644
index 0000000000..1829e6e699
--- /dev/null
+++ b/source/Calamari.Tests/Fixtures/DotnetScript/DotnetScriptRuntimeConfigFixture.cs
@@ -0,0 +1,102 @@
+#nullable enable
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using Calamari.Testing.Helpers;
+using FluentAssertions;
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+
+namespace Calamari.Tests.Fixtures.DotnetScript
+{
+ ///
+ /// dotnet-script is framework-dependent and the upstream build requests
+ /// Microsoft.NETCore.App 8.0.0 with no rollForward, so by default it will not start on a target
+ /// that has no 8.x runtime installed. Calamari ships a roll-forward policy over the top.
+ ///
+ /// The policy lives in source control as dotnet-script.runtimeconfig.json and is copied over the
+ /// extracted file by IncludeDotNetScript.targets. These tests guard the two ways that can break:
+ /// the copy silently not happening, and a future re-vendor bringing new upstream settings that
+ /// the override then clobbers.
+ ///
+ [TestFixture]
+ [Category(TestCategory.PlatformAgnostic)]
+ public class DotnetScriptRuntimeConfigFixture
+ {
+ const string ExpectedRollForward = "Major";
+
+ [Test]
+ public void BundledDotnetScript_RollsForwardToTheNewestInstalledRuntime()
+ {
+ var runtimeConfig = JObject.Parse(File.ReadAllText(BundledRuntimeConfigPath()));
+
+ runtimeConfig["runtimeOptions"]?["rollForward"]?.Value()
+ .Should()
+ .Be(ExpectedRollForward,
+ "without it, C# script steps fail to launch on a target that has no 8.x runtime");
+ }
+
+ ///
+ /// The override replaces the whole file, so anything upstream adds or changes would be
+ /// silently dropped. If this fails after re-vendoring the zip, reconcile the override with
+ /// the new upstream file rather than just updating the expectation.
+ ///
+ [Test]
+ public void BundledRuntimeConfig_DiffersFromUpstreamOnlyByRollForward()
+ {
+ var vendoredZip = FindVendoredZip();
+ if (vendoredZip == null)
+ Assert.Inconclusive("Vendored dotnet-script zip not found - this test needs a source checkout.");
+
+ using var archive = ZipFile.OpenRead(vendoredZip!);
+ var upstreamEntry = archive.Entries
+ .Single(e => e.FullName.EndsWith("dotnet-script.runtimeconfig.json"));
+
+ using var reader = new StreamReader(upstreamEntry.Open());
+ var upstream = JObject.Parse(reader.ReadToEnd());
+ var shipped = JObject.Parse(File.ReadAllText(BundledRuntimeConfigPath()));
+
+ upstream["runtimeOptions"]?["rollForward"]
+ .Should()
+ .BeNull("upstream is expected to set no policy - if it now does, the override may be redundant");
+
+ // Normalise away the one intended difference, then the two files must agree.
+ ((JObject)shipped["runtimeOptions"]!).Remove("rollForward");
+
+ JToken.DeepEquals(shipped, upstream)
+ .Should()
+ .BeTrue($"the shipped runtimeconfig should match upstream apart from rollForward.{System.Environment.NewLine}"
+ + $"upstream: {upstream.ToString(Newtonsoft.Json.Formatting.None)}{System.Environment.NewLine}"
+ + $"shipped: {shipped.ToString(Newtonsoft.Json.Formatting.None)}");
+ }
+
+ static string BundledRuntimeConfigPath()
+ {
+ var path = TestEnvironment.GetTestPath("dotnet-script", "dotnet-script.runtimeconfig.json");
+ File.Exists(path)
+ .Should()
+ .BeTrue($"IncludeDotNetScript.targets should have extracted dotnet-script to {path}");
+ return path;
+ }
+
+ ///
+ /// Walks up from the test output to the checkout, since the zip is a source artefact and is
+ /// not copied to the build output.
+ ///
+ static string? FindVendoredZip()
+ {
+ var directory = new DirectoryInfo(TestEnvironment.CurrentWorkingDirectory);
+
+ while (directory != null)
+ {
+ var candidate = Path.Combine(directory.FullName, "source", "Calamari.Scripting", "DotnetScript");
+ if (Directory.Exists(candidate))
+ return Directory.GetFiles(candidate, "dotnet-script.*.zip").SingleOrDefault();
+
+ directory = directory.Parent;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/source/IncludeDotNetScript.targets b/source/IncludeDotNetScript.targets
index 1f6ed73182..81ba0e407d 100644
--- a/source/IncludeDotNetScript.targets
+++ b/source/IncludeDotNetScript.targets
@@ -3,6 +3,13 @@
+
+
@@ -10,6 +17,7 @@
+
@@ -18,6 +26,7 @@
+
-
\ No newline at end of file
+