From 169a51c52e1664577127afdcb6ab32f347fe1d9a Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Wed, 15 Apr 2026 23:09:24 -0700 Subject: [PATCH 1/3] Force MetaDataGetDispenser to be linked into singlefilehost Add linker flags on Unix/macOS to force MetaDataGetDispenser as an undefined symbol, which makes the linker pull mscoree.o from coreclr_static. On Windows, the .def file already forces this. Add a test that validates the export is present in the singlefilehost binary using NativeLibrary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/AppHost.Bundle.Tests/StaticHost.cs | 51 +++++++++++++++++++ .../corehost/apphost/static/CMakeLists.txt | 9 ++++ 2 files changed, 60 insertions(+) diff --git a/src/installer/tests/AppHost.Bundle.Tests/StaticHost.cs b/src/installer/tests/AppHost.Bundle.Tests/StaticHost.cs index 1aa93f860be6f3..4c1bf1c11530c3 100644 --- a/src/installer/tests/AppHost.Bundle.Tests/StaticHost.cs +++ b/src/installer/tests/AppHost.Bundle.Tests/StaticHost.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Runtime.InteropServices; using Microsoft.DotNet.Cli.Build.Framework; using Microsoft.DotNet.CoreSetup.Test; using Xunit; @@ -26,5 +27,55 @@ private void NotMarkedAsBundle_RunSelfContainedApp() .And.HaveStdOutContaining("Hello World"); } } + + [Fact] + private void ExpectedExports() + { + string[] expectedExports = + [ + "DotNetRuntimeInfo", + "g_dacTable", + "MetaDataGetDispenser", + ]; + + string singleFileHostPath = Binaries.SingleFileHost.FilePath; + + if (OperatingSystem.IsWindows()) + { + IntPtr handle = NativeLibrary.Load(singleFileHostPath); + try + { + foreach (string exportName in expectedExports) + { + Assert.True( + NativeLibrary.TryGetExport(handle, exportName, out _), + $"Expected singlefilehost to export {exportName}"); + } + } + finally + { + NativeLibrary.Free(handle); + } + } + else + { + // Use nm to check for exported dynamic symbols. + // On macOS, nm shows all symbols; on Linux, -D shows dynamic symbols. + string args = OperatingSystem.IsMacOS() + ? singleFileHostPath + : $"-D {singleFileHostPath}"; + + CommandResult result = Command.Create("nm", args) + .CaptureStdOut() + .CaptureStdErr() + .Execute(); + result.Should().Pass(); + + foreach (string exportName in expectedExports) + { + Assert.Contains(exportName, result.StdOut); + } + } + } } } diff --git a/src/native/corehost/apphost/static/CMakeLists.txt b/src/native/corehost/apphost/static/CMakeLists.txt index 6a2ced4343facc..b1706e56542626 100644 --- a/src/native/corehost/apphost/static/CMakeLists.txt +++ b/src/native/corehost/apphost/static/CMakeLists.txt @@ -266,6 +266,15 @@ endif() set_property(TARGET singlefilehost PROPERTY ENABLE_EXPORTS 1) +# Force the linker to pull in MetaDataGetDispenser from coreclr_static even though +# nothing in singlefilehost references it directly. Without this, the linker's +# dead code elimination drops mscoree.o and the export is missing from the binary. +if(CLR_CMAKE_TARGET_APPLE) + target_link_options(singlefilehost PRIVATE -Wl,-u,_MetaDataGetDispenser) +elseif(NOT CLR_CMAKE_TARGET_WIN32) + target_link_options(singlefilehost PRIVATE -Wl,-u,MetaDataGetDispenser) +endif() + target_link_libraries( singlefilehost PRIVATE From e3beab1c9a58cb0e6777df471c9669c15148e682 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Thu, 16 Apr 2026 16:20:36 -0700 Subject: [PATCH 2/3] PR feedback --- .../tests/AppHost.Bundle.Tests/StaticHost.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/installer/tests/AppHost.Bundle.Tests/StaticHost.cs b/src/installer/tests/AppHost.Bundle.Tests/StaticHost.cs index 4c1bf1c11530c3..0752cf74d6f83d 100644 --- a/src/installer/tests/AppHost.Bundle.Tests/StaticHost.cs +++ b/src/installer/tests/AppHost.Bundle.Tests/StaticHost.cs @@ -59,13 +59,14 @@ private void ExpectedExports() } else { - // Use nm to check for exported dynamic symbols. - // On macOS, nm shows all symbols; on Linux, -D shows dynamic symbols. - string args = OperatingSystem.IsMacOS() - ? singleFileHostPath - : $"-D {singleFileHostPath}"; + // Use nm to check for exported defined symbols. + // On macOS, -gUj shows external defined symbol names only. + // On Linux, -D --defined-only shows defined dynamic symbols. + Command command = OperatingSystem.IsMacOS() + ? Command.Create("nm", "-gUj", singleFileHostPath) + : Command.Create("nm", "-D", "--defined-only", singleFileHostPath); - CommandResult result = Command.Create("nm", args) + CommandResult result = command .CaptureStdOut() .CaptureStdErr() .Execute(); From 561bc1dde8e0acb063d7a007bd1f8cae374848ad Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Thu, 16 Apr 2026 18:52:39 -0700 Subject: [PATCH 3/3] Include other exports --- .../tests/AppHost.Bundle.Tests/StaticHost.cs | 1 + .../corehost/apphost/static/CMakeLists.txt | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/installer/tests/AppHost.Bundle.Tests/StaticHost.cs b/src/installer/tests/AppHost.Bundle.Tests/StaticHost.cs index 0752cf74d6f83d..30c78cd2d1877a 100644 --- a/src/installer/tests/AppHost.Bundle.Tests/StaticHost.cs +++ b/src/installer/tests/AppHost.Bundle.Tests/StaticHost.cs @@ -36,6 +36,7 @@ private void ExpectedExports() "DotNetRuntimeInfo", "g_dacTable", "MetaDataGetDispenser", + "DotNetRuntimeContractDescriptor", ]; string singleFileHostPath = Binaries.SingleFileHost.FilePath; diff --git a/src/native/corehost/apphost/static/CMakeLists.txt b/src/native/corehost/apphost/static/CMakeLists.txt index b1706e56542626..3a973afd31be7c 100644 --- a/src/native/corehost/apphost/static/CMakeLists.txt +++ b/src/native/corehost/apphost/static/CMakeLists.txt @@ -266,13 +266,22 @@ endif() set_property(TARGET singlefilehost PROPERTY ENABLE_EXPORTS 1) -# Force the linker to pull in MetaDataGetDispenser from coreclr_static even though -# nothing in singlefilehost references it directly. Without this, the linker's -# dead code elimination drops mscoree.o and the export is missing from the binary. +# Force the linker to pull in symbols from coreclr_static even though nothing in +# singlefilehost references them directly. Without this, the linker's dead code +# elimination can drop object files and the exports are missing from the binary. +# This should be kept in sync with singlefilehost_unixexports.src. if(CLR_CMAKE_TARGET_APPLE) - target_link_options(singlefilehost PRIVATE -Wl,-u,_MetaDataGetDispenser) + target_link_options(singlefilehost PRIVATE + -Wl,-u,_DotNetRuntimeInfo + -Wl,-u,_g_dacTable + -Wl,-u,_MetaDataGetDispenser + -Wl,-u,_DotNetRuntimeContractDescriptor) elseif(NOT CLR_CMAKE_TARGET_WIN32) - target_link_options(singlefilehost PRIVATE -Wl,-u,MetaDataGetDispenser) + target_link_options(singlefilehost PRIVATE + -Wl,-u,DotNetRuntimeInfo + -Wl,-u,g_dacTable + -Wl,-u,MetaDataGetDispenser + -Wl,-u,DotNetRuntimeContractDescriptor) endif() target_link_libraries(