Skip to content

Commit

Permalink
Disable multi-level lookup by default (#67022)
Browse files Browse the repository at this point in the history
  • Loading branch information
elinor-fung authored Mar 25, 2022
1 parent 32320b1 commit 5098d45
Show file tree
Hide file tree
Showing 37 changed files with 545 additions and 377 deletions.
2 changes: 1 addition & 1 deletion docs/design/features/framework-version-resolution.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Framework version resolution

This document describes .NET Core 3.0 version resolution behavior when the host resolves framework references for framework dependent apps.
It's just a part of the overall framework resolution scenario described in [multilevel-sharedfx-lookup](multilevel-sharedfx-lookup.md).
It's just a part of the overall framework resolution scenario described in [sharedfx-lookup](sharedfx-lookup.md).

## Framework references
Application defines its framework dependencies in its `.runtimeconfig.json` file. Each framework then defines its dependencies in its copy of `.runtimeconfig.json`. Each dependency is expressed as a framework reference. Together these form a graph. The host must resolve the references by finding the actual frameworks which are available on the machine. It must also unify references if there are multi references to the same framework.
Expand Down
2 changes: 1 addition & 1 deletion docs/design/features/host-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The entry-point typically does just one thing: it finds the `hostfxr` library an
## Host FXR
This library finds and resolves the runtime and all the frameworks the app needs. Then it loads the `hostpolicy` library and transfers control to it.

The host FXR library reads the `.runtimeconfig.json` of the app (and all it's dependent frameworks) and resolves the frameworks. It implements the algorithm for framework resolution as described in [SharedFX Lookup](multilevel-sharedfx-lookup.md) and in [Framework version resolution](framework-version-resolution.md).
The host FXR library reads the `.runtimeconfig.json` of the app (and all it's dependent frameworks) and resolves the frameworks. It implements the algorithm for framework resolution as described in [SharedFX Lookup](sharedfx-lookup.md) and in [Framework version resolution](framework-version-resolution.md).

In most cases the latest available version of `hostfxr` is used. Self-contained apps use `hostfxr` from the app folder.

Expand Down
2 changes: 1 addition & 1 deletion docs/design/features/host-probing.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ The list of probing paths ordered according to their priority. First path in the
If the app (or framework) has dependencies on frameworks, these frameworks are used as probing paths.
The order is from the higher level framework to lower level framework. The app is considered the highest level, it direct dependencies are next and so on.
For assets from frameworks, only that framework and lower level frameworks are considered.
Note: These directories come directly out of the framework resolution process. Special note on Windows where global locations are always considered even if the app is not executed via the shared `dotnet.exe`. More details can be found in [Multi-level Shared FX Lookup](multilevel-sharedfx-lookup.md).
Note: These directories come directly out of the framework resolution process. Special note on Windows where global locations are always considered even if the app is not executed via the shared `dotnet.exe`. More details can be found in [Shared FX Lookup](sharedfx-lookup.md).
* Shared store paths
* `$DOTNET_SHARED_STORE/|arch|/|tfm|` - The environment variable `DOTNET_SHARED_STORE` can contain multiple paths, in which case each is appended with `|arch|/|tfm|` and used as a probing path.
* If the app is executed through `dotnet.exe` then path relative to the directory with the `dotnet.exe` is used
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Multi-level SharedFX Lookup
# SharedFX Lookup

## Introduction

There are two possible ways of running .NET Core Applications: through dotnet.exe or through a custom executable appname.exe. The first one is used when the user wants to run a framework-dependent app or a .NET Core command while the second one is used for self-contained applications. Both executables share exactly the same source code.
There are two main ways of running .NET Applications: through `dotnet` or through the `apphost` executables. The executable is in charge of finding and loading `hostfxr`. `hostfxr`, in turn, must find and load `hostpolicy`. It is also responsible for searching for the SDK when running .NET SDK commands. Finally, `hostpolicy` must find and load the runtime (`coreclr`). See [host components](host-components.md) for details.

The executable is in charge of finding and loading the hostfxr.dll file. The hostfxr, in turn, must find and load the hostpolicy.dll file (it’s also responsible for searching for the SDK when running .NET commands). At last the coreclr.dll file must be found and loaded by the hostpolicy. Self-contained apps are supposed to keep all its dependencies in the same location as the executable. Framework-dependent apps must have the runtime files inside predefined folders.
An application can either be [framework-dependent](https://docs.microsoft.com/dotnet/core/deploying/#publish-framework-dependent) or [self-contained](https://docs.microsoft.com/dotnet/core/deploying/#publish-self-contained). Framework-dependent apps must have the runtime files inside predefined folders. Self-contained apps are expected to have their dependencies in the same location as the executable.

## Semantic Versioning

Expand Down Expand Up @@ -219,6 +219,16 @@ In order to compare versions of an assembly, the assemblyVersion and fileVersion

## Global locations

Global install locations are described in the [install locations design](https://github.com/dotnet/designs/blob/main/accepted/2020/install-locations.md) document.

**.NET 7.0 and above**

When running `dotnet`, only the executable directory will be searched and global locations are not searched. For all other [entry-point hosts](host-components.md#entry-point-hosts), if the `DOTNET_ROOT` environment variable is set, that path is searched. If the environment variable is not set, the global location as described in [install locations](https://github.com/dotnet/designs/blob/main/accepted/2020/install-locations.md) is searched.

See [disable multi-level lookup](https://github.com/dotnet/designs/blob/main/accepted/2022/disable-multi-level-lookup-by-default.md) for more details.

**Before .NET 7.0**

In addition to searching the executable directory, the global .NET location is also searched. The global folders may vary depending on the running operational system. They are defined as follows:

Global .NET location:
Expand Down Expand Up @@ -271,11 +281,19 @@ To make sure that the changes are working correctly, the following behavior cond

### SDK search

Like the Framework search, the SDK is searched for a compatible version. Instead of looking for it only in relation to the executable directory, it is also searched in the folders specified above by following the same priority rank.
Like the Framework search, the SDK is searched for a compatible version.

Unlike the Framework search, the SDK search does a roll-forward for pre-release versions when the patch version changes. For example, if you install v2.0.1-pre, it will be used over v2.0.0.

**.NET 7.0 and above**

Only the executable directory will be searched. See [disable multi-level lookup](https://github.com/dotnet/designs/blob/main/accepted/2022/disable-multi-level-lookup-by-default.md) for more details.

**Before .NET 7.0**

Aside from looking for it in relation to the executable directory, it is also searched in the folders specified above by following the same priority rank.

The search is conducted as follows:

1. In relation to the executable directory: search for the specified version. If it cannot be found, choose the most appropriate available version. If there’s no available version, proceed to the next step.
2. In relation to the global location: search for the specified version. If it cannot be found, choose the most appropriate available version. If there’s no available version, then we were not able to find any version folder and an error message is returned.

Unlike the Framework search, the SDK search does a roll-forward for pre-release versions when the patch version changes. For example, if you install v2.0.1-pre, it will be used over v2.0.0.
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public void InvalidFileOrCommand_NoSDK_ListsPossibleIssues()
.Execute(fExpectedToFail: true)
.Should().Fail()
.And.HaveStdErrContaining($"The application '{fileName}' does not exist")
.And.HaveStdErrContaining($"It was not possible to find any installed .NET SDKs");
.And.FindAnySdk(false);
}

// Return a non-exisitent path that contains a mix of / and \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,26 +35,32 @@ public static AndConstraint<CommandResultAssertions> ShouldHaveResolvedFramework
/// <returns>Constraint</returns>
public static AndConstraint<CommandResultAssertions> ShouldHaveResolvedFrameworkOrFailToFind(this CommandResult result, string resolvedFrameworkName, string resolvedFrameworkVersion, string resolvedFrameworkBasePath = null)
{
if (resolvedFrameworkName == null || resolvedFrameworkVersion == null ||
if (resolvedFrameworkName == null || resolvedFrameworkVersion == null ||
resolvedFrameworkVersion == FrameworkResolutionBase.ResolvedFramework.NotFound)
{
return result.ShouldFailToFindCompatibleFrameworkVersion();
return result.ShouldFailToFindCompatibleFrameworkVersion(resolvedFrameworkName);
}
else
{
return result.ShouldHaveResolvedFramework(resolvedFrameworkName, resolvedFrameworkVersion, resolvedFrameworkBasePath);
}
}

public static AndConstraint<CommandResultAssertions> DidNotFindCompatibleFrameworkVersion(this CommandResultAssertions assertion)
public static AndConstraint<CommandResultAssertions> DidNotFindCompatibleFrameworkVersion(this CommandResultAssertions assertion, string frameworkName, string requestedVersion)
{
return assertion.HaveStdErrContaining("It was not possible to find any compatible framework version");
var constraint = assertion.HaveStdErrContaining("You must install or update .NET to run this application.");
if (frameworkName is not null)
{
constraint = constraint.And.HaveStdErrContaining($"Framework: '{frameworkName}', {(requestedVersion is null ? "" : $"version '{requestedVersion}'")}");
}

return constraint;
}

public static AndConstraint<CommandResultAssertions> ShouldFailToFindCompatibleFrameworkVersion(this CommandResult result)
public static AndConstraint<CommandResultAssertions> ShouldFailToFindCompatibleFrameworkVersion(this CommandResult result, string frameworkName, string requestedVersion = null)
{
return result.Should().Fail()
.And.DidNotFindCompatibleFrameworkVersion();
.And.DidNotFindCompatibleFrameworkVersion(frameworkName, requestedVersion);
}

public static AndConstraint<CommandResultAssertions> FailedToReconcileFrameworkReference(
Expand Down Expand Up @@ -96,7 +102,7 @@ public static AndConstraint<CommandResultAssertions> ShouldHaveResolvedFramework
}
else if (resolvedVersion == FrameworkResolutionBase.ResolvedFramework.NotFound)
{
return result.ShouldFailToFindCompatibleFrameworkVersion();
return result.ShouldFailToFindCompatibleFrameworkVersion(frameworkName, null);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public MultipleHives(SharedTestState sharedState)
[Theory]
// MLL where global hive has a better match
[InlineData("5.0.0", "net5.0", true, "5.1.2")]
[InlineData("5.0.0", "net5.0", null, "5.1.2")] // MLL is on by default, so same as true
[InlineData("5.0.0", "net5.0", null, "5.1.2")] // MLL is on by default before 7.0, so same as true
[InlineData("5.0.0", "net5.0", false, "5.2.0")] // No global hive allowed
// MLL (where global hive has better match) with various TFMs
[InlineData("5.0.0", "netcoreapp3.0", true, "5.1.2")]
Expand All @@ -37,11 +37,12 @@ public MultipleHives(SharedTestState sharedState)
[InlineData("5.0.0", "net6.0", true, "5.1.2")]
[InlineData("5.0.0", "net6.0", null, "5.1.2")]
[InlineData("5.0.0", "net6.0", false, "5.2.0")]
[InlineData("7.0.0", "net7.0", true, "7.0.1")]
[InlineData("7.0.0", "net7.0", null, "7.0.1")]
// MLL is disabled for 7.0+
[InlineData("7.0.0", "net7.0", true, "7.1.2")] // MLL disabled for 7.0+ - setting it doesn't change anything
[InlineData("7.0.0", "net7.0", null, "7.1.2")]
[InlineData("7.0.0", "net7.0", false, "7.1.2")]
[InlineData("7.0.0", "net8.0", true, "7.0.1")]
[InlineData("7.0.0", "net8.0", null, "7.0.1")]
[InlineData("7.0.0", "net8.0", true, "7.1.2")] // MLL disabled for 7.0+ - setting it doesn't change anything
[InlineData("7.0.0", "net8.0", null, "7.1.2")]
[InlineData("7.0.0", "net8.0", false, "7.1.2")]
// MLL where main hive has a better match
[InlineData("6.0.0", "net6.0", true, "6.1.4")] // Global hive with better version (higher patch)
Expand Down Expand Up @@ -82,7 +83,7 @@ public void FrameworkHiveSelection_CurrentDirectoryIsIgnored()
[InlineData("6.1.4", "net6.0", true, "6.1.4", true)]
[InlineData("6.1.4", "net6.0", null, "6.1.4", true)]
[InlineData("6.1.4", "net6.0", false, ResolvedFramework.NotFound, false)]
[InlineData("6.1.4", "net7.0", true, "6.1.4", true)]
[InlineData("6.1.4", "net7.0", true, ResolvedFramework.NotFound, false)] // MLL disabled for 7.0+
[InlineData("7.1.2", "net6.0", true, "7.1.2", false)] // 7.1.2 is in both main and global hives - the main should always win with exact match
[InlineData("7.1.2", "net6.0", null, "7.1.2", false)]
[InlineData("7.1.2", "net6.0", false, "7.1.2", false)]
Expand Down Expand Up @@ -154,7 +155,7 @@ public void ListRuntimes(bool? multiLevelLookup)

string expectedOutput = string.Join(
string.Empty,
GetExpectedFrameworks(multiLevelLookup)
GetExpectedFrameworks(false) // MLL Is always disabled for dotnet --list-runtimes
.Select(t => $"{MicrosoftNETCoreApp} {t.Version} [{Path.Combine(t.Path, "shared", MicrosoftNETCoreApp)}]{Environment.NewLine}"));

// !!IMPORTANT!!: This test verifies the exact match of the entire output of the command (not a substring!)
Expand All @@ -179,7 +180,7 @@ public void DotnetInfo(bool? multiLevelLookup)
string expectedOutput =
$".NET runtimes installed:{Environment.NewLine}" +
string.Join(string.Empty,
GetExpectedFrameworks(multiLevelLookup)
GetExpectedFrameworks(false) // MLL is always disabled for dotnet --info
.Select(t => $" {MicrosoftNETCoreApp} {t.Version} [{Path.Combine(t.Path, "shared", MicrosoftNETCoreApp)}]{Environment.NewLine}"));

RunTest(
Expand All @@ -190,13 +191,14 @@ public void DotnetInfo(bool? multiLevelLookup)
}

[Theory]
[InlineData("net5.0", true)]
[InlineData("net5.0", null)]
[InlineData("net5.0", false)]
[InlineData("net7.0", true)]
[InlineData("net7.0", null)]
[InlineData("net7.0", false)]
public void FrameworkResolutionError(string tfm, bool? multiLevelLookup)
[InlineData("net5.0", true, true)]
[InlineData("net5.0", null, true)]
[InlineData("net5.0", false, false)]
// MLL is disabled for 7.0+
[InlineData("net7.0", true, false)]
[InlineData("net7.0", null, false)]
[InlineData("net7.0", false, false)]
public void FrameworkResolutionError(string tfm, bool? multiLevelLookup, bool effectiveMultiLevelLookup)
{
// Multi-level lookup is only supported on Windows.
if (!OperatingSystem.IsWindows() && multiLevelLookup != false)
Expand All @@ -205,16 +207,17 @@ public void FrameworkResolutionError(string tfm, bool? multiLevelLookup)
string expectedOutput =
$"The following frameworks were found:{Environment.NewLine}" +
string.Join(string.Empty,
GetExpectedFrameworks(multiLevelLookup)
.Select(t => $" {t.Version} at [{Path.Combine(t.Path, "shared", MicrosoftNETCoreApp)}]{Environment.NewLine}"));
GetExpectedFrameworks(effectiveMultiLevelLookup)
.Select(t => $" {t.Version} at [{Path.Combine(t.Path, "shared", MicrosoftNETCoreApp)}]{Environment.NewLine}"));

RunTest(
runtimeConfig => runtimeConfig
.WithTfm(tfm)
.WithFramework(MicrosoftNETCoreApp, "9999.9.9"),
multiLevelLookup)
.Should().Fail()
.And.HaveStdErrContaining(expectedOutput);
.And.HaveStdErrContaining(expectedOutput)
.And.HaveStdErrContaining("https://aka.ms/dotnet/app-launch-failed");
}

private CommandResult RunTest(Func<RuntimeConfig, RuntimeConfig> runtimeConfig, bool? multiLevelLookup = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,11 @@ public void Precedence(
SettingLocation rollForwardOnNoCandidateFxLocation,
bool passes)
{
string requestedVersion = "5.0.0";
CommandResult result = RunTest(
new TestSettings()
.WithRuntimeConfigCustomizer(runtimeConfig => runtimeConfig
.WithFramework(MicrosoftNETCoreApp, "5.0.0"))
.WithFramework(MicrosoftNETCoreApp, requestedVersion))
.With(RollForwardSetting(rollForwardLocation, Constants.RollForwardSetting.Major))
.With(RollForwardOnNoCandidateFxSetting(rollForwardOnNoCandidateFxLocation, 0)));

Expand All @@ -114,7 +115,7 @@ public void Precedence(
}
else
{
result.Should().Fail().And.DidNotFindCompatibleFrameworkVersion();
result.ShouldFailToFindCompatibleFrameworkVersion(MicrosoftNETCoreApp, requestedVersion);
}
}

Expand Down
Loading

0 comments on commit 5098d45

Please sign in to comment.