diff --git a/src/installer/tests/HostActivation.Tests/HostCommands.cs b/src/installer/tests/HostActivation.Tests/HostCommands.cs index 44883cf75ffe89..7b01096ca2acef 100644 --- a/src/installer/tests/HostActivation.Tests/HostCommands.cs +++ b/src/installer/tests/HostActivation.Tests/HostCommands.cs @@ -83,6 +83,91 @@ public void Info_Utf8Path() .And.HaveStdOutMatching($@"DOTNET_ROOT.*{installLocation}"); } + [Fact] + public void Info_ListEnvironment() + { + var command = TestContext.BuiltDotNet.Exec("--info") + .CaptureStdOut(); + + // Add DOTNET_ROOT environment variables + (string Architecture, string Path)[] dotnetRootEnvVars = [ + ("arm64", "/arm64/dotnet/root"), + ("x64", "/x64/dotnet/root"), + ("x86", "/x86/dotnet/root"), + ("unknown", "/unknown/dotnet/root") + ]; + foreach (var envVar in dotnetRootEnvVars) + { + command = command.DotNetRoot(envVar.Path, envVar.Architecture); + } + + string dotnetRootNoArch = "/dotnet/root"; + command = command.DotNetRoot(dotnetRootNoArch); + + // Add additional DOTNET_* environment variables + (string Name, string Value)[] envVars = [ + ("DOTNET_ROLL_FORWARD", "Major"), + ("DOTNET_SOME_SETTING", "/some/setting"), + ("DOTNET_HOST_TRACE", "1") + ]; + + (string Name, string Value)[] differentCaseEnvVars = [ + ("dotnet_env_var", "dotnet env var value"), + ("dOtNeT_setting", "doOtNeT setting value"), + ]; + foreach ((string name, string value) in envVars.Concat(differentCaseEnvVars)) + { + command = command.EnvironmentVariable(name, value); + } + + string otherEnvVar = "OTHER"; + command = command.EnvironmentVariable(otherEnvVar, "value"); + + var result = command.Execute(); + result.Should().Pass() + .And.HaveStdOutContaining("Environment variables:") + .And.HaveStdOutMatching($@"{Constants.DotnetRoot.EnvironmentVariable}\s*\[{dotnetRootNoArch}\]") + .And.NotHaveStdOutContaining(otherEnvVar); + + foreach ((string architecture, string path) in dotnetRootEnvVars) + { + result.Should() + .HaveStdOutMatching($@"{Constants.DotnetRoot.ArchitectureEnvironmentVariablePrefix}{architecture.ToUpper()}\s*\[{path}\]"); + } + + foreach ((string name, string value) in envVars) + { + result.Should().HaveStdOutMatching($@"{name}\s*\[{value}\]"); + } + + foreach ((string name, string value) in differentCaseEnvVars) + { + if (OperatingSystem.IsWindows()) + { + // Environment variables are case-insensitive on Windows + result.Should().HaveStdOutMatching($@"{name}\s*\[{value}\]"); + } + else + { + result.Should().NotHaveStdOutContaining(name); + } + } + } + + [Fact] + public void Info_ListEnvironment_LegacyPrefixDetection() + { + string comPlusEnvVar = "COMPlus_ReadyToRun"; + TestContext.BuiltDotNet.Exec("--info") + .EnvironmentVariable(comPlusEnvVar, "0") + .CaptureStdOut() + .Execute() + .Should().Pass() + .And.HaveStdOutContaining("Environment variables:") + .And.NotHaveStdOutContaining(comPlusEnvVar) + .And.HaveStdOutContaining("Detected COMPlus_* environment variable(s). Consider transitioning to DOTNET_* equivalent."); + } + [Fact] public void ListRuntimes() { diff --git a/src/installer/tests/HostActivation.Tests/InstallLocation.cs b/src/installer/tests/HostActivation.Tests/InstallLocation.cs index cb2a15dd0a5836..afcfddc90a55b8 100644 --- a/src/installer/tests/HostActivation.Tests/InstallLocation.cs +++ b/src/installer/tests/HostActivation.Tests/InstallLocation.cs @@ -121,42 +121,6 @@ public void EnvironmentVariable_DotnetRootPathExistsButHasNoHost() .And.HaveStdErrContaining($"The required library {Binaries.HostFxr.FileName} could not be found."); } - [Fact] - public void EnvironmentVariable_DotNetInfo_ListEnvironment() - { - var command = TestContext.BuiltDotNet.Exec("--info") - .CaptureStdOut(); - - var envVars = new (string Architecture, string Path)[] { - ("arm64", "/arm64/dotnet/root"), - ("x64", "/x64/dotnet/root"), - ("x86", "/x86/dotnet/root") - }; - foreach(var envVar in envVars) - { - command = command.DotNetRoot(envVar.Path, envVar.Architecture); - } - - string dotnetRootNoArch = "/dotnet/root"; - command = command.DotNetRoot(dotnetRootNoArch); - - (string Architecture, string Path) unknownEnvVar = ("unknown", "/unknown/dotnet/root"); - command = command.DotNetRoot(unknownEnvVar.Path, unknownEnvVar.Architecture); - - var result = command.Execute(); - result.Should().Pass() - .And.HaveStdOutContaining("Environment variables:") - .And.HaveStdOutMatching($@"{Constants.DotnetRoot.EnvironmentVariable}\s*\[{dotnetRootNoArch}\]") - .And.NotHaveStdOutContaining($"{Constants.DotnetRoot.ArchitectureEnvironmentVariablePrefix}{unknownEnvVar.Architecture.ToUpper()}") - .And.NotHaveStdOutContaining($"[{unknownEnvVar.Path}]"); - - foreach ((string architecture, string path) in envVars) - { - result.Should() - .HaveStdOutMatching($@"{Constants.DotnetRoot.ArchitectureEnvironmentVariablePrefix}{architecture.ToUpper()}\s*\[{path}\]"); - } - } - [Fact] public void DefaultInstallLocation() { diff --git a/src/native/corehost/fxr/install_info.cpp b/src/native/corehost/fxr/install_info.cpp index 64eb5490903174..bf7103ed31f33c 100644 --- a/src/native/corehost/fxr/install_info.cpp +++ b/src/native/corehost/fxr/install_info.cpp @@ -6,29 +6,47 @@ #include "trace.h" #include "utils.h" +#include +#include + bool install_info::print_environment(const pal::char_t* leading_whitespace) { - bool found_any = false; - - const pal::char_t* fmt = _X("%s%-17s [%s]"); - pal::string_t value; - if (pal::getenv(DOTNET_ROOT_ENV_VAR, &value)) - { - found_any = true; - trace::println(fmt, leading_whitespace, DOTNET_ROOT_ENV_VAR, value.c_str()); - } - - for (uint32_t i = 0; i < static_cast(pal::architecture::__last); ++i) + // Enumerate environment variables and filter for DOTNET_ + std::vector> env_vars; + bool found_complus_var = false; + pal::enumerate_environment_variables([&](const pal::char_t* name, const pal::char_t* value) { - pal::string_t env_var = get_dotnet_root_env_var_for_arch(static_cast(i)); - if (pal::getenv(env_var.c_str(), &value)) + // Check if the environment variable starts with DOTNET_ +#if defined(TARGET_WINDOWS) + // Environment variables are case-insensitive on Windows + auto comp_func = pal::strncasecmp; +#else + auto comp_func = pal::strncmp; +#endif + if (comp_func(name, _X("DOTNET_"), STRING_LENGTH("DOTNET_")) == 0) { - found_any = true; - trace::println(fmt, leading_whitespace, env_var.c_str(), value.c_str()); + env_vars.push_back(std::make_pair(name, value)); } + else if (!found_complus_var && comp_func(name, _X("COMPlus_"), STRING_LENGTH("COMPlus_")) == 0) + { + found_complus_var = true; + } + }); + + // Sort for consistent output + std::sort(env_vars.begin(), env_vars.end()); + + // Print all relevant environment variables + const pal::char_t* fmt = _X("%s%-40s [%s]"); + for (const auto& env_var : env_vars) + { + trace::println(fmt, leading_whitespace, env_var.first.c_str(), env_var.second.c_str()); } - return found_any; + if (found_complus_var) + trace::println(_X("%sDetected COMPlus_* environment variable(s). Consider transitioning to DOTNET_* equivalent."), leading_whitespace); + + return env_vars.size() > 0 || found_complus_var; } bool install_info::try_get_install_location(pal::architecture arch, pal::string_t& out_install_location, bool* out_is_registered) diff --git a/src/native/corehost/hostmisc/pal.h b/src/native/corehost/hostmisc/pal.h index 484aedef68fba2..334ab5a4a0f97e 100644 --- a/src/native/corehost/hostmisc/pal.h +++ b/src/native/corehost/hostmisc/pal.h @@ -17,6 +17,7 @@ #include #include #include +#include #if defined(_WIN32) @@ -305,6 +306,7 @@ namespace pal bool get_module_path(dll_t mod, string_t* recv); bool get_current_module(dll_t* mod); bool getenv(const char_t* name, string_t* recv); + void enumerate_environment_variables(const std::function callback); bool get_default_servicing_directory(string_t* recv); enum class architecture diff --git a/src/native/corehost/hostmisc/pal.unix.cpp b/src/native/corehost/hostmisc/pal.unix.cpp index 6a0922ff84159b..4a6197f541c4eb 100644 --- a/src/native/corehost/hostmisc/pal.unix.cpp +++ b/src/native/corehost/hostmisc/pal.unix.cpp @@ -957,6 +957,24 @@ bool pal::getenv(const pal::char_t* name, pal::string_t* recv) return (recv->length() > 0); } +extern char **environ; +void pal::enumerate_environment_variables(const std::function callback) +{ + if (environ == nullptr) + return; + + for (char **env = environ; *env != nullptr; ++env) + { + const char* current = *env; + const char* separator = ::strchr(current, '='); + if (separator != nullptr && separator != current) + { + pal::string_t name(current, separator - current); + callback(name.c_str(), separator + 1); + } + } +} + bool pal::fullpath(pal::string_t* path, bool skip_error_logging) { return realpath(path, skip_error_logging); diff --git a/src/native/corehost/hostmisc/pal.windows.cpp b/src/native/corehost/hostmisc/pal.windows.cpp index 2b9c88bd72f1e2..4561fef00d6543 100644 --- a/src/native/corehost/hostmisc/pal.windows.cpp +++ b/src/native/corehost/hostmisc/pal.windows.cpp @@ -714,6 +714,28 @@ bool pal::getenv(const char_t* name, string_t* recv) return true; } +void pal::enumerate_environment_variables(const std::function callback) +{ + LPWCH env_strings = ::GetEnvironmentStringsW(); + if (env_strings == nullptr) + return; + + LPWCH current = env_strings; + while (*current != L'\0') + { + LPWCH eq_ptr = ::wcschr(current, L'='); + if (eq_ptr != nullptr && eq_ptr != current) + { + pal::string_t name(current, eq_ptr - current); + callback(name.c_str(), eq_ptr + 1); + } + + current += pal::strlen(current) + 1; // Move to next string + } + + ::FreeEnvironmentStringsW(env_strings); +} + int pal::xtoi(const char_t* input) { return ::_wtoi(input);