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
8 changes: 5 additions & 3 deletions src/Mono.Android/Android.Runtime/JNIEnvInit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,11 @@ static void RunStartupHooks ()
return;
}

// Pass empty string for diagnosticStartupHooks parameter
// The method will read STARTUP_HOOKS from AppContext internally
method.Invoke (null, [ "" ]);
// ProcessStartupHooks accepts startup hooks directly via parameter.
// It will also read STARTUP_HOOKS from AppContext internally.
// Pass DOTNET_STARTUP_HOOKS env var value so it works without needing AppContext setup.
string? startupHooks = Environment.GetEnvironmentVariable ("DOTNET_STARTUP_HOOKS");
method.Invoke (null, [ startupHooks ?? "" ]);
}

static void SetSynchronizationContext () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ This file is imported *after* the Microsoft.NET.Sdk/Sdk.targets.
<Import Project="Microsoft.Android.Sdk.Publish.targets" />
<Import Project="Microsoft.Android.Sdk.RuntimeConfig.targets" />
<Import Project="Microsoft.Android.Sdk.Tooling.targets" />
<Import Project="Microsoft.Android.Sdk.HotReload.targets" Condition=" '$(AndroidApplication)' == 'true' " />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<!--
***********************************************************************************************
Microsoft.Android.Sdk.HotReload.targets
Copy link
Member Author

Choose a reason for hiding this comment

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

@rolfbjarne do you see anything problematic in this file for iOS? $(DotNetHotReloadAgentStartupHook) will have the full path to the .dll.

Copy link
Member

Choose a reason for hiding this comment

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

The solution for iOS will be rather different, but it doesn't look impossible.


This file contains targets for Hot Reload support in .NET for Android.
These targets are invoked by dotnet-watch/IDE when Hot Reload starts.

See: https://github.com/dotnet/sdk/issues/52492

***********************************************************************************************
-->

<Project>

<!-- Always enable startup hooks support when Hot Reload agent is configured -->
<PropertyGroup Condition=" '$(DotNetHotReloadAgentStartupHook)' != '' ">
<StartupHookSupport>true</StartupHookSupport>
</PropertyGroup>

<!--
_AndroidHotReloadAgentConfiguration:
Configures the Hot Reload agent by:
a) Adding the Hot Reload agent DLL as a reference
b) Setting up STARTUP_HOOKS via RuntimeHostConfigurationOption (for MonoVM)
c) Setting up DOTNET_STARTUP_HOOKS environment variable (for CoreCLR)
d) Adding additional environment variables from DotNetHotReloadAgentEnvironment
-->
<Target Name="_AndroidHotReloadAgentConfiguration"
Condition=" '$(DotNetHotReloadAgentStartupHook)' != '' ">

<PropertyGroup>
<_HotReloadAgentAssemblyName>$([System.IO.Path]::GetFileNameWithoutExtension('$(DotNetHotReloadAgentStartupHook)'))</_HotReloadAgentAssemblyName>
</PropertyGroup>

<!-- Add the Hot Reload agent DLL as a reference so it gets deployed -->
<ItemGroup>
<Reference Include="$(DotNetHotReloadAgentStartupHook)" Condition=" Exists('$(DotNetHotReloadAgentStartupHook)') " />
Copy link
Member

Choose a reason for hiding this comment

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

Does this still work if the app is trimmed? What if the hot reload agent assembly contains references to BCL APIs that were trimmed away?

Copy link
Member Author

Choose a reason for hiding this comment

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

They have the BCL part setup here:

I also did this in a test project that seems to work in all runtimes/configurations:

<TrimmerRootAssembly Include="StartupHook" RootMode="All" />

We might have to do this, if Debug builds are trimmed on iOS.

Copy link
Member

Choose a reason for hiding this comment

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

Hot Reload isn't supported for trimmed apps.

Copy link
Member Author

Choose a reason for hiding this comment

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

My usage of @(TrimmerRootAssembly) was to test startup hooks in general.

But yeah, hot reload, would break randomly if they decided to use System.Guid, for example, during a reload and it was trimmed away.

iOS Debug builds run the trimmer, but I bet it is setup to preserve almost everything. We may need to review that behavior.

@rolfbjarne it's also worth noting we are mostly targeting CoreCLR for this -- if that helps anything.

</ItemGroup>

<!-- Set STARTUP_HOOKS via RuntimeHostConfigurationOption for MonoVM (read by Mono runtime) -->
<ItemGroup Condition=" '$(UseMonoRuntime)' == 'true' ">
<RuntimeHostConfigurationOption Include="STARTUP_HOOKS" Value="$(_HotReloadAgentAssemblyName)" />
</ItemGroup>

<!-- Generate Hot Reload environment file for other runtimes -->
<ItemGroup>
<_HotReloadEnvironment Include="DOTNET_STARTUP_HOOKS=$(_HotReloadAgentAssemblyName)" />
</ItemGroup>

<!-- <DotNetHotReloadAgentEnvironment Include="<name>" Value="<value>"/> -->
<ItemGroup>
<_HotReloadEnvironment Include="@(DotNetHotReloadAgentEnvironment->'%(Identity)=%(Value)')" />
</ItemGroup>

<WriteLinesToFile
File="$(IntermediateOutputPath)__hotreload_environment__.txt"
Lines="@(_HotReloadEnvironment)"
Overwrite="True"
WriteOnlyWhenDifferent="True"
/>

<ItemGroup>
<AndroidEnvironment Include="$(IntermediateOutputPath)__hotreload_environment__.txt" />
<FileWrites Include="$(IntermediateOutputPath)__hotreload_environment__.txt" />
</ItemGroup>

</Target>

<!--
DeployHotReloadAgentConfiguration:
Entry point target invoked by dotnet-watch/IDE.
Sets up the Hot Reload agent configuration and deploys to the device.

This target is discovered by checking if the project has a target named
"DeployHotReloadAgentConfiguration". If found, dotnet-watch/IDE will
set the following:
- DotNetHotReloadAgentStartupHook (Property): Path to Microsoft.Extensions.DotNetDeltaApplier.dll
- DotNetHotReloadAgentEnvironment (ItemGroup): Environment variables as <DotNetHotReloadAgentEnvironment Include="name" Value="value" />
-->
<Target Name="DeployHotReloadAgentConfiguration"
Copy link
Member

Choose a reason for hiding this comment

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

The caller of this target will have to set the Device property, so that we know which device/simulator to deploy to.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, after we have the initial part working, I hope to share the code with dotnet-run that will prompt for a target framework and device. So, you should get the same prompts and property.

Copy link
Member

Choose a reason for hiding this comment

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

Hot Reload is typically used from an IDE though, and the IDE in question must pass the selected device in this case.

DependsOnTargets="_AndroidHotReloadAgentConfiguration;DeployToDevice" />
Copy link
Member

Choose a reason for hiding this comment

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

Does DeployToDevice run the Build target? Or does it just deploy a previously built app?

Because adding anything to RuntimeHostConfigurationOption is useless unless the app is (re)built, at least for us.

Copy link
Member

Choose a reason for hiding this comment

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

Does DeployToDevice run the Build target? Or does it just deploy a previously built app?

I just looked at Android's code, and it looks like you effectively end up modifying the apk when running DeployToDevice.

This seems a bit wasteful for us, because this is what would end up happening:

  1. dotnet build: build everything, create the app bundle, sign the app bundle
  2. dotnet build -t:DeployHotReloadAgentConfiguration: build incrementally, which would modify the app bundle, sign the app bundle
  3. dotnet run: deploy & launch the app

Note how the app is signed twice here, which is not ideal when we're supposedly in a fast path for the debug loop.

I would propose something else:

  1. dotnet build -p:EnableHotReloadConfiguration=true -p:OtherHotReloadProperties=...
  2. dotnet run

This way we only sign the app bundle once.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

DeployToDevice is supposed to skip nearly everything, the new plan dotnet run -e FOO=BAR would be able pass in env vars.

We have an option to "fast deploy" env vars, and so if something isn't working there or not skipping, it is a bug.


</Project>

104 changes: 104 additions & 0 deletions tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1582,5 +1582,109 @@ public void AppStartsWithManagedMarshalMethodsLookupEnabled ()
Path.Combine (Root, builder.ProjectDirectory, "logcat.log"), 30);
Assert.IsTrue (didLaunch, "Activity should have started.");
}

[Test]
public void DeployHotReloadAgentConfiguration ()
{
// Create a library project that contains the startup hook
var startupHookLib = new XamarinAndroidLibraryProject {
ProjectName = "MyStartupHook",
Sources = {
new BuildItem.Source ("StartupHook.cs") {
TextContent = () => @"
using System;

internal static class StartupHook
{
public static void Initialize ()
{
Console.WriteLine (""HOTRELOAD_TEST_HOOK_INITIALIZED=true"");
}
}
"
}
}
};

var proj = new XamarinAndroidApplicationProject {
ProjectName = nameof (DeployHotReloadAgentConfiguration),
IsRelease = false,
Imports = {
// Add a .targets file that simulates what dotnet-watch/IDE would inject
new Import (() => "HotReload.targets") {
TextContent = () => @"<Project xmlns=""http://schemas.microsoft.com/developer/msbuild/2003"">
<PropertyGroup>
<DotNetHotReloadAgentStartupHook>MyStartupHook</DotNetHotReloadAgentStartupHook>
</PropertyGroup>
<ItemGroup>
<DotNetHotReloadAgentEnvironment Include=""HOTRELOAD_TEST_VAR"" Value=""TestValue123"" />
<DotNetHotReloadAgentEnvironment Include=""ANOTHER_VAR"" Value=""AnotherValue456"" />
</ItemGroup>
</Project>"
},
}
};
proj.SetRuntime (AndroidRuntime.CoreCLR);
proj.AddReference (startupHookLib);
proj.MainActivity = proj.DefaultMainActivity.Replace ("//${AFTER_ONCREATE}", @"
Console.WriteLine (""DOTNET_STARTUP_HOOKS="" + Environment.GetEnvironmentVariable(""DOTNET_STARTUP_HOOKS""));
Console.WriteLine (""HOTRELOAD_TEST_VAR="" + Environment.GetEnvironmentVariable(""HOTRELOAD_TEST_VAR""));
Console.WriteLine (""ANOTHER_VAR="" + Environment.GetEnvironmentVariable(""ANOTHER_VAR""));
");

var rootPath = Path.Combine (Root, "temp", TestName);
using var libBuilder = CreateDllBuilder (Path.Combine (rootPath, startupHookLib.ProjectName));
Assert.IsTrue (libBuilder.Build (startupHookLib), "Library build should have succeeded.");

using var builder = CreateApkBuilder (Path.Combine (rootPath, proj.ProjectName));
builder.Save (proj);

var projectDirectory = Path.Combine (rootPath, proj.ProjectName);
var dotnet = new DotNetCLI (Path.Combine (projectDirectory, proj.ProjectFilePath));

// Build normally first
Assert.IsTrue (dotnet.Build (), "`dotnet build` should succeed");

// Run the DeployHotReloadAgentConfiguration target (hot reload properties come from HotReload.targets)
Assert.IsTrue (dotnet.Build (target: "DeployHotReloadAgentConfiguration"),
"`dotnet build -t:DeployHotReloadAgentConfiguration` should succeed");

// Launch the app using adb
ClearAdbLogcat ();
var result = AdbStartActivity ($"{proj.PackageName}/{proj.JavaPackageName}.MainActivity");
Assert.IsTrue (result.Contains ("Starting: Intent"), $"Activity should have launched. adb output:\n{result}");

bool didLaunch = WaitForActivityToStart (proj.PackageName, "MainActivity",
Path.Combine (projectDirectory, "logcat.log"), 30);
Assert.IsTrue (didLaunch, "Activity should have started.");

var logcatOutput = File.ReadAllText (Path.Combine (projectDirectory, "logcat.log"));

// Verify the startup hook was set via DOTNET_STARTUP_HOOKS
StringAssert.Contains (
"DOTNET_STARTUP_HOOKS=MyStartupHook",
logcatOutput,
"DOTNET_STARTUP_HOOKS should be set to MyStartupHook"
);

// Verify the startup hook was called
StringAssert.Contains (
"HOTRELOAD_TEST_HOOK_INITIALIZED=true",
logcatOutput,
"StartupHook.Initialize() should have been called"
);

// Verify the additional env vars from DotNetHotReloadAgentEnvironment were set
StringAssert.Contains (
"HOTRELOAD_TEST_VAR=TestValue123",
logcatOutput,
"HOTRELOAD_TEST_VAR should be set from DotNetHotReloadAgentEnvironment"
);
StringAssert.Contains (
"ANOTHER_VAR=AnotherValue456",
logcatOutput,
"ANOTHER_VAR should be set from DotNetHotReloadAgentEnvironment"
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,12 @@
<StartupHookSupport>true</StartupHookSupport>
</PropertyGroup>

<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<ItemGroup>
<!-- trying to track:
JNI ERROR (app bug): accessed deleted Global 0x3056
-->
<AndroidEnvironment Include="env.txt" />
<AndroidEnvironment Include="env.txt" Condition=" '$(Configuration)' == 'Debug' " />
<AndroidEnvironment Include="hotreload.env" />
</ItemGroup>

<ItemGroup>
Expand Down Expand Up @@ -231,7 +232,8 @@
<RuntimeHostConfigurationOption Include="test_bool" Value="true" />
<RuntimeHostConfigurationOption Include="test_integer" Value="42" />
<RuntimeHostConfigurationOption Include="test_string" Value="foo" />
<RuntimeHostConfigurationOption Include="STARTUP_HOOKS" Value="StartupHook" />
<!-- Set STARTUP_HOOKS via RuntimeHostConfigurationOption for MonoVM and NativeAOT (read via AppContext.GetData) -->
<RuntimeHostConfigurationOption Include="STARTUP_HOOKS" Value="StartupHook" Condition=" '$(UseMonoRuntime)' == 'true' or '$(PublishAot)' == 'true' " />
Comment on lines +235 to +236
Copy link
Member Author

Choose a reason for hiding this comment

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

Filed this to fix NativeAOT later:

</ItemGroup>

<ItemGroup Condition=" '$(AndroidPackageFormat)' != 'aab' ">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ namespace SystemTests
[TestFixture]
public class StartupHookTest
{
[Test]
public void FeatureFlagIsEnabled ()
{
// NOTE: this is set to true in tests\Mono.Android-Tests\Mono.Android-Tests\Mono.Android.NET-Tests.csproj
Assert.IsTrue (Microsoft.Android.Runtime.RuntimeFeature.StartupHookSupport, "RuntimeFeature.StartupHookSupport should be true");
}

[Test]
public void EnvironmentVariableIsSet ()
{
var value = Environment.GetEnvironmentVariable ("DOTNET_STARTUP_HOOKS");
Assert.AreEqual ("StartupHook", value, "DOTNET_STARTUP_HOOKS should be set to 'StartupHook'");
}

[Test]
public void IsInitialized ()
{
Expand Down
1 change: 0 additions & 1 deletion tests/Mono.Android-Tests/Mono.Android-Tests/env.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# Environment Variables and system properties
# debug.mono.log=gref,default
debug.mono.debug=1
DOTNET_STARTUP_HOOKS=StartupHook
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DOTNET_STARTUP_HOOKS=StartupHook
Copy link
Member Author

Choose a reason for hiding this comment

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

Note this wasn't being used in some of our on-device tests, because we had:

<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
    <!-- trying to track:
      JNI ERROR (app bug): accessed deleted Global 0x3056
    -->
    <AndroidEnvironment Include="env.txt" />

I just made a new file for this.