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 @@ -234,6 +234,7 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android.

<GenerateNativeAotEnvironmentAssemblerSources
Environments="@(AndroidEnvironment);@(LibraryEnvironments)"
DotNetStartupHooks="@(_AndroidDotnetStartupHooksDistinct)"
HttpClientHandlerType="$(AndroidHttpClientHandlerType)"
OutputSources="@(_PrivateEnvironmentAssemblySource)"
RID="$(RuntimeIdentifier)" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
AdditionalProviderSources="@(_AdditionalProviderSources)"
AndroidRuntime="$(_AndroidRuntime)"
Environments="@(_EnvironmentFiles)"
DotNetStartupHooks="@(_AndroidDotnetStartupHooksDistinct)"
HttpClientHandlerType="$(AndroidHttpClientHandlerType)"
EnableSGenConcurrent="$(AndroidEnableSGenConcurrent)"
CodeGenerationTarget="$(_AndroidJcwCodegenTarget)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class GenerateAdditionalProviderSources : AndroidTask
public string TargetName { get; set; } = "";

public ITaskItem[]? Environments { get; set; }
public ITaskItem[]? DotNetStartupHooks { get; set; }

// We need to pass these two to the environment builder, otherwise not used
// by this task. See also GenerateNativeApplicationSources.cs
Expand Down Expand Up @@ -92,6 +93,7 @@ void Generate (NativeCodeGenStateObject codeGenState)
// We care only about environment variables here
var envBuilder = new EnvironmentBuilder (Log);
envBuilder.Read (Environments);
envBuilder.AddDotNetStartupHooks (DotNetStartupHooks);
GenerateNativeApplicationConfigSources.AddDefaultEnvironmentVariables (envBuilder, HttpClientHandlerType, EnableSGenConcurrent);

var envVarNames = new StringBuilder ();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ public class GenerateNativeAotEnvironmentAssemblerSources : AndroidTask
[Required]
public string RID { get; set; } = "";
public ITaskItem[]? Environments { get; set; }
public ITaskItem[]? DotNetStartupHooks { get; set; }
public string? HttpClientHandlerType { get; set; }

public override bool RunTask ()
{
var envBuilder = new EnvironmentBuilder (Log);
envBuilder.Read (Environments);
envBuilder.AddDotNetStartupHooks (DotNetStartupHooks);

// Environment variables are set by Java (code generated in the GenerateAdditionalProviderSources task)
// We still want to set system properties, if any
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public class GenerateNativeApplicationConfigSources : AndroidTask
public string? PackageNamingPolicy { get; set; }
public string? Debug { get; set; }
public ITaskItem[]? Environments { get; set; }
public ITaskItem[]? DotNetStartupHooks { get; set; }
public string? AndroidAotMode { get; set; }
public bool AndroidAotEnableLazyLoad { get; set; }
public bool EnableLLVM { get; set; }
Expand Down Expand Up @@ -113,6 +114,7 @@ public override bool RunTask ()
// files (generated by us) which weren't present by the time GeneratJavaStubs ran.
var envBuilder = new EnvironmentBuilder (Log, EnablePreloadAssembliesDefault, sequencePointsMode);
envBuilder.Read (Environments);
envBuilder.AddDotNetStartupHooks (DotNetStartupHooks);

if (_Debug) {
envBuilder.AddDefaultDebugBuildLogLevel ();
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
56 changes: 54 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 @@ -48,12 +50,42 @@ public void Read (ITaskItem[]? envItems)
}
}

public void AddDotNetStartupHooks (ITaskItem[]? startupHooks)
{
if (startupHooks == null || startupHooks.Length == 0) {
return;
}

var values = new List<string> (startupHooks.Length);
foreach (ITaskItem hook in startupHooks) {
if (hook.ItemSpec.IsNullOrWhiteSpace ()) {
continue;
}

string value = hook.ItemSpec.Trim ();
values.Add (value);
}

if (values.Count == 0) {
return;
}

AddEnvironmentVariable (DotNetStartupHooks, String.Join (":", values));
}

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 +140,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 ("\"", "\\\"");
}
23 changes: 22 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,11 +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>

</Target>

<Target Name="_GenerateEnvironmentFiles" DependsOnTargets="_ComposeDotNetStartupHooks">
<PropertyGroup>
<_AndroidDotnetStartupHooksValue>@(_AndroidDotnetStartupHooksDistinct,':')</_AndroidDotnetStartupHooksValue>
</PropertyGroup>
<ItemGroup>
<RuntimeEnvironmentVariable Remove="DOTNET_STARTUP_HOOKS" />
<_GeneratedAndroidEnvironment Include="mono.enable_assembly_preload=0" Condition=" '$(AndroidEnablePreloadAssemblies)' != 'True' " />
<_GeneratedAndroidEnvironment Include="DOTNET_MODIFIABLE_ASSEMBLIES=Debug" Condition=" '$(AndroidIncludeDebugSymbols)' == 'true' and '$(AndroidUseInterpreter)' == 'true' " />
<_GeneratedAndroidEnvironment Include="DOTNET_DiagnosticPorts=$(DiagnosticConfiguration)" Condition=" '$(DiagnosticConfiguration)' != '' " />
<_GeneratedAndroidEnvironment Include="DOTNET_STARTUP_HOOKS=$(_AndroidDotnetStartupHooksValue)" Condition=" '$(_AndroidDotnetStartupHooksValue)' != '' " />
<!-- RuntimeEnvironmentVariable items come from 'dotnet run -e NAME=VALUE' -->
<_GeneratedAndroidEnvironment Include="@(RuntimeEnvironmentVariable->'%(Identity)=%(Value)')" />
</ItemGroup>
Expand Down Expand Up @@ -1734,6 +1754,7 @@ because xbuild doesn't support framework reference assemblies.
MonoComponents="@(_MonoComponent)"
EnvironmentOutputDirectory="$(IntermediateOutputPath)android"
Environments="@(_EnvironmentFiles)"
DotNetStartupHooks="@(_AndroidDotnetStartupHooksDistinct)"
AndroidAotMode="$(AndroidAotMode)"
AndroidAotEnableLazyLoad="$(AndroidAotEnableLazyLoad)"
EnableLLVM="$(EnableLLVM)"
Expand Down