diff --git a/src/installer/tests/AppHost.Bundle.Tests/AppLaunch.cs b/src/installer/tests/AppHost.Bundle.Tests/AppLaunch.cs index c1ccb708ad6719..e301aabfee94f9 100644 --- a/src/installer/tests/AppHost.Bundle.Tests/AppLaunch.cs +++ b/src/installer/tests/AppHost.Bundle.Tests/AppLaunch.cs @@ -24,13 +24,21 @@ public AppLaunch(AppLaunch.SharedTestState fixture) private void RunTheApp(string path, bool selfContained) { - Command.Create(path) - .CaptureStdErr() - .CaptureStdOut() + var result = Command.Create(path) + .EnableTracingAndCaptureOutputs() .DotNetRoot(selfContained ? null : TestContext.BuiltDotNet.BinPath) - .Execute() - .Should().Pass() + .Execute(); + result.Should().Pass() .And.HaveStdOutContaining("Hello World!"); + + if (selfContained) + { + result.Should().NotHaveProperty(Constants.RuntimeProperty.DotnetHostPath); + } + else + { + result.Should().HaveProperty(Constants.RuntimeProperty.DotnetHostPath, TestContext.BuiltDotNet.DotnetExecutablePath); + } } private string MakeUniversalBinary(string path, Architecture architecture) diff --git a/src/installer/tests/HostActivation.Tests/NativeHosting/ApplicationExecution.cs b/src/installer/tests/HostActivation.Tests/NativeHosting/ApplicationExecution.cs index a91cd009aa20c0..f0d8f8df2d378a 100644 --- a/src/installer/tests/HostActivation.Tests/NativeHosting/ApplicationExecution.cs +++ b/src/installer/tests/HostActivation.Tests/NativeHosting/ApplicationExecution.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Runtime.InteropServices; using Xunit; +using HostActivation.Tests; namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.NativeHosting { @@ -37,6 +38,7 @@ public void RunApp() .Execute() .Should().Pass() .And.InitializeContextForApp(app.AppDll) + .And.HaveProperty(Constants.RuntimeProperty.DotnetHostPath, TestContext.BuiltDotNet.DotnetExecutablePath) .And.ExecuteApplication(sharedState.NativeHostPath, app.AppDll); } diff --git a/src/installer/tests/HostActivation.Tests/NativeHosting/GetFunctionPointer.cs b/src/installer/tests/HostActivation.Tests/NativeHosting/GetFunctionPointer.cs index 98eadc0b0769c3..7e7712ebbca8eb 100644 --- a/src/installer/tests/HostActivation.Tests/NativeHosting/GetFunctionPointer.cs +++ b/src/installer/tests/HostActivation.Tests/NativeHosting/GetFunctionPointer.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; +using HostActivation.Tests; using Microsoft.DotNet.Cli.Build.Framework; using Xunit; @@ -40,7 +41,8 @@ public void CallDelegateOnApplicationContext(bool validType, bool validMethod) .Execute(); result.Should() - .InitializeContextForApp(app.AppDll); + .InitializeContextForApp(app.AppDll) + .And.HaveProperty(Constants.RuntimeProperty.DotnetHostPath, TestContext.BuiltDotNet.DotnetExecutablePath); if (validType && validMethod) { @@ -73,7 +75,8 @@ public void CallDelegateOnComponentContext(bool validType, bool validMethod) .Execute(); result.Should() - .InitializeContextForConfig(component.RuntimeConfigJson); + .InitializeContextForConfig(component.RuntimeConfigJson) + .And.HaveProperty(Constants.RuntimeProperty.DotnetHostPath, TestContext.BuiltDotNet.DotnetExecutablePath); // This should fail even with the valid type and valid method, // because the type is not resolvable from the default AssemblyLoadContext. @@ -96,6 +99,7 @@ public void CallDelegateOnSelfContainedApplicationContext() .Execute(); result.Should().Pass() + .And.NotHaveProperty(Constants.RuntimeProperty.DotnetHostPath) .And.InitializeContextForApp(app.AppDll) .And.ExecuteFunctionPointer(sharedState.FunctionPointerEntryPoint1, 1, 1) .And.ExecuteInDefaultContext(app.AssemblyName); diff --git a/src/installer/tests/HostActivation.Tests/RuntimeProperties.cs b/src/installer/tests/HostActivation.Tests/RuntimeProperties.cs index 39b872df613288..bac0d74cd39239 100644 --- a/src/installer/tests/HostActivation.Tests/RuntimeProperties.cs +++ b/src/installer/tests/HostActivation.Tests/RuntimeProperties.cs @@ -4,6 +4,7 @@ using System; using System.IO; using Microsoft.DotNet.Cli.Build; +using Microsoft.DotNet.Cli.Build.Framework; using Microsoft.DotNet.CoreSetup.Test; using Microsoft.DotNet.TestUtils; using Xunit; @@ -28,7 +29,7 @@ public void AppConfigProperty_AppCanGetData() .EnableTracingAndCaptureOutputs() .Execute() .Should().Pass() - .And.HaveStdErrContaining($"Property {SharedTestState.AppTestPropertyName} = {SharedTestState.AppTestPropertyValue}") + .And.HaveProperty(SharedTestState.AppTestPropertyName, SharedTestState.AppTestPropertyValue) .And.HaveStdOutContaining($"AppContext.GetData({SharedTestState.AppTestPropertyName}) = {SharedTestState.AppTestPropertyValue}"); } @@ -39,7 +40,7 @@ public void FrameworkConfigProperty_AppCanGetData() .EnableTracingAndCaptureOutputs() .Execute() .Should().Pass() - .And.HaveStdErrContaining($"Property {SharedTestState.FrameworkTestPropertyName} = {SharedTestState.FrameworkTestPropertyValue}") + .And.HaveProperty(SharedTestState.FrameworkTestPropertyName, SharedTestState.FrameworkTestPropertyValue) .And.HaveStdOutContaining($"AppContext.GetData({SharedTestState.FrameworkTestPropertyName}) = {SharedTestState.FrameworkTestPropertyValue}"); } @@ -55,7 +56,7 @@ public void DuplicateConfigProperty_AppConfigValueUsed() .EnableTracingAndCaptureOutputs() .Execute() .Should().Pass() - .And.HaveStdErrContaining($"Property {SharedTestState.FrameworkTestPropertyName} = {SharedTestState.AppTestPropertyValue}") + .And.HaveProperty(SharedTestState.FrameworkTestPropertyName, SharedTestState.AppTestPropertyValue) .And.HaveStdOutContaining($"AppContext.GetData({SharedTestState.FrameworkTestPropertyName}) = {SharedTestState.AppTestPropertyValue}"); } @@ -67,7 +68,7 @@ public void HostFxrPathProperty_SetWhenRunningSDKCommand() .EnableTracingAndCaptureOutputs() .Execute() .Should().Pass() - .And.HaveStdErrContaining($"Property {SharedTestState.HostFxrPathPropertyName} = {dotnet.GreatestVersionHostFxrFilePath}"); + .And.HaveProperty(SharedTestState.HostFxrPathPropertyName, dotnet.GreatestVersionHostFxrFilePath); } [Fact] @@ -77,9 +78,57 @@ public void HostFxrPathProperty_NotVisibleFromApp() .EnableTracingAndCaptureOutputs() .Execute() .Should().Pass() + .And.NotHaveProperty(SharedTestState.HostFxrPathPropertyName) .And.HaveStdOutContaining($"Property '{SharedTestState.HostFxrPathPropertyName}' was not found."); } + [Fact] + public void DotNetHostPathProperty_FrameworkDependentApp() + { + // DOTNET_HOST_PATH property should point to the dotnet executable in the dotnet root directory + sharedState.DotNet.Exec(sharedState.App.AppDll, PrintProperties, Constants.RuntimeProperty.DotnetHostPath) + .EnableTracingAndCaptureOutputs() + .Execute() + .Should().Pass() + .And.HaveProperty(Constants.RuntimeProperty.DotnetHostPath, sharedState.DotNet.DotnetExecutablePath) + .And.HaveStdOutContaining($"AppContext.GetData({Constants.RuntimeProperty.DotnetHostPath}) = {sharedState.DotNet.DotnetExecutablePath}"); + + Command.Create(sharedState.App.AppExe, PrintProperties, Constants.RuntimeProperty.DotnetHostPath) + .EnableTracingAndCaptureOutputs() + .DotNetRoot(sharedState.DotNet.BinPath) + .Execute() + .Should().Pass() + .And.HaveProperty(Constants.RuntimeProperty.DotnetHostPath, sharedState.DotNet.DotnetExecutablePath) + .And.HaveStdOutContaining($"AppContext.GetData({Constants.RuntimeProperty.DotnetHostPath}) = {sharedState.DotNet.DotnetExecutablePath}"); + } + + [Fact] + public void DotNetHostPathProperty_SDKCommand() + { + // DOTNET_HOST_PATH property should point to the dotnet executable used to run the command + var dotnet = sharedState.MockSDK; + dotnet.Exec("--info") + .EnableTracingAndCaptureOutputs() + .Execute() + .Should().Pass() + .And.HaveProperty(Constants.RuntimeProperty.DotnetHostPath, dotnet.DotnetExecutablePath); + } + + [Fact] + public void DotNetHostPathProperty_SDKCommand_LocalInstall() + { + using TestArtifact workingDirectory = TestArtifact.Create("dotnetHostPath"); + GlobalJson.Write(workingDirectory.Location, new GlobalJson.Sdk() { Paths = [ sharedState.MockSDK.BinPath ] }); + + // DOTNET_HOST_PATH should point to the dotnet executable in the local SDK install used to run the command + sharedState.DotNet.Exec("--info") + .EnableTracingAndCaptureOutputs() + .WorkingDirectory(workingDirectory.Location) + .Execute() + .Should().Pass() + .And.HaveProperty(Constants.RuntimeProperty.DotnetHostPath, sharedState.MockSDK.DotnetExecutablePath); + } + [Fact] public void DuplicateCommonProperty_Fails() { @@ -110,7 +159,7 @@ public void SpecifiedInConfigAndDevConfig_ConfigWins() .EnableTracingAndCaptureOutputs() .Execute() .Should().Pass() - .And.HaveStdErrContaining($"Property {SharedTestState.AppTestPropertyName} = {SharedTestState.AppTestPropertyValue}") + .And.HaveProperty(SharedTestState.AppTestPropertyName, SharedTestState.AppTestPropertyValue) .And.HaveStdOutContaining($"AppContext.GetData({SharedTestState.AppTestPropertyName}) = {SharedTestState.AppTestPropertyValue}"); } diff --git a/src/installer/tests/HostActivation.Tests/SelfContainedAppLaunch.cs b/src/installer/tests/HostActivation.Tests/SelfContainedAppLaunch.cs index f421335995f696..28f432514b98cd 100644 --- a/src/installer/tests/HostActivation.Tests/SelfContainedAppLaunch.cs +++ b/src/installer/tests/HostActivation.Tests/SelfContainedAppLaunch.cs @@ -34,7 +34,8 @@ public void Default() .Execute() .Should().Pass() .And.HaveStdOutContaining("Hello World") - .And.HaveStdOutContaining(TestContext.MicrosoftNETCoreAppVersion); + .And.HaveStdOutContaining(TestContext.MicrosoftNETCoreAppVersion) + .And.NotHaveProperty(Constants.RuntimeProperty.DotnetHostPath); if (OperatingSystem.IsWindows()) { diff --git a/src/installer/tests/TestUtils/Assertions/CommandResultExtensions.RuntimeProperty.cs b/src/installer/tests/TestUtils/Assertions/CommandResultExtensions.RuntimeProperty.cs new file mode 100644 index 00000000000000..be7b3163f95eea --- /dev/null +++ b/src/installer/tests/TestUtils/Assertions/CommandResultExtensions.RuntimeProperty.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; + +namespace Microsoft.DotNet.CoreSetup.Test +{ + public static partial class CommandResultExtensions + { + public static AndConstraint HaveProperty(this CommandResultAssertions assertion, string name, string value) + { + // Expected trace message from hostpolicy + return assertion.HaveStdErrContaining($"Property {name} = {value}"); + } + + public static AndConstraint NotHaveProperty(this CommandResultAssertions assertion, string name) + { + return assertion.NotHaveStdErrContaining($"Property {name} ="); + } + } +} diff --git a/src/installer/tests/TestUtils/Assertions/CommandResultExtensions.cs b/src/installer/tests/TestUtils/Assertions/CommandResultExtensions.cs index d0e0175060b893..e02a235d95bbb0 100644 --- a/src/installer/tests/TestUtils/Assertions/CommandResultExtensions.cs +++ b/src/installer/tests/TestUtils/Assertions/CommandResultExtensions.cs @@ -8,7 +8,7 @@ namespace Microsoft.DotNet.CoreSetup.Test { - public static class CommandResultExtensions + public static partial class CommandResultExtensions { public static CommandResultAssertions Should(this CommandResult commandResult) { diff --git a/src/installer/tests/TestUtils/Constants.cs b/src/installer/tests/TestUtils/Constants.cs index e9e2e21b2df22e..ef760cb8b8f74b 100644 --- a/src/installer/tests/TestUtils/Constants.cs +++ b/src/installer/tests/TestUtils/Constants.cs @@ -134,5 +134,10 @@ public static class ErrorCode public const int COMPlusException = unchecked((int)0xe0434352); public const int SIGABRT = 134; } + + public static class RuntimeProperty + { + public const string DotnetHostPath = "DOTNET_HOST_PATH"; + } } } diff --git a/src/native/corehost/hostmisc/pal.h b/src/native/corehost/hostmisc/pal.h index 334ab5a4a0f97e..8c8cad6eefcad9 100644 --- a/src/native/corehost/hostmisc/pal.h +++ b/src/native/corehost/hostmisc/pal.h @@ -71,12 +71,15 @@ #if defined(TARGET_WINDOWS) #define LIB_PREFIX "" #define LIB_FILE_EXT ".dll" +#define EXE_FILE_EXT ".exe" #elif defined(TARGET_OSX) #define LIB_PREFIX "lib" #define LIB_FILE_EXT ".dylib" +#define EXE_FILE_EXT "" #else #define LIB_PREFIX "lib" #define LIB_FILE_EXT ".so" +#define EXE_FILE_EXT "" #endif #define _STRINGIFY(s) _X(s) @@ -139,8 +142,6 @@ namespace pal CRITICAL_SECTION _impl; }; - inline const pal::char_t* exe_suffix() { return _X(".exe"); } - inline int cstrcasecmp(const char* str1, const char* str2) { return ::_stricmp(str1, str2); } inline int strcmp(const char_t* str1, const char_t* str2) { return ::wcscmp(str1, str2); } inline int strcasecmp(const char_t* str1, const char_t* str2) { return ::_wcsicmp(str1, str2); } @@ -213,8 +214,6 @@ namespace pal typedef void* proc_t; typedef std::mutex mutex_t; - inline const pal::char_t* exe_suffix() { return nullptr; } - inline int cstrcasecmp(const char* str1, const char* str2) { return ::strcasecmp(str1, str2); } inline int strcmp(const char_t* str1, const char_t* str2) { return ::strcmp(str1, str2); } inline int strcasecmp(const char_t* str1, const char_t* str2) { return ::strcasecmp(str1, str2); } diff --git a/src/native/corehost/hostmisc/utils.cpp b/src/native/corehost/hostmisc/utils.cpp index a48cd2d39f9bd0..557adefb5bc4a5 100644 --- a/src/native/corehost/hostmisc/utils.cpp +++ b/src/native/corehost/hostmisc/utils.cpp @@ -71,15 +71,11 @@ void append_path(pal::string_t* path1, const pal::char_t* path2) pal::string_t strip_executable_ext(const pal::string_t& filename) { - const pal::char_t* exe_suffix = pal::exe_suffix(); - if (exe_suffix == nullptr) - return filename; - - size_t suffix_len = pal::strlen(exe_suffix); + size_t suffix_len = STRING_LENGTH(EXE_FILE_EXT); if (suffix_len == 0) return filename; - if (utils::ends_with(filename, exe_suffix, suffix_len, false)) + if (utils::ends_with(filename, _STRINGIFY(EXE_FILE_EXT), false)) { // We need to strip off the old extension pal::string_t result(filename); diff --git a/src/native/corehost/hostpolicy/coreclr.cpp b/src/native/corehost/hostpolicy/coreclr.cpp index 8f71aa18c4a19a..97c079aaa26622 100644 --- a/src/native/corehost/hostpolicy/coreclr.cpp +++ b/src/native/corehost/hostpolicy/coreclr.cpp @@ -165,6 +165,7 @@ namespace _X("STARTUP_HOOKS"), _X("APP_PATHS"), _X("RUNTIME_IDENTIFIER"), + _X("DOTNET_HOST_PATH"), }; static_assert((sizeof(PropertyNameMapping) / sizeof(*PropertyNameMapping)) == static_cast(common_property::Last), "Invalid property count"); diff --git a/src/native/corehost/hostpolicy/coreclr.h b/src/native/corehost/hostpolicy/coreclr.h index 1ee29ba7edf98c..dfa2b79df1b0de 100644 --- a/src/native/corehost/hostpolicy/coreclr.h +++ b/src/native/corehost/hostpolicy/coreclr.h @@ -64,6 +64,7 @@ enum class common_property StartUpHooks, AppPaths, RuntimeIdentifier, + DotNetHostPath, // Sentinel value - new values should be defined above Last }; diff --git a/src/native/corehost/hostpolicy/hostpolicy_context.cpp b/src/native/corehost/hostpolicy/hostpolicy_context.cpp index e513472211f01b..d41cb110dee9fc 100644 --- a/src/native/corehost/hostpolicy/hostpolicy_context.cpp +++ b/src/native/corehost/hostpolicy/hostpolicy_context.cpp @@ -343,6 +343,22 @@ int hostpolicy_context_t::initialize(const hostpolicy_init_t &hostpolicy_init, c coreclr_properties.add(common_property::StartUpHooks, startup_hooks.c_str()); } + // Path to dotnet corresponding to the runtime used to run the application (if it exists) + // Only for framework-dependent applications, as there is no corresponding dotnet for self-contained. +#if !defined(NATIVE_LIBS_EMBEDDED) + if (hostpolicy_init.is_framework_dependent) + { + pal::string_t dotnet_host_path = hostpolicy_init.host_info.dotnet_root; + append_path(&dotnet_host_path, _X("dotnet")); + dotnet_host_path.append(_STRINGIFY(EXE_FILE_EXT)); + if (!coreclr_properties.add(common_property::DotNetHostPath, dotnet_host_path.c_str())) + { + log_duplicate_property_error(coreclr_property_bag_t::common_property_to_string(common_property::DotNetHostPath)); + return StatusCode::LibHostDuplicateProperty; + } + } +#endif + { host_contract = { sizeof(host_runtime_contract), this }; if (bundle::info_t::is_single_file_bundle())