diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.HotReload.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.HotReload.targets index a1f29e119d7..b1ef8ccaff8 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.HotReload.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.HotReload.targets @@ -23,8 +23,7 @@ See: https://github.com/dotnet/sdk/pull/52581 $([System.IO.Path]::GetFileNameWithoutExtension('$(_AndroidHotReloadAgentAssemblyPath)')) - + - - + <_AndroidDotnetStartupHooks Include="$(_AndroidHotReloadAgentAssemblyName)" /> diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/EnvironmentContentTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/EnvironmentContentTests.cs index 1f5d51ef909..e72fdffa481 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/EnvironmentContentTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/EnvironmentContentTests.cs @@ -14,6 +14,15 @@ namespace Xamarin.Android.Build.Tests [Parallelizable (ParallelScope.Children)] public class EnvironmentContentTests : BaseTest { + class StartupHookEnvironmentProject : XamarinAndroidApplicationProject + { + protected override string ExtraDirectoryBuildTargetsContent => """ + + + + """; + } + [Test] [NonParallelizable] public void BuildApplicationWithMonoEnvironment ([Values ("", "Normal", "Offline")] string sequencePointsMode, [Values] AndroidRuntime runtime) @@ -161,6 +170,41 @@ public void CheckConcurrentGC () } } + [Test] + public void DotNetStartupHooksAreMergedFromRuntimeEnvironmentVariableAndAndroidEnvironment () + { + const string supportedAbis = "armeabi-v7a;x86"; + + var proj = new StartupHookEnvironmentProject { + IsRelease = false, + OtherBuildItems = { + new BuildItem.NoActionResource ("hotreload/Microsoft.Extensions.DotNetDeltaApplier.dll") { + BinaryContent = () => [], + }, + new AndroidItem.AndroidEnvironment ("startup.env") { + TextContent = () => "DOTNET_STARTUP_HOOKS=MyStartupHook.dll", + }, + }, + }; + proj.SetRuntime (AndroidRuntime.MonoVM); + proj.SetAndroidSupportedAbis (supportedAbis); + + using (var b = CreateApkBuilder ()) { + Assert.IsTrue (b.Build (proj), "Build should have succeeded."); + + string intermediateOutputDir = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath); + List envFiles = EnvironmentHelper.GatherEnvironmentFiles (intermediateOutputDir, supportedAbis, true, AndroidRuntime.MonoVM); + Dictionary envvars = EnvironmentHelper.ReadEnvironmentVariables (envFiles, AndroidRuntime.MonoVM); + + Assert.IsTrue (envvars.TryGetValue ("DOTNET_STARTUP_HOOKS", out string startupHooks), "Environment should contain DOTNET_STARTUP_HOOKS"); + CollectionAssert.AreEquivalent ( + new [] { "Microsoft.Extensions.DotNetDeltaApplier", "MyStartupHook.dll" }, + startupHooks.Split (':'), + "DOTNET_STARTUP_HOOKS should merge values from RuntimeEnvironmentVariable and AndroidEnvironment." + ); + } + } + [Test] public void CheckForInvalidHttpClientHandlerType ([Values] AndroidRuntime runtime) { diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/EnvironmentBuilder.cs b/src/Xamarin.Android.Build.Tasks/Utilities/EnvironmentBuilder.cs index 51121c052bf..95d3e91733c 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/EnvironmentBuilder.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/EnvironmentBuilder.cs @@ -7,6 +7,8 @@ namespace Xamarin.Android.Tasks; class EnvironmentBuilder { + const string DotNetStartupHooks = "DOTNET_STARTUP_HOOKS"; + static readonly string[] defaultLogLevel = {"MONO_LOG_LEVEL", "info"}; static readonly string[] defaultMonoDebug = {"MONO_DEBUG", "gen-compact-seq-points"}; static readonly string defaultHttpMessageHandler = "System.Net.Http.HttpClientHandler, System.Net.Http"; @@ -50,10 +52,17 @@ public void Read (ITaskItem[]? envItems) public void AddEnvironmentVariable (string name, string value) { + string escapedName = ValidAssemblerString (name); + string escapedValue = ValidAssemblerString (value); + if (Char.IsUpper(name [0]) || !Char.IsLetter(name [0])) { - environmentVariables [ValidAssemblerString (name)] = ValidAssemblerString (value); + if (name == DotNetStartupHooks && environmentVariables.TryGetValue (escapedName, out string? existingStartupHooks)) { + environmentVariables [escapedName] = MergeDotNetStartupHooks (existingStartupHooks, escapedValue); + } else { + environmentVariables [escapedName] = escapedValue; + } } else { - systemProperties [ValidAssemblerString (name)] = ValidAssemblerString (value); + systemProperties [escapedName] = escapedValue; } } @@ -108,5 +117,25 @@ public void AddMonoGcParams (bool enableSgenConcurrent) AddEnvironmentVariable ("MONO_GC_PARAMS", enableSgenConcurrent ? "major=marksweep-conc" : "major=marksweep"); } + static string MergeDotNetStartupHooks (string first, string second) + { + var mergedHooks = new List (); + var seenHooks = new HashSet (StringComparer.Ordinal); + + foreach (string hook in first.Split (new char [] { ':' }, StringSplitOptions.RemoveEmptyEntries)) { + if (seenHooks.Add (hook)) { + mergedHooks.Add (hook); + } + } + + foreach (string hook in second.Split (new char [] { ':' }, StringSplitOptions.RemoveEmptyEntries)) { + if (seenHooks.Add (hook)) { + mergedHooks.Add (hook); + } + } + + return String.Join (":", mergedHooks); + } + static string ValidAssemblerString (string s) => s.Replace ("\\", "\\\\").Replace ("\"", "\\\""); } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 1f37f9198bc..7e21ca0b27c 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1587,7 +1587,31 @@ because xbuild doesn't support framework reference assemblies. - + + + <_AndroidStartupHooksFromRuntimeEnvironmentVariable Include="@(RuntimeEnvironmentVariable->WithMetadataValue('Identity', 'DOTNET_STARTUP_HOOKS'))" /> + <_AndroidStartupHooksFromRuntimeEnvironmentVariable Remove="@(_AndroidStartupHooksFromRuntimeEnvironmentVariable)" + Condition=" '$(_AndroidHotReloadAgentAssemblyPath)' != '' And '%(_AndroidStartupHooksFromRuntimeEnvironmentVariable.Value)' == '$(_AndroidHotReloadAgentAssemblyPath)' " /> + <_AndroidDotnetStartupHooks Include="@(_AndroidStartupHooksFromRuntimeEnvironmentVariable->'%(Value)')" /> + + + + + + + + + + + + + + + + + <_GeneratedAndroidEnvironment Include="mono.enable_assembly_preload=0" Condition=" '$(AndroidEnablePreloadAssemblies)' != 'True' " /> <_GeneratedAndroidEnvironment Include="DOTNET_MODIFIABLE_ASSEMBLIES=Debug" Condition=" '$(AndroidIncludeDebugSymbols)' == 'true' and '$(AndroidUseInterpreter)' == 'true' " />