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 @@ -23,8 +23,7 @@ See: https://github.com/dotnet/sdk/pull/52581
<!--
_AndroidConfigureHotReloadEnvironment:
Configures Hot Reload when dotnet-watch passes environment variables via @(RuntimeEnvironmentVariable).
- Adds the Hot Reload agent DLL as a reference so it gets deployed
- Updates DOTNET_STARTUP_HOOKS to use just the assembly name (not the full path)
- Adds the Hot Reload startup hook assembly name to @(_AndroidDotnetStartupHooks)
- Sets up STARTUP_HOOKS via RuntimeHostConfigurationOption for MonoVM
-->
<Target Name="_AndroidConfigureHotReloadEnvironment"
Expand All @@ -40,13 +39,9 @@ See: https://github.com/dotnet/sdk/pull/52581
<_AndroidHotReloadAgentAssemblyName>$([System.IO.Path]::GetFileNameWithoutExtension('$(_AndroidHotReloadAgentAssemblyPath)'))</_AndroidHotReloadAgentAssemblyName>
</PropertyGroup>

<!--
Update DOTNET_STARTUP_HOOKS in @(RuntimeEnvironmentVariable) to use just the assembly name.
The full path doesn't work on Android since the DLL is deployed alongside the app.
-->
<!-- The full path doesn't work on Android since the DLL is deployed alongside the app. -->
<ItemGroup Condition=" '$(_AndroidHotReloadAgentAssemblyName)' != '' ">
<RuntimeEnvironmentVariable Remove="DOTNET_STARTUP_HOOKS" />
<RuntimeEnvironmentVariable Include="DOTNET_STARTUP_HOOKS" Value="$(_AndroidHotReloadAgentAssemblyName)" />
<_AndroidDotnetStartupHooks Include="$(_AndroidHotReloadAgentAssemblyName)" />
<!-- Set STARTUP_HOOKS via RuntimeHostConfigurationOption for MonoVM (read by Mono runtime) -->
<RuntimeHostConfigurationOption Include="STARTUP_HOOKS" Value="$(_AndroidHotReloadAgentAssemblyName)" Condition=" '$(UseMonoRuntime)' == 'true' " />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ namespace Xamarin.Android.Build.Tests
[Parallelizable (ParallelScope.Children)]
public class EnvironmentContentTests : BaseTest
{
class StartupHookEnvironmentProject : XamarinAndroidApplicationProject
{
protected override string ExtraDirectoryBuildTargetsContent => """
<ItemGroup>
<RuntimeEnvironmentVariable Include="DOTNET_STARTUP_HOOKS" Value="$(MSBuildProjectDirectory)/hotreload/Microsoft.Extensions.DotNetDeltaApplier.dll" />
</ItemGroup>
""";
}

[Test]
[NonParallelizable]
public void BuildApplicationWithMonoEnvironment ([Values ("", "Normal", "Offline")] string sequencePointsMode, [Values] AndroidRuntime runtime)
Expand Down Expand Up @@ -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<EnvironmentHelper.EnvironmentFile> envFiles = EnvironmentHelper.GatherEnvironmentFiles (intermediateOutputDir, supportedAbis, true, AndroidRuntime.MonoVM);
Dictionary<string, string> 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)
{
Expand Down
33 changes: 31 additions & 2 deletions src/Xamarin.Android.Build.Tasks/Utilities/EnvironmentBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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<string> ();
var seenHooks = new HashSet<string> (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 ("\"", "\\\"");
}
26 changes: 25 additions & 1 deletion src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets
Original file line number Diff line number Diff line change
Expand Up @@ -1587,7 +1587,31 @@ because xbuild doesn't support framework reference assemblies.
</PrepareAbiItems>
</Target>

<Target Name="_GenerateEnvironmentFiles" DependsOnTargets="_AndroidConfigureHotReloadEnvironment">
<Target Name="_ComposeDotNetStartupHooks"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This target does not have Inputs and Outputs, should it have them?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. I kept _ComposeDotNetStartupHooks without Inputs/Outputs intentionally because it only transforms in-memory MSBuild items/properties for the current build graph; persisting incremental state there could skip needed recomposition. I also moved the final cross-file merge to EnvironmentBuilder in 120aedf, so this target stays lightweight and order-safe.

DependsOnTargets="_AndroidConfigureHotReloadEnvironment">
<ItemGroup>
<_AndroidStartupHooksFromRuntimeEnvironmentVariable Include="@(RuntimeEnvironmentVariable->WithMetadataValue('Identity', 'DOTNET_STARTUP_HOOKS'))" />
<_AndroidStartupHooksFromRuntimeEnvironmentVariable Remove="@(_AndroidStartupHooksFromRuntimeEnvironmentVariable)"
Condition=" '$(_AndroidHotReloadAgentAssemblyPath)' != '' And '%(_AndroidStartupHooksFromRuntimeEnvironmentVariable.Value)' == '$(_AndroidHotReloadAgentAssemblyPath)' " />
<_AndroidDotnetStartupHooks Include="@(_AndroidStartupHooksFromRuntimeEnvironmentVariable->'%(Value)')" />
</ItemGroup>

<RemoveDuplicates Inputs="@(_AndroidDotnetStartupHooks)">
<Output TaskParameter="Filtered" ItemName="_AndroidDotnetStartupHooksDistinct" />
</RemoveDuplicates>

<CreateProperty Value="@(_AndroidDotnetStartupHooksDistinct,':')">
<Output TaskParameter="Value" PropertyName="_AndroidDotnetStartupHooksValue" />
</CreateProperty>

<ItemGroup>
<RuntimeEnvironmentVariable Remove="DOTNET_STARTUP_HOOKS" />
<RuntimeEnvironmentVariable Include="DOTNET_STARTUP_HOOKS" Value="$(_AndroidDotnetStartupHooksValue)"
Condition=" '$(_AndroidDotnetStartupHooksValue)' != '' " />
</ItemGroup>
</Target>

<Target Name="_GenerateEnvironmentFiles" DependsOnTargets="_ComposeDotNetStartupHooks">
<ItemGroup>
<_GeneratedAndroidEnvironment Include="mono.enable_assembly_preload=0" Condition=" '$(AndroidEnablePreloadAssemblies)' != 'True' " />
<_GeneratedAndroidEnvironment Include="DOTNET_MODIFIABLE_ASSEMBLIES=Debug" Condition=" '$(AndroidIncludeDebugSymbols)' == 'true' and '$(AndroidUseInterpreter)' == 'true' " />
Expand Down