diff --git a/source/Calamari.Common/Features/Scripting/DotnetScript/DotnetScriptBootstrapper.cs b/source/Calamari.Common/Features/Scripting/DotnetScript/DotnetScriptBootstrapper.cs index 0e92786e7b..34536083b5 100644 --- a/source/Calamari.Common/Features/Scripting/DotnetScript/DotnetScriptBootstrapper.cs +++ b/source/Calamari.Common/Features/Scripting/DotnetScript/DotnetScriptBootstrapper.cs @@ -95,13 +95,23 @@ public static string FindBundledExecutable() throw new CommandException(string.Format("dotnet-script was not found at '{0}'", executable)); } - public static string FormatCommandArguments(string bootstrapFile, string? scriptParameters, string? nugetSource = null) + // dotnet-script 2.0 makes the isolated assembly load context the default and renames the + // opt-in flag to an opt-out. Isolation is what makes native NuGet assets work (SQLite, + // SkiaSharp, Microsoft.Data.SqlClient), but it also means a type loaded via + // Assembly.LoadFrom is no longer reference-equal to the same type in the script's own + // closure. This flag restores the pre-2.0 behaviour for a step that needs it. + // dotnet-script 1.6.0 ignores the flag, so it is safe to pass to a customer's own + // locally-installed copy as well. + const string DisableIsolatedLoadContextArgument = "--disable-isolated-load-context"; + + public static string FormatCommandArguments(string bootstrapFile, string? scriptParameters, string? nugetSource = null, bool disableIsolatedLoadContext = false) { var (scriptCommandArguments, scriptArguments) = RetrieveParameterValues(scriptParameters); var encryptionKey = Convert.ToBase64String(VariableEncryptor.EncryptionKey); var source = string.IsNullOrWhiteSpace(nugetSource) ? "https://api.nuget.org/v3/index.json" : nugetSource; var commandArguments = new StringBuilder(); commandArguments.Append($"-s {source} "); + if (disableIsolatedLoadContext) commandArguments.Append($"{DisableIsolatedLoadContextArgument} "); if (!string.IsNullOrWhiteSpace(scriptCommandArguments)) commandArguments.Append($"{scriptCommandArguments} "); commandArguments.AppendFormat("\"{0}\" -- {1} \"{2}\"", bootstrapFile, scriptArguments, encryptionKey); return commandArguments.ToString(); diff --git a/source/Calamari.Common/Features/Scripting/DotnetScript/DotnetScriptExecutor.cs b/source/Calamari.Common/Features/Scripting/DotnetScript/DotnetScriptExecutor.cs index 1df9532fc5..3ea8dbd35d 100644 --- a/source/Calamari.Common/Features/Scripting/DotnetScript/DotnetScriptExecutor.cs +++ b/source/Calamari.Common/Features/Scripting/DotnetScript/DotnetScriptExecutor.cs @@ -11,6 +11,9 @@ namespace Calamari.Common.Features.Scripting.DotnetScript { public class DotnetScriptExecutor : ScriptExecutor { + const string DotnetRollForwardVariableName = "DOTNET_ROLL_FORWARD"; + const string RollForwardVariable = "Octopus.Action.Script.CSharp.RollForward"; + readonly ICommandLineRunner commandLineRunner; public DotnetScriptExecutor(ICommandLineRunner commandLineRunner, ILog log): base(log) @@ -33,17 +36,45 @@ protected override IEnumerable PrepareExecution(Script script, var configurationFile = DotnetScriptBootstrapper.PrepareConfigurationFile(workingDirectory, variables); var (bootstrapFile, otherTemporaryFiles) = DotnetScriptBootstrapper.PrepareBootstrapFile(script.File, configurationFile, workingDirectory, variables); var nugetSource = variables.Get("Octopus.Action.Script.CSharp.NuGetSource"); - var arguments = DotnetScriptBootstrapper.FormatCommandArguments(bootstrapFile, script.Parameters, nugetSource); + bool.TryParse(variables.Get("Octopus.Action.Script.CSharp.DisableIsolatedLoadContext", "false"), out var disableIsolatedLoadContext); + var arguments = DotnetScriptBootstrapper.FormatCommandArguments(bootstrapFile, script.Parameters, nugetSource, disableIsolatedLoadContext); bool.TryParse(variables.Get("Octopus.Action.Script.CSharp.BypassIsolation", "false"), out var bypassDotnetScriptIsolation); var cli = CreateCommandLineInvocation(executable, arguments, !string.IsNullOrWhiteSpace(localDotnetScriptPath)); - cli.EnvironmentVars = environmentVars; + cli.EnvironmentVars = WithRollForwardOverride(environmentVars, variables.Get(RollForwardVariable)); cli.WorkingDirectory = workingDirectory; cli.Isolate = !bypassDotnetScriptIsolation; yield return new ScriptExecution(cli, otherTemporaryFiles.Concat(new[] { bootstrapFile, configurationFile })); } + /// + /// The roll-forward default ships in the vendored dotnet-script.runtimeconfig.json + /// (see source/IncludeDotNetScript.targets), which is process-scoped and covers both the + /// Windows and Linux launch paths without Calamari having to do anything. + /// + /// This only handles the per-step override. DOTNET_ROLL_FORWARD sits above the + /// runtimeconfig in the host's precedence order, and unlike the runtimeconfig it also + /// reaches a dotnet-script the customer installed themselves and put on the PATH, which is + /// preferred over our bundled copy. + /// + /// Setting an environment variable leaks it into every process the customer's script goes + /// on to start, so we only do it when a step has explicitly asked for a policy. + /// + static Dictionary? WithRollForwardOverride(Dictionary? environmentVars, string? rollForward) + { + if (string.IsNullOrWhiteSpace(rollForward)) + return environmentVars; + + var vars = environmentVars == null + ? new Dictionary() + : new Dictionary(environmentVars); + + vars[DotnetRollForwardVariableName] = rollForward; + + return vars; + } + private string GetExecutable(string? localDotnetScriptPath, string bundledExecutable) { return string.IsNullOrWhiteSpace(localDotnetScriptPath) 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..dc6ee1c720 --- /dev/null +++ b/source/Calamari.Scripting/DotnetScript/dotnet-script.runtimeconfig.json @@ -0,0 +1,14 @@ +{ + "runtimeOptions": { + "tfm": "net8.0", + "framework": { + "name": "Microsoft.NETCore.App", + "version": "8.0.0" + }, + "configProperties": { + "System.Reflection.Metadata.MetadataUpdater.IsSupported": false, + "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false + }, + "rollForward": "LatestMajor" + } +} diff --git a/source/Calamari.Tests/Fixtures/DotnetScript/DotnetScriptBootstrapperFixture.cs b/source/Calamari.Tests/Fixtures/DotnetScript/DotnetScriptBootstrapperFixture.cs index ba81b3c9a9..30f1a7093d 100644 --- a/source/Calamari.Tests/Fixtures/DotnetScript/DotnetScriptBootstrapperFixture.cs +++ b/source/Calamari.Tests/Fixtures/DotnetScript/DotnetScriptBootstrapperFixture.cs @@ -31,5 +31,30 @@ public void FormatCommandArguments_UsesCustomNuGetSource_WhenProvided() result.Should().Contain($"-s {customSource} "); result.Should().NotContain("api.nuget.org"); } + + [Test] + public void FormatCommandArguments_DoesNotDisableIsolatedLoadContext_ByDefault() + { + var result = DotnetScriptBootstrapper.FormatCommandArguments("Bootstrap.csx", null); + result.Should().NotContain("--disable-isolated-load-context"); + } + + [Test] + public void FormatCommandArguments_DisablesIsolatedLoadContext_WhenRequested() + { + var result = DotnetScriptBootstrapper.FormatCommandArguments("Bootstrap.csx", null, null, true); + result.Should().Contain("--disable-isolated-load-context "); + } + + [Test] + public void FormatCommandArguments_PlacesDisableIsolatedLoadContextBeforeTheScriptFile() + { + // Anything after the bootstrap file is passed to the script, not to dotnet-script. + var bootstrapFile = "Bootstrap.csx"; + var result = DotnetScriptBootstrapper.FormatCommandArguments(bootstrapFile, "--verbosity debug -- \"Parameter 1\"", null, true); + result.IndexOf("--disable-isolated-load-context", StringComparison.Ordinal) + .Should() + .BeLessThan(result.IndexOf($"\"{bootstrapFile}\"", StringComparison.Ordinal)); + } } } \ No newline at end of file diff --git a/source/IncludeDotNetScript.targets b/source/IncludeDotNetScript.targets index 1f6ed73182..e47d96925f 100644 --- a/source/IncludeDotNetScript.targets +++ b/source/IncludeDotNetScript.targets @@ -3,6 +3,27 @@ + + + @@ -10,6 +31,7 @@ + @@ -18,6 +40,7 @@ + - \ No newline at end of file +