diff --git a/src/installer/tests/HostActivation.Tests/FrameworkResolution/FrameworkResolution.cs b/src/installer/tests/HostActivation.Tests/FrameworkResolution/FrameworkResolution.cs index 42e724c8ad5356..b5e993a100a31d 100644 --- a/src/installer/tests/HostActivation.Tests/FrameworkResolution/FrameworkResolution.cs +++ b/src/installer/tests/HostActivation.Tests/FrameworkResolution/FrameworkResolution.cs @@ -103,6 +103,64 @@ public void FrameworkResolutionError_ListOtherArchitectures() } } + [Theory] + [InlineData("6.1.0", "", "6.1.3")] // Roll forward to 6.1.3 - empty value has no effect on resolution + [InlineData("6.1.0", " ", "6.1.3")] // Roll forward to 6.1.3 - whitespace value has no effect on resolution + [InlineData("6.1.0", "6.1.2", "6.1.3")] // Roll forward to 6.1.3 when 6.1.2 is disabled + [InlineData("6.1.0", "6.1.3", "6.1.2")] // Roll forward to 6.1.2 when 6.1.3 is disabled + [InlineData("6.1.3", "6.1.3", ResolvedFramework.NotFound)] // Fail when the only matching version is disabled + [InlineData("7.2.0", "7.2.3", ResolvedFramework.NotFound)] // Fail when the only matching version is disabled + [InlineData("6.1.0", "6.1.2;6.1.3", ResolvedFramework.NotFound)] // Fail when all matching versions are disabled + [InlineData("6.1.0", "invalid;6.1.3", "6.1.2")] // Roll forward to 6.1.2 - invalid value ignored, 6.1.3 disabled + [InlineData("6.1.0", "v6.1.3;;6.1.0", "6.1.3")] // Roll forward to 6.1.3 - invalid or non-existent versions have no effect on resolution + public void DisabledVersions(string requestedVersion, string disabledVersions, string expectedResolution) + { + CommandResult result = RunTest( + new TestSettings() + .WithRuntimeConfigCustomizer(rc => rc.WithFramework(MicrosoftNETCoreApp, requestedVersion)) + .WithEnvironment(Constants.DisableRuntimeVersions.EnvironmentVariable, disabledVersions)); + + result.ShouldHaveResolvedFrameworkOrFailToFind(MicrosoftNETCoreApp, expectedResolution); + if (string.IsNullOrWhiteSpace(disabledVersions)) + { + result.Should().NotHaveStdErrContaining($"Ignoring disabled version"); + } + else + { + foreach (string value in disabledVersions.Split(';')) + { + if (SharedState.InstalledVersions.Contains(value)) + { + result.Should().HaveStdErrContaining($"Ignoring disabled version [{value}]"); + if (expectedResolution == ResolvedFramework.NotFound) + { + result.Should().HaveStdErrContaining( + $""" + {value} at [{SharedState.InstalledDotNet.SharedFxPath}] + Disabled via {Constants.DisableRuntimeVersions.EnvironmentVariable} environment variable + """); + + } + } + } + } + } + + [Fact] + public void DisabledVersions_NoRollForward() + { + string disabledVersion = "6.1.2"; + CommandResult result = RunTest( + new TestSettings() + .WithRuntimeConfigCustomizer(rc => rc + .WithFramework(MicrosoftNETCoreApp, disabledVersion) + .WithRollForward(Constants.RollForwardSetting.Disable)) + .WithEnvironment(Constants.DisableRuntimeVersions.EnvironmentVariable, disabledVersion)); + + result.ShouldFailToFindCompatibleFrameworkVersion(MicrosoftNETCoreApp, disabledVersion) + .And.HaveStdErrContaining($"Ignoring disabled version [{disabledVersion}]"); + } + private CommandResult RunTest(TestSettings testSettings, [CallerMemberName] string caller = "") { return RunTest( diff --git a/src/installer/tests/HostActivation.Tests/HostCommands.cs b/src/installer/tests/HostActivation.Tests/HostCommands.cs index d7fb6d30cfd83a..5e5d03ba7f2282 100644 --- a/src/installer/tests/HostActivation.Tests/HostCommands.cs +++ b/src/installer/tests/HostActivation.Tests/HostCommands.cs @@ -241,6 +241,22 @@ public void ListRuntimes() .And.HaveStdOut(expectedOutput); } + [Fact] + public void ListRuntimes_DisabledVersions() + { + // Verify exact match of command output. The output of --list-runtimes is intended to be machine-readable + // and must not change in a way that breaks existing parsing. + string disabledVersion = SharedState.InstalledVersions[0]; + string[] expectedVersions = SharedState.InstalledVersions[1..]; + string expectedOutput = GetListRuntimesOutput(SharedState.DotNet.BinPath, expectedVersions); + SharedState.DotNet.Exec("--list-runtimes") + .CaptureStdOut() + .EnvironmentVariable(Constants.DisableRuntimeVersions.EnvironmentVariable, disabledVersion) + .Execute() + .Should().Pass() + .And.HaveStdOut(expectedOutput); + } + [Theory] [InlineData(true)] [InlineData(false)] diff --git a/src/installer/tests/HostActivation.Tests/NativeHostApis.cs b/src/installer/tests/HostActivation.Tests/NativeHostApis.cs index 35b73c5fc1dd2e..a7b4d553781dc8 100644 --- a/src/installer/tests/HostActivation.Tests/NativeHostApis.cs +++ b/src/installer/tests/HostActivation.Tests/NativeHostApis.cs @@ -317,26 +317,10 @@ public void Hostfxr_get_dotnet_environment_info_dotnet_root_only() string expectedSdkVersions = string.Join(";", f.LocalSdks); string expectedSdkPaths = string.Join(';', f.LocalSdkPaths); - string expectedFrameworkNames = string.Join(';', new[] - { - "HostFxr.Test.B", - "HostFxr.Test.B", - "HostFxr.Test.C" - }); - - string expectedFrameworkVersions = string.Join(';', new[] - { - "4.0.0", - "5.6.7-A", - "3.0.0" - }); - - string expectedFrameworkPaths = string.Join(';', new[] - { - Path.Combine(f.LocalFrameworksDir, "HostFxr.Test.B"), - Path.Combine(f.LocalFrameworksDir, "HostFxr.Test.B"), - Path.Combine(f.LocalFrameworksDir, "HostFxr.Test.C") - }); + IEnumerable<(string Name, string Version)> frameworks = f.LocalFrameworks.SelectMany(fw => fw.fwVersions.Select(v => (fw.fwName, v))); + string expectedFrameworkNames = string.Join(';', frameworks.Select(fw => fw.Name)); + string expectedFrameworkVersions = string.Join(';', frameworks.Select(fw => fw.Version)); + string expectedFrameworkPaths = string.Join(';', frameworks.Select(fw => Path.Combine(f.LocalFrameworksDir, fw.Name))); string api = ApiNames.hostfxr_get_dotnet_environment_info; TestContext.BuiltDotNet.Exec(sharedTestState.HostApiInvokerApp.AppDll, api, f.ExeDir) @@ -351,6 +335,40 @@ public void Hostfxr_get_dotnet_environment_info_dotnet_root_only() .And.HaveStdOutContaining($"{api} framework paths:[{expectedFrameworkPaths}]"); } + [Fact] + public void Hostfxr_get_dotnet_environment_info_DisabledVersions() + { + var f = sharedTestState.SdkAndFrameworkFixture; + string expectedSdkVersions = string.Join(";", f.LocalSdks); + string expectedSdkPaths = string.Join(';', f.LocalSdkPaths); + + string[] disabledVersions = [ "4.0.0", "3.0.0"]; + IEnumerable<(string Name, string Version)> frameworks = f.LocalFrameworks + .SelectMany(fw => fw.fwVersions + .Where(v => !disabledVersions.Contains(v)) + .Select(v => (fw.fwName, v))); + string expectedFrameworkNames = string.Join(';', frameworks.Select(fw => fw.Name)); + string expectedFrameworkVersions = string.Join(';', frameworks.Select(fw => fw.Version)); + string expectedFrameworkPaths = string.Join(';', frameworks.Select(fw => Path.Combine(f.LocalFrameworksDir, fw.Name))); + + string api = ApiNames.hostfxr_get_dotnet_environment_info; + var result = TestContext.BuiltDotNet.Exec(sharedTestState.HostApiInvokerApp.AppDll, api, f.ExeDir) + .EnableTracingAndCaptureOutputs() + .EnvironmentVariable(Constants.DisableRuntimeVersions.EnvironmentVariable, string.Join(';', disabledVersions)) + .Execute(); + result.Should().Pass() + .And.ReturnStatusCode(api, Constants.ErrorCode.Success) + .And.HaveStdOutContaining($"{api} sdk versions:[{expectedSdkVersions}]") + .And.HaveStdOutContaining($"{api} sdk paths:[{expectedSdkPaths}]") + .And.HaveStdOutContaining($"{api} framework names:[{expectedFrameworkNames}]") + .And.HaveStdOutContaining($"{api} framework versions:[{expectedFrameworkVersions}]") + .And.HaveStdOutContaining($"{api} framework paths:[{expectedFrameworkPaths}]"); + foreach (string version in disabledVersions) + { + result.Should().HaveStdErrContaining($"Ignoring disabled version [{version}]"); + } + } + [Fact] public void Hostfxr_get_dotnet_environment_info_global_install_path() { diff --git a/src/installer/tests/TestUtils/Constants.cs b/src/installer/tests/TestUtils/Constants.cs index 4d3be90319ad15..e965aa55e7c03a 100644 --- a/src/installer/tests/TestUtils/Constants.cs +++ b/src/installer/tests/TestUtils/Constants.cs @@ -35,6 +35,11 @@ public static class RollForwardSetting public const string Disable = "Disable"; } + public static class DisableRuntimeVersions + { + public const string EnvironmentVariable = "DOTNET_DISABLE_RUNTIME_VERSIONS"; + } + public static class FxVersion { public const string CommandLineArgument = "--fx-version"; diff --git a/src/native/corehost/fxr/framework_info.cpp b/src/native/corehost/fxr/framework_info.cpp index a7912c2b18627f..4e19a036643b08 100644 --- a/src/native/corehost/fxr/framework_info.cpp +++ b/src/native/corehost/fxr/framework_info.cpp @@ -3,6 +3,7 @@ #include #include "framework_info.h" +#include "fx_resolver.h" #include "pal.h" #include "trace.h" #include "utils.h" @@ -36,13 +37,15 @@ bool compare_by_name_and_version(const framework_info &a, const framework_info & const pal::string_t& dotnet_dir, const pal::char_t* fx_name, bool disable_multilevel_lookup, + bool include_disabled_versions, std::vector* framework_infos) { std::vector hive_dir; get_framework_locations(dotnet_dir, disable_multilevel_lookup, &hive_dir); - int32_t hive_depth = 0; + std::vector disabled_versions = fx_resolver_t::get_disabled_versions(); + int32_t hive_depth = 0; for (const pal::string_t& dir : hive_dir) { auto fx_shared_dir = dir; @@ -92,9 +95,16 @@ bool compare_by_name_and_version(const framework_info &a, const framework_info & continue; } + bool is_disabled = std::find(disabled_versions.begin(), disabled_versions.end(), ver) != disabled_versions.end(); + if (is_disabled && !include_disabled_versions) + { + trace::verbose(_X("Ignoring disabled version [%s]"), ver.c_str()); + continue; + } + trace::verbose(_X("Found FX version [%s]"), ver.c_str()); - framework_info info(fx_name_local, fx_dir, parsed, hive_depth); + framework_info info(fx_name_local, fx_dir, parsed, hive_depth, is_disabled); framework_infos->push_back(info); } } @@ -110,7 +120,7 @@ bool compare_by_name_and_version(const framework_info &a, const framework_info & assert(leading_whitespace != nullptr); std::vector framework_infos; - get_all_framework_infos(dotnet_dir, nullptr, /*disable_multilevel_lookup*/ true, &framework_infos); + get_all_framework_infos(dotnet_dir, nullptr, /*disable_multilevel_lookup*/ true, /*include_disabled_versions*/ false, &framework_infos); for (framework_info info : framework_infos) { trace::println(_X("%s%s %s [%s]"), leading_whitespace, info.name.c_str(), info.version.as_str().c_str(), info.path.c_str()); diff --git a/src/native/corehost/fxr/framework_info.h b/src/native/corehost/fxr/framework_info.h index 736b6debbea2f4..3c63759e78fd72 100644 --- a/src/native/corehost/fxr/framework_info.h +++ b/src/native/corehost/fxr/framework_info.h @@ -9,16 +9,19 @@ struct framework_info { - framework_info(pal::string_t name, pal::string_t path, fx_ver_t version, int32_t hive_depth) + framework_info(pal::string_t name, pal::string_t path, fx_ver_t version, int32_t hive_depth, bool disabled) : name(name) , path(path) , version(version) - , hive_depth(hive_depth) { } + , hive_depth(hive_depth) + , disabled(disabled) + { } static void get_all_framework_infos( const pal::string_t& dotnet_dir, const pal::char_t* fx_name, bool disable_multilevel_lookup, + bool include_disabled_versions, std::vector* framework_infos); static bool print_all_frameworks(const pal::string_t& dotnet_dir, const pal::char_t* leading_whitespace); @@ -27,6 +30,7 @@ struct framework_info pal::string_t path; fx_ver_t version; int32_t hive_depth; + bool disabled; }; #endif // __FRAMEWORK_INFO_H_ diff --git a/src/native/corehost/fxr/fx_resolver.cpp b/src/native/corehost/fxr/fx_resolver.cpp index af970d7c56b204..bdd3e17833e578 100644 --- a/src/native/corehost/fxr/fx_resolver.cpp +++ b/src/native/corehost/fxr/fx_resolver.cpp @@ -188,7 +188,8 @@ namespace const fx_reference_t & fx_ref, const pal::string_t & oldest_requested_version, const pal::string_t & dotnet_dir, - const bool disable_multilevel_lookup) + const bool disable_multilevel_lookup, + const std::vector& disabled_versions) { #if defined(DEBUG) assert(!fx_ref.get_fx_name().empty()); @@ -236,6 +237,12 @@ namespace append_path(&fx_dir, fx_ref.get_fx_version().c_str()); if (file_exists_in_dir(fx_dir, deps_file_name.c_str(), nullptr)) { + if (std::find(disabled_versions.begin(), disabled_versions.end(), fx_ref.get_fx_version()) != disabled_versions.end()) + { + trace::verbose(_X("Ignoring disabled version [%s]"), fx_ref.get_fx_version().c_str()); + continue; + } + selected_fx_dir = fx_dir; selected_fx_version = fx_ref.get_fx_version(); break; @@ -252,6 +259,12 @@ namespace fx_ver_t ver; if (fx_ver_t::parse(version, &ver, false)) { + if (std::find(disabled_versions.begin(), disabled_versions.end(), version) != disabled_versions.end()) + { + trace::verbose(_X("Ignoring disabled version [%s]"), version.c_str()); + continue; + } + version_list.push_back(ver); } } @@ -307,6 +320,40 @@ namespace } } +// static +std::vector fx_resolver_t::get_disabled_versions() +{ + pal::string_t env_var; + if (!pal::getenv(_X("DOTNET_DISABLE_RUNTIME_VERSIONS"), &env_var) || env_var.empty()) + return {}; + + std::vector disabled_versions; + size_t start = 0; + size_t pos = 0; + while ((pos = env_var.find(_X(';'), start)) != pal::string_t::npos) + { + if (pos > start) + { + disabled_versions.emplace_back(env_var, start, pos - start); + } + start = pos + 1; + } + + // Add the last version (after the last semicolon or the only version if no semicolons) + if (start < env_var.size()) + { + disabled_versions.emplace_back(env_var, start, env_var.size() - start); + } + + return disabled_versions; +} + +fx_resolver_t::fx_resolver_t(bool disable_multilevel_lookup, const runtime_config_t::settings_t& override_settings) + : m_disable_multilevel_lookup{disable_multilevel_lookup} + , m_override_settings{override_settings} + , m_disabled_versions{get_disabled_versions()} +{ } + // Reconciles two framework references into a new effective framework reference // This process is sometimes also called "soft roll forward" (soft as in no IO) // - fx_ref_a - one of the framework references to reconcile @@ -438,7 +485,7 @@ StatusCode fx_resolver_t::read_framework( m_effective_fx_references[fx_name] = new_effective_fx_ref; // Resolve the effective framework reference against the existing physical framework folders - std::unique_ptr fx = resolve_framework_reference(new_effective_fx_ref, m_oldest_fx_references[fx_name].get_fx_version(), dotnet_root, m_disable_multilevel_lookup); + std::unique_ptr fx = resolve_framework_reference(new_effective_fx_ref, m_oldest_fx_references[fx_name].get_fx_version(), dotnet_root, m_disable_multilevel_lookup, m_disabled_versions); if (fx == nullptr) { resolution_failure.missing = std::move(new_effective_fx_ref); diff --git a/src/native/corehost/fxr/fx_resolver.h b/src/native/corehost/fxr/fx_resolver.h index 9abfd706baadc1..dd0eec70c91c72 100644 --- a/src/native/corehost/fxr/fx_resolver.h +++ b/src/native/corehost/fxr/fx_resolver.h @@ -40,11 +40,10 @@ class fx_resolver_t const runtime_config_t& config, const std::unordered_map &existing_framework_versions_by_name); + static std::vector get_disabled_versions(); + private: - fx_resolver_t(bool disable_multilevel_lookup, const runtime_config_t::settings_t& override_settings) - : m_disable_multilevel_lookup{disable_multilevel_lookup} - , m_override_settings{override_settings} - { } + fx_resolver_t(bool disable_multilevel_lookup, const runtime_config_t::settings_t& override_settings); void update_newest_references( const runtime_config_t& config); @@ -97,6 +96,9 @@ class fx_resolver_t bool m_disable_multilevel_lookup; const runtime_config_t::settings_t& m_override_settings; + + // Disabled runtime versions + std::vector m_disabled_versions; }; #endif // __FX_RESOLVER_H__ diff --git a/src/native/corehost/fxr/fx_resolver.messages.cpp b/src/native/corehost/fxr/fx_resolver.messages.cpp index a922858ae5b390..3b98fa145bac4b 100644 --- a/src/native/corehost/fxr/fx_resolver.messages.cpp +++ b/src/native/corehost/fxr/fx_resolver.messages.cpp @@ -110,13 +110,17 @@ void fx_resolver_t::display_missing_framework_error( trace::error(_X(".NET location: %s\n"), dotnet_root.c_str()); std::vector framework_infos; - framework_info::get_all_framework_infos(dotnet_root, fx_name.c_str(), disable_multilevel_lookup, &framework_infos); + framework_info::get_all_framework_infos(dotnet_root, fx_name.c_str(), disable_multilevel_lookup, /*include_disabled_versions*/ true, &framework_infos); if (framework_infos.size()) { trace::error(_X("The following frameworks were found:")); for (const framework_info& info : framework_infos) { trace::error(_X(" %s at [%s]"), info.version.as_str().c_str(), info.path.c_str()); + if (info.disabled) + { + trace::error(_X(" Disabled via DOTNET_DISABLE_RUNTIME_VERSIONS environment variable")); + } } } else @@ -129,7 +133,7 @@ void fx_resolver_t::display_missing_framework_error( [&](pal::architecture arch, const pal::string_t& install_location, bool is_registered) { std::vector other_arch_infos; - framework_info::get_all_framework_infos(install_location, fx_name.c_str(), disable_multilevel_lookup, &other_arch_infos); + framework_info::get_all_framework_infos(install_location, fx_name.c_str(), disable_multilevel_lookup, /*include_disabled_versions*/ true, &other_arch_infos); if (!other_arch_infos.empty()) { other_arch_framework_infos.push_back(std::make_pair(arch, std::move(other_arch_infos))); @@ -144,6 +148,10 @@ void fx_resolver_t::display_missing_framework_error( for (const framework_info& info : arch_info_pair.second) { trace::error(_X(" %s at [%s]"), info.version.as_str().c_str(), info.path.c_str()); + if (info.disabled) + { + trace::error(_X(" Disabled via DOTNET_DISABLE_RUNTIME_VERSIONS environment variable")); + } } } } diff --git a/src/native/corehost/fxr/hostfxr.cpp b/src/native/corehost/fxr/hostfxr.cpp index 1b3438c05b4d28..e6464fc478e6d3 100644 --- a/src/native/corehost/fxr/hostfxr.cpp +++ b/src/native/corehost/fxr/hostfxr.cpp @@ -437,7 +437,7 @@ SHARED_API int32_t HOSTFXR_CALLTYPE hostfxr_get_dotnet_environment_info( } std::vector framework_infos; - framework_info::get_all_framework_infos(dotnet_dir, nullptr, /*disable_multilevel_lookup*/ true, &framework_infos); + framework_info::get_all_framework_infos(dotnet_dir, nullptr, /*disable_multilevel_lookup*/ true, /*include_disabled_versions*/ false, &framework_infos); std::vector environment_framework_infos; std::vector framework_versions;