Skip to content

Commit 2ff366b

Browse files
authored
Make dotnet --info print all DOTNET_* environment variables (#117797)
`dotnet --info` currently only prints `DOTNET_ROOT` / `DOTNET_ROOT_<ARCH>` if they are set. Expand it to all environment variables with a `DOTNET_` prefix. Also add a line about detecting COMPlus_* variables. Example: ``` Environment variables: DOTNET_ROLL_FORWARD [Major] DOTNET_ROOT [C:\repos\runtime\.dotnet] DOTNET_SKIP_FIRST_TIME_EXPERIENCE [1] Detected COMPlus_* environment variable(s). Consider transitioning to DOTNET_* equivalent. ```
1 parent b019cbd commit 2ff366b

File tree

6 files changed

+161
-52
lines changed

6 files changed

+161
-52
lines changed

src/installer/tests/HostActivation.Tests/HostCommands.cs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,91 @@ public void Info_Utf8Path()
8383
.And.HaveStdOutMatching($@"DOTNET_ROOT.*{installLocation}");
8484
}
8585

86+
[Fact]
87+
public void Info_ListEnvironment()
88+
{
89+
var command = TestContext.BuiltDotNet.Exec("--info")
90+
.CaptureStdOut();
91+
92+
// Add DOTNET_ROOT environment variables
93+
(string Architecture, string Path)[] dotnetRootEnvVars = [
94+
("arm64", "/arm64/dotnet/root"),
95+
("x64", "/x64/dotnet/root"),
96+
("x86", "/x86/dotnet/root"),
97+
("unknown", "/unknown/dotnet/root")
98+
];
99+
foreach (var envVar in dotnetRootEnvVars)
100+
{
101+
command = command.DotNetRoot(envVar.Path, envVar.Architecture);
102+
}
103+
104+
string dotnetRootNoArch = "/dotnet/root";
105+
command = command.DotNetRoot(dotnetRootNoArch);
106+
107+
// Add additional DOTNET_* environment variables
108+
(string Name, string Value)[] envVars = [
109+
("DOTNET_ROLL_FORWARD", "Major"),
110+
("DOTNET_SOME_SETTING", "/some/setting"),
111+
("DOTNET_HOST_TRACE", "1")
112+
];
113+
114+
(string Name, string Value)[] differentCaseEnvVars = [
115+
("dotnet_env_var", "dotnet env var value"),
116+
("dOtNeT_setting", "doOtNeT setting value"),
117+
];
118+
foreach ((string name, string value) in envVars.Concat(differentCaseEnvVars))
119+
{
120+
command = command.EnvironmentVariable(name, value);
121+
}
122+
123+
string otherEnvVar = "OTHER";
124+
command = command.EnvironmentVariable(otherEnvVar, "value");
125+
126+
var result = command.Execute();
127+
result.Should().Pass()
128+
.And.HaveStdOutContaining("Environment variables:")
129+
.And.HaveStdOutMatching($@"{Constants.DotnetRoot.EnvironmentVariable}\s*\[{dotnetRootNoArch}\]")
130+
.And.NotHaveStdOutContaining(otherEnvVar);
131+
132+
foreach ((string architecture, string path) in dotnetRootEnvVars)
133+
{
134+
result.Should()
135+
.HaveStdOutMatching($@"{Constants.DotnetRoot.ArchitectureEnvironmentVariablePrefix}{architecture.ToUpper()}\s*\[{path}\]");
136+
}
137+
138+
foreach ((string name, string value) in envVars)
139+
{
140+
result.Should().HaveStdOutMatching($@"{name}\s*\[{value}\]");
141+
}
142+
143+
foreach ((string name, string value) in differentCaseEnvVars)
144+
{
145+
if (OperatingSystem.IsWindows())
146+
{
147+
// Environment variables are case-insensitive on Windows
148+
result.Should().HaveStdOutMatching($@"{name}\s*\[{value}\]");
149+
}
150+
else
151+
{
152+
result.Should().NotHaveStdOutContaining(name);
153+
}
154+
}
155+
}
156+
157+
[Fact]
158+
public void Info_ListEnvironment_LegacyPrefixDetection()
159+
{
160+
string comPlusEnvVar = "COMPlus_ReadyToRun";
161+
TestContext.BuiltDotNet.Exec("--info")
162+
.EnvironmentVariable(comPlusEnvVar, "0")
163+
.CaptureStdOut()
164+
.Execute()
165+
.Should().Pass()
166+
.And.HaveStdOutContaining("Environment variables:")
167+
.And.NotHaveStdOutContaining(comPlusEnvVar)
168+
.And.HaveStdOutContaining("Detected COMPlus_* environment variable(s). Consider transitioning to DOTNET_* equivalent.");
169+
}
170+
86171
[Fact]
87172
public void ListRuntimes()
88173
{

src/installer/tests/HostActivation.Tests/InstallLocation.cs

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -121,42 +121,6 @@ public void EnvironmentVariable_DotnetRootPathExistsButHasNoHost()
121121
.And.HaveStdErrContaining($"The required library {Binaries.HostFxr.FileName} could not be found.");
122122
}
123123

124-
[Fact]
125-
public void EnvironmentVariable_DotNetInfo_ListEnvironment()
126-
{
127-
var command = TestContext.BuiltDotNet.Exec("--info")
128-
.CaptureStdOut();
129-
130-
var envVars = new (string Architecture, string Path)[] {
131-
("arm64", "/arm64/dotnet/root"),
132-
("x64", "/x64/dotnet/root"),
133-
("x86", "/x86/dotnet/root")
134-
};
135-
foreach(var envVar in envVars)
136-
{
137-
command = command.DotNetRoot(envVar.Path, envVar.Architecture);
138-
}
139-
140-
string dotnetRootNoArch = "/dotnet/root";
141-
command = command.DotNetRoot(dotnetRootNoArch);
142-
143-
(string Architecture, string Path) unknownEnvVar = ("unknown", "/unknown/dotnet/root");
144-
command = command.DotNetRoot(unknownEnvVar.Path, unknownEnvVar.Architecture);
145-
146-
var result = command.Execute();
147-
result.Should().Pass()
148-
.And.HaveStdOutContaining("Environment variables:")
149-
.And.HaveStdOutMatching($@"{Constants.DotnetRoot.EnvironmentVariable}\s*\[{dotnetRootNoArch}\]")
150-
.And.NotHaveStdOutContaining($"{Constants.DotnetRoot.ArchitectureEnvironmentVariablePrefix}{unknownEnvVar.Architecture.ToUpper()}")
151-
.And.NotHaveStdOutContaining($"[{unknownEnvVar.Path}]");
152-
153-
foreach ((string architecture, string path) in envVars)
154-
{
155-
result.Should()
156-
.HaveStdOutMatching($@"{Constants.DotnetRoot.ArchitectureEnvironmentVariablePrefix}{architecture.ToUpper()}\s*\[{path}\]");
157-
}
158-
}
159-
160124
[Fact]
161125
public void DefaultInstallLocation()
162126
{

src/native/corehost/fxr/install_info.cpp

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,47 @@
66
#include "trace.h"
77
#include "utils.h"
88

9+
#include <algorithm>
10+
#include <vector>
11+
912
bool install_info::print_environment(const pal::char_t* leading_whitespace)
1013
{
11-
bool found_any = false;
12-
13-
const pal::char_t* fmt = _X("%s%-17s [%s]");
14-
pal::string_t value;
15-
if (pal::getenv(DOTNET_ROOT_ENV_VAR, &value))
16-
{
17-
found_any = true;
18-
trace::println(fmt, leading_whitespace, DOTNET_ROOT_ENV_VAR, value.c_str());
19-
}
20-
21-
for (uint32_t i = 0; i < static_cast<uint32_t>(pal::architecture::__last); ++i)
14+
// Enumerate environment variables and filter for DOTNET_
15+
std::vector<std::pair<pal::string_t, pal::string_t>> env_vars;
16+
bool found_complus_var = false;
17+
pal::enumerate_environment_variables([&](const pal::char_t* name, const pal::char_t* value)
2218
{
23-
pal::string_t env_var = get_dotnet_root_env_var_for_arch(static_cast<pal::architecture>(i));
24-
if (pal::getenv(env_var.c_str(), &value))
19+
// Check if the environment variable starts with DOTNET_
20+
#if defined(TARGET_WINDOWS)
21+
// Environment variables are case-insensitive on Windows
22+
auto comp_func = pal::strncasecmp;
23+
#else
24+
auto comp_func = pal::strncmp;
25+
#endif
26+
if (comp_func(name, _X("DOTNET_"), STRING_LENGTH("DOTNET_")) == 0)
2527
{
26-
found_any = true;
27-
trace::println(fmt, leading_whitespace, env_var.c_str(), value.c_str());
28+
env_vars.push_back(std::make_pair(name, value));
2829
}
30+
else if (!found_complus_var && comp_func(name, _X("COMPlus_"), STRING_LENGTH("COMPlus_")) == 0)
31+
{
32+
found_complus_var = true;
33+
}
34+
});
35+
36+
// Sort for consistent output
37+
std::sort(env_vars.begin(), env_vars.end());
38+
39+
// Print all relevant environment variables
40+
const pal::char_t* fmt = _X("%s%-40s [%s]");
41+
for (const auto& env_var : env_vars)
42+
{
43+
trace::println(fmt, leading_whitespace, env_var.first.c_str(), env_var.second.c_str());
2944
}
3045

31-
return found_any;
46+
if (found_complus_var)
47+
trace::println(_X("%sDetected COMPlus_* environment variable(s). Consider transitioning to DOTNET_* equivalent."), leading_whitespace);
48+
49+
return env_vars.size() > 0 || found_complus_var;
3250
}
3351

3452
bool install_info::try_get_install_location(pal::architecture arch, pal::string_t& out_install_location, bool* out_is_registered)

src/native/corehost/hostmisc/pal.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <memory>
1818
#include <algorithm>
1919
#include <cassert>
20+
#include <functional>
2021

2122
#if defined(_WIN32)
2223

@@ -305,6 +306,7 @@ namespace pal
305306
bool get_module_path(dll_t mod, string_t* recv);
306307
bool get_current_module(dll_t* mod);
307308
bool getenv(const char_t* name, string_t* recv);
309+
void enumerate_environment_variables(const std::function<void(const char_t*, const char_t*)> callback);
308310
bool get_default_servicing_directory(string_t* recv);
309311

310312
enum class architecture

src/native/corehost/hostmisc/pal.unix.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -957,6 +957,24 @@ bool pal::getenv(const pal::char_t* name, pal::string_t* recv)
957957
return (recv->length() > 0);
958958
}
959959

960+
extern char **environ;
961+
void pal::enumerate_environment_variables(const std::function<void(const pal::char_t*, const pal::char_t*)> callback)
962+
{
963+
if (environ == nullptr)
964+
return;
965+
966+
for (char **env = environ; *env != nullptr; ++env)
967+
{
968+
const char* current = *env;
969+
const char* separator = ::strchr(current, '=');
970+
if (separator != nullptr && separator != current)
971+
{
972+
pal::string_t name(current, separator - current);
973+
callback(name.c_str(), separator + 1);
974+
}
975+
}
976+
}
977+
960978
bool pal::fullpath(pal::string_t* path, bool skip_error_logging)
961979
{
962980
return realpath(path, skip_error_logging);

src/native/corehost/hostmisc/pal.windows.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,28 @@ bool pal::getenv(const char_t* name, string_t* recv)
714714
return true;
715715
}
716716

717+
void pal::enumerate_environment_variables(const std::function<void(const pal::char_t*, const pal::char_t*)> callback)
718+
{
719+
LPWCH env_strings = ::GetEnvironmentStringsW();
720+
if (env_strings == nullptr)
721+
return;
722+
723+
LPWCH current = env_strings;
724+
while (*current != L'\0')
725+
{
726+
LPWCH eq_ptr = ::wcschr(current, L'=');
727+
if (eq_ptr != nullptr && eq_ptr != current)
728+
{
729+
pal::string_t name(current, eq_ptr - current);
730+
callback(name.c_str(), eq_ptr + 1);
731+
}
732+
733+
current += pal::strlen(current) + 1; // Move to next string
734+
}
735+
736+
::FreeEnvironmentStringsW(env_strings);
737+
}
738+
717739
int pal::xtoi(const char_t* input)
718740
{
719741
return ::_wtoi(input);

0 commit comments

Comments
 (0)