diff --git a/dotnet/targets/Xamarin.Shared.Sdk.targets b/dotnet/targets/Xamarin.Shared.Sdk.targets
index e5e5b4d5f571..186244756bc7 100644
--- a/dotnet/targets/Xamarin.Shared.Sdk.targets
+++ b/dotnet/targets/Xamarin.Shared.Sdk.targets
@@ -124,7 +124,6 @@
false
true
- false
true
true
false
@@ -142,6 +141,12 @@
false
<_ComObjectDescriptorSupport Condition="'$(_ComObjectDescriptorSupport)' == ''">false
+
+
+ <_SuppressStartupHookSupportTrimWarning Condition="'$(StartupHookSupport)' == '' And '$(Optimize)' != 'true'">true
+ true
+ false
+
false
@@ -694,6 +699,7 @@
+
@@ -1266,6 +1272,7 @@
<_RuntimeConfigReservedProperties Include="NATIVE_DLL_SEARCH_DIRECTORIES" />
<_RuntimeConfigReservedProperties Include="RUNTIME_IDENTIFIER" />
<_RuntimeConfigReservedProperties Include="APP_CONTEXT_BASE_DIRECTORY" />
+ <_RuntimeConfigReservedProperties Include="STARTUP_HOOKS" />
+
+
+
+
+
+ ILLink
+ IL2026
+
+
+
+
diff --git a/tests/dotnet/StartupHookLibrary/MacCatalyst/StartupHookLibrary.csproj b/tests/dotnet/StartupHookLibrary/MacCatalyst/StartupHookLibrary.csproj
new file mode 100644
index 000000000000..02fabd2f8260
--- /dev/null
+++ b/tests/dotnet/StartupHookLibrary/MacCatalyst/StartupHookLibrary.csproj
@@ -0,0 +1,8 @@
+
+
+
+ net$(BundledNETCoreAppTargetFrameworkVersion)-maccatalyst
+
+
+
+
diff --git a/tests/dotnet/StartupHookLibrary/StartupHook.cs b/tests/dotnet/StartupHookLibrary/StartupHook.cs
new file mode 100644
index 000000000000..c8a6d94e4b68
--- /dev/null
+++ b/tests/dotnet/StartupHookLibrary/StartupHook.cs
@@ -0,0 +1,17 @@
+using System;
+
+using Foundation;
+
+
+class StartupHook {
+ public static void Initialize ()
+ {
+ Console.WriteLine ("STARTUP");
+
+ StartupStatus.Initialized = true;
+ }
+}
+
+public static class StartupStatus {
+ public static bool Initialized { get; internal set; }
+}
diff --git a/tests/dotnet/StartupHookLibrary/iOS/StartupHookLibrary.csproj b/tests/dotnet/StartupHookLibrary/iOS/StartupHookLibrary.csproj
new file mode 100644
index 000000000000..bb9259517c64
--- /dev/null
+++ b/tests/dotnet/StartupHookLibrary/iOS/StartupHookLibrary.csproj
@@ -0,0 +1,8 @@
+
+
+
+ net$(BundledNETCoreAppTargetFrameworkVersion)-ios
+
+
+
+
diff --git a/tests/dotnet/StartupHookLibrary/macOS/StartupHookLibrary.csproj b/tests/dotnet/StartupHookLibrary/macOS/StartupHookLibrary.csproj
new file mode 100644
index 000000000000..71b28ba48c7c
--- /dev/null
+++ b/tests/dotnet/StartupHookLibrary/macOS/StartupHookLibrary.csproj
@@ -0,0 +1,8 @@
+
+
+
+ net$(BundledNETCoreAppTargetFrameworkVersion)-macos
+
+
+
+
diff --git a/tests/dotnet/StartupHookLibrary/shared.csproj b/tests/dotnet/StartupHookLibrary/shared.csproj
new file mode 100644
index 000000000000..753c8fe96d98
--- /dev/null
+++ b/tests/dotnet/StartupHookLibrary/shared.csproj
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/tests/dotnet/StartupHookLibrary/tvOS/StartupHookLibrary.csproj b/tests/dotnet/StartupHookLibrary/tvOS/StartupHookLibrary.csproj
new file mode 100644
index 000000000000..388e767c58ed
--- /dev/null
+++ b/tests/dotnet/StartupHookLibrary/tvOS/StartupHookLibrary.csproj
@@ -0,0 +1,8 @@
+
+
+
+ net$(BundledNETCoreAppTargetFrameworkVersion)-tvos
+
+
+
+
diff --git a/tests/dotnet/StartupHookTest/AppDelegate.cs b/tests/dotnet/StartupHookTest/AppDelegate.cs
new file mode 100644
index 000000000000..22b018a22d00
--- /dev/null
+++ b/tests/dotnet/StartupHookTest/AppDelegate.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Runtime.InteropServices;
+
+using Foundation;
+
+namespace MySimpleApp {
+ public class Program {
+ static int Main (string [] args)
+ {
+ GC.KeepAlive (typeof (NSObject)); // prevent linking away the platform assembly
+
+ Console.WriteLine (Environment.GetEnvironmentVariable ("MAGIC_WORD"));
+ Console.WriteLine ($"Startup.Initialized: {StartupHook.Initialized}");
+ Console.WriteLine ($"StartupStatus.Initialized: {StartupStatus.Initialized}");
+
+ var rv = 0;
+
+ if (!StartupHook.Initialized)
+ rv += 1;
+
+ if (!StartupStatus.Initialized)
+ rv += 2;
+
+ return rv;
+ }
+ }
+}
+
+class StartupHook {
+ public static bool Initialized { get; private set; }
+ public static void Initialize ()
+ {
+ Initialized = true;
+ }
+}
diff --git a/tests/dotnet/StartupHookTest/MacCatalyst/Makefile b/tests/dotnet/StartupHookTest/MacCatalyst/Makefile
new file mode 100644
index 000000000000..110d078f4577
--- /dev/null
+++ b/tests/dotnet/StartupHookTest/MacCatalyst/Makefile
@@ -0,0 +1 @@
+include ../shared.mk
diff --git a/tests/dotnet/StartupHookTest/MacCatalyst/StartupHookTest.csproj b/tests/dotnet/StartupHookTest/MacCatalyst/StartupHookTest.csproj
new file mode 100644
index 000000000000..6b0e2c773180
--- /dev/null
+++ b/tests/dotnet/StartupHookTest/MacCatalyst/StartupHookTest.csproj
@@ -0,0 +1,7 @@
+
+
+
+ net$(BundledNETCoreAppTargetFrameworkVersion)-maccatalyst
+
+
+
diff --git a/tests/dotnet/StartupHookTest/Makefile b/tests/dotnet/StartupHookTest/Makefile
new file mode 100644
index 000000000000..6affa45ff122
--- /dev/null
+++ b/tests/dotnet/StartupHookTest/Makefile
@@ -0,0 +1,2 @@
+TOP=../../..
+include $(TOP)/tests/common/shared-dotnet-test.mk
diff --git a/tests/dotnet/StartupHookTest/iOS/Makefile b/tests/dotnet/StartupHookTest/iOS/Makefile
new file mode 100644
index 000000000000..110d078f4577
--- /dev/null
+++ b/tests/dotnet/StartupHookTest/iOS/Makefile
@@ -0,0 +1 @@
+include ../shared.mk
diff --git a/tests/dotnet/StartupHookTest/iOS/StartupHookTest.csproj b/tests/dotnet/StartupHookTest/iOS/StartupHookTest.csproj
new file mode 100644
index 000000000000..86d408734aa8
--- /dev/null
+++ b/tests/dotnet/StartupHookTest/iOS/StartupHookTest.csproj
@@ -0,0 +1,7 @@
+
+
+
+ net$(BundledNETCoreAppTargetFrameworkVersion)-ios
+
+
+
diff --git a/tests/dotnet/StartupHookTest/macOS/Makefile b/tests/dotnet/StartupHookTest/macOS/Makefile
new file mode 100644
index 000000000000..110d078f4577
--- /dev/null
+++ b/tests/dotnet/StartupHookTest/macOS/Makefile
@@ -0,0 +1 @@
+include ../shared.mk
diff --git a/tests/dotnet/StartupHookTest/macOS/StartupHookTest.csproj b/tests/dotnet/StartupHookTest/macOS/StartupHookTest.csproj
new file mode 100644
index 000000000000..a77287b9ba00
--- /dev/null
+++ b/tests/dotnet/StartupHookTest/macOS/StartupHookTest.csproj
@@ -0,0 +1,7 @@
+
+
+
+ net$(BundledNETCoreAppTargetFrameworkVersion)-macos
+
+
+
diff --git a/tests/dotnet/StartupHookTest/shared.csproj b/tests/dotnet/StartupHookTest/shared.csproj
new file mode 100644
index 000000000000..193d25df8cee
--- /dev/null
+++ b/tests/dotnet/StartupHookTest/shared.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+
+ StartupHookTest
+ com.xamarin.startuphooktest
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/dotnet/StartupHookTest/shared.mk b/tests/dotnet/StartupHookTest/shared.mk
new file mode 100644
index 000000000000..7224dd07d815
--- /dev/null
+++ b/tests/dotnet/StartupHookTest/shared.mk
@@ -0,0 +1,3 @@
+TOP=../../../..
+TESTNAME=StartupHookTest
+include $(TOP)/tests/common/shared-dotnet.mk
diff --git a/tests/dotnet/StartupHookTest/tvOS/Makefile b/tests/dotnet/StartupHookTest/tvOS/Makefile
new file mode 100644
index 000000000000..110d078f4577
--- /dev/null
+++ b/tests/dotnet/StartupHookTest/tvOS/Makefile
@@ -0,0 +1 @@
+include ../shared.mk
diff --git a/tests/dotnet/StartupHookTest/tvOS/StartupHookTest.csproj b/tests/dotnet/StartupHookTest/tvOS/StartupHookTest.csproj
new file mode 100644
index 000000000000..bd487ddcd88d
--- /dev/null
+++ b/tests/dotnet/StartupHookTest/tvOS/StartupHookTest.csproj
@@ -0,0 +1,7 @@
+
+
+
+ net$(BundledNETCoreAppTargetFrameworkVersion)-tvos
+
+
+
diff --git a/tests/dotnet/UnitTests/StartupHookTest.cs b/tests/dotnet/UnitTests/StartupHookTest.cs
new file mode 100644
index 000000000000..e8b22e33d0af
--- /dev/null
+++ b/tests/dotnet/UnitTests/StartupHookTest.cs
@@ -0,0 +1,114 @@
+#nullable enable
+
+namespace Xamarin.Tests {
+ [TestFixture]
+ public class StartupHookTest : TestBaseClass {
+ const string project = "StartupHookTest";
+
+ [TestCase (ApplePlatform.MacOSX, "osx-arm64")]
+ [TestCase (ApplePlatform.MacCatalyst, "maccatalyst-x64")]
+ public void EnabledForDebug (ApplePlatform platform, string runtimeIdentifiers)
+ {
+ Configuration.IgnoreIfIgnoredPlatform (platform);
+
+ var configuration = "Debug";
+ var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath, configuration: configuration);
+ Clean (project_path);
+
+ var properties = GetDefaultProperties (runtimeIdentifiers);
+ properties ["Configuration"] = configuration;
+ DotNet.AssertBuild (project_path, properties);
+
+ if (CanExecute (platform, properties)) {
+ var appExecutable = GetNativeExecutable (platform, appPath);
+ var env = new Dictionary {
+ { "DOTNET_STARTUP_HOOKS", "StartupHookTest:StartupHookLibrary" },
+ };
+ ExecuteWithMagicWordAndAssert (appExecutable, env);
+
+ env = new Dictionary {
+ { "DOTNET_STARTUP_HOOKS", "StartupHookLibrary" },
+ };
+ ExecuteWithMagicWordAndAssert (appExecutable, env, expectedExitCode: 1); // this should fail
+
+ env = new Dictionary {
+ { "DOTNET_STARTUP_HOOKS", "StartupHookTest" },
+ };
+ ExecuteWithMagicWordAndAssert (appExecutable, env, expectedExitCode: 2); // this should fail
+ }
+ }
+
+ [TestCase (ApplePlatform.MacOSX, "osx-arm64")]
+ [TestCase (ApplePlatform.MacCatalyst, "maccatalyst-x64")]
+ public void DisabledForRelease (ApplePlatform platform, string runtimeIdentifiers)
+ {
+ Configuration.IgnoreIfIgnoredPlatform (platform);
+
+ var configuration = "Release";
+ var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath, configuration: configuration);
+ Clean (project_path);
+
+ var properties = GetDefaultProperties (runtimeIdentifiers);
+ properties ["Configuration"] = configuration;
+ DotNet.AssertBuild (project_path, properties);
+
+ if (CanExecute (platform, properties)) {
+ var appExecutable = GetNativeExecutable (platform, appPath);
+ var env = new Dictionary {
+ { "DOTNET_STARTUP_HOOKS", "StartupHookTest:StartupHookLibrary" },
+ };
+ ExecuteWithMagicWordAndAssert (appExecutable, env, expectedExitCode: 3); // this should fail
+
+ env = new Dictionary ();
+ ExecuteWithMagicWordAndAssert (appExecutable, env, expectedExitCode: 3); // this should fail
+
+ env = new Dictionary {
+ { "DOTNET_STARTUP_HOOKS", "StartupHookLibrary" },
+ };
+ ExecuteWithMagicWordAndAssert (appExecutable, env, expectedExitCode: 3); // this should fail
+
+ env = new Dictionary {
+ { "DOTNET_STARTUP_HOOKS", "StartupHookTest" },
+ };
+ ExecuteWithMagicWordAndAssert (appExecutable, env, expectedExitCode: 3); // this should fail
+ }
+ }
+
+ [TestCase (ApplePlatform.MacOSX, "osx-arm64")]
+ [TestCase (ApplePlatform.MacCatalyst, "maccatalyst-x64")]
+ public void Enableable (ApplePlatform platform, string runtimeIdentifiers)
+ {
+ Configuration.IgnoreIfIgnoredPlatform (platform);
+
+ var configuration = "Release";
+ var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath, configuration: configuration);
+ Clean (project_path);
+
+ var properties = GetDefaultProperties (runtimeIdentifiers);
+ properties ["Configuration"] = configuration;
+ properties ["StartupHookSupport"] = "true";
+ DotNet.AssertBuild (project_path, properties);
+
+ if (CanExecute (platform, properties)) {
+ var appExecutable = GetNativeExecutable (platform, appPath);
+ var env = new Dictionary {
+ { "DOTNET_STARTUP_HOOKS", "StartupHookTest:StartupHookLibrary" },
+ };
+ ExecuteWithMagicWordAndAssert (appExecutable, env); // this should work
+
+ env = new Dictionary ();
+ ExecuteWithMagicWordAndAssert (appExecutable, env, expectedExitCode: 3); // this should fail
+
+ env = new Dictionary {
+ { "DOTNET_STARTUP_HOOKS", "StartupHookLibrary" },
+ };
+ ExecuteWithMagicWordAndAssert (appExecutable, env, expectedExitCode: 1); // this should fail
+
+ env = new Dictionary {
+ { "DOTNET_STARTUP_HOOKS", "StartupHookTest" },
+ };
+ ExecuteWithMagicWordAndAssert (appExecutable, env, expectedExitCode: 2); // this should fail
+ }
+ }
+ }
+}
diff --git a/tests/dotnet/UnitTests/TestBaseClass.cs b/tests/dotnet/UnitTests/TestBaseClass.cs
index fed8415f5fc5..6b35686da384 100644
--- a/tests/dotnet/UnitTests/TestBaseClass.cs
+++ b/tests/dotnet/UnitTests/TestBaseClass.cs
@@ -386,7 +386,7 @@ protected string ExecuteWithMagicWordAndAssert (ApplePlatform platform, string r
return ExecuteWithMagicWordAndAssert (executable, environment);
}
- protected string ExecuteWithMagicWordAndAssert (string executable, Dictionary? environment = null)
+ protected string ExecuteWithMagicWordAndAssert (string executable, Dictionary? environment = null, int expectedExitCode = 0)
{
if (Environment.OSVersion.Platform == PlatformID.Win32NT) {
Console.WriteLine ($"Not executing '{executable}' because we're on Windows.");
@@ -395,8 +395,8 @@ protected string ExecuteWithMagicWordAndAssert (string executable, Dictionary