Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -33,17 +36,45 @@ protected override IEnumerable<ScriptExecution> 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 }));
}

/// <summary>
/// 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.
/// </summary>
static Dictionary<string, string>? WithRollForwardOverride(Dictionary<string, string>? environmentVars, string? rollForward)
{
if (string.IsNullOrWhiteSpace(rollForward))
return environmentVars;

var vars = environmentVars == null
? new Dictionary<string, string>()
: new Dictionary<string, string>(environmentVars);

vars[DotnetRollForwardVariableName] = rollForward;

return vars;
}

private string GetExecutable(string? localDotnetScriptPath, string bundledExecutable)
{
return string.IsNullOrWhiteSpace(localDotnetScriptPath)
Expand Down
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
}
25 changes: 24 additions & 1 deletion source/IncludeDotNetScript.targets
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,35 @@
<Target Name="GetToolFiles" DependsOnTargets="RunResolvePackageDependencies">
<ItemGroup>
<DotnetScriptFiles Include="$(MSBuildProjectDirectory)/../Calamari.Scripting/DotnetScript/dotnet-script.*.zip"/>

<!--
dotnet-script is a framework-dependent application. The zip we vendor requests
Microsoft.NETCore.App 8.0.0 and declares no rollForward of its own, so the default
policy is Minor: any 8.x, and nothing higher. On a target that has only a newer
runtime installed it therefore fails to launch with
"You must install or update .NET to run this application" (exit 150).

We overwrite its runtimeconfig.json with a copy carrying rollForward: LatestMajor,
which selects the newest installed runtime and degrades silently to 8.x when nothing
newer is present. The host reads this file on every launch path - Linux
`dotnet "<path>/dotnet-script.dll"` and Windows `dotnet-script.cmd` alike - so this
covers both platforms with no change to how Calamari invokes the tool.

Precedence is command line > DOTNET_ROLL_FORWARD > runtimeconfig, so this is a
default that a step can still override via Octopus.Action.Script.CSharp.RollForward.

Keep this file in sync when bumping the vendored zip: it is a verbatim copy of the
zip's own dotnet-script.runtimeconfig.json plus the rollForward property.
-->
<DotnetScriptRuntimeConfig Include="$(MSBuildProjectDirectory)/../Calamari.Scripting/DotnetScript/dotnet-script.runtimeconfig.json"/>
</ItemGroup>
</Target>
<Target Name="CopyDotnetScriptFilesAfterBuild" AfterTargets="Build" DependsOnTargets="GetToolFiles">
<ItemGroup>
<DotnetScriptFilesExe Include="$(OutputPath)/dotnet-script/*.sh;$(OutputPath)/dotnet-script/*.exe"/>
</ItemGroup>
<Unzip SourceFiles="@(DotnetScriptFiles)" DestinationFolder="$(OutputPath)/" />
<Copy SourceFiles="@(DotnetScriptRuntimeConfig)" DestinationFolder="$(OutputPath)/dotnet-script/" />
<Exec Command="chmod +x %(DotnetScriptFilesExe.Identity)" IgnoreExitCode="true" Condition="!$([MSBuild]::IsOSPlatform('Windows'))"/>
</Target>

Expand All @@ -18,6 +40,7 @@
<DotnetScriptFilesExe Include="$(PublishDir)/dotnet-script/*.sh;$(PublishDir)/dotnet-script/*.exe"/>
</ItemGroup>
<Unzip SourceFiles="@(DotnetScriptFiles)" DestinationFolder="$(PublishDir)/" />
<Copy SourceFiles="@(DotnetScriptRuntimeConfig)" DestinationFolder="$(PublishDir)/dotnet-script/" />
<Exec Command="chmod +x %(DotnetScriptFilesExe.Identity)" IgnoreExitCode="true" Condition="!$([MSBuild]::IsOSPlatform('Windows'))"/>
</Target>
</Project>
</Project>