diff --git a/TestAssets/TestUtils/SDKLookup/dotnet.runtimeconfig.json b/TestAssets/TestUtils/SDKLookup/dotnet.runtimeconfig.json new file mode 100644 index 0000000000..12aa5319e8 --- /dev/null +++ b/TestAssets/TestUtils/SDKLookup/dotnet.runtimeconfig.json @@ -0,0 +1,8 @@ +{ + "runtimeOptions": { + "framework": { + "name": "Microsoft.NETCore.App", + "version": "9999.0.0" + } + } +} \ No newline at end of file diff --git a/TestAssets/TestUtils/SDKLookup/global.json b/TestAssets/TestUtils/SDKLookup/global.json new file mode 100644 index 0000000000..773bb6584a --- /dev/null +++ b/TestAssets/TestUtils/SDKLookup/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "9999.0.0-global-dummy" + } +} diff --git a/src/corehost/cli/fxr/fx_muxer.cpp b/src/corehost/cli/fxr/fx_muxer.cpp index 84a01ad036..6831d73c18 100644 --- a/src/corehost/cli/fxr/fx_muxer.cpp +++ b/src/corehost/cli/fxr/fx_muxer.cpp @@ -232,6 +232,23 @@ pal::string_t get_deps_file( } } +/** +* Multilevel Lookup is enabled by default +* It can be disabled by setting DOTNET_MULTILEVEL_LOOKUP env var to a value that is not 1 +*/ +bool multilevel_lookup_enabled() +{ + pal::string_t env_lookup; + bool multilevel_lookup = true; + + if (pal::getenv(_X("DOTNET_MULTILEVEL_LOOKUP"), &env_lookup)) + { + auto env_val = pal::xtoi(env_lookup.c_str()); + multilevel_lookup = (env_val == 1); + } + return multilevel_lookup; +} + /** * Given own location, FX location, app binary and specified --depsfile and probe paths * return location that is expected to contain hostpolicy @@ -375,19 +392,10 @@ pal::string_t fx_muxer_t::resolve_fx_dir(host_mode_t mode, const pal::string_t& // If it is not activated, then only .exe directory will be considered std::vector hive_dir; - pal::string_t env_lookup; - - // Multi-level SharedFX lookup can be disabled by setting DOTNET_MULTILEVEL_LOOKUP env var to zero - bool multilevel_lookup = true; - if (pal::getenv(_X("DOTNET_MULTILEVEL_LOOKUP"), &env_lookup)) - { - auto env_val = pal::xtoi(env_lookup.c_str()); - multilevel_lookup = (env_val != 0); - } - pal::string_t cwd; pal::string_t local_dir; pal::string_t global_dir; + bool multilevel_lookup = multilevel_lookup_enabled(); if (multilevel_lookup) { @@ -582,36 +590,68 @@ bool fx_muxer_t::resolve_sdk_dotnet_path(const pal::string_t& own_dir, pal::stri trace::verbose(_X("Failed to obtain current working dir")); } - pal::string_t retval; - if (!global.empty()) + std::vector hive_dir; + pal::string_t local_dir; + pal::string_t global_dir; + bool multilevel_lookup = multilevel_lookup_enabled(); + + if (multilevel_lookup) { - pal::string_t cli_version = resolve_cli_version(global); - if (!cli_version.empty()) + if (pal::getcwd(&cwd)) { - pal::string_t sdk_path = own_dir; - append_path(&sdk_path, _X("sdk")); - append_path(&sdk_path, cli_version.c_str()); + hive_dir.push_back(cwd); + } + if (pal::get_local_dotnet_dir(&local_dir)) + { + hive_dir.push_back(local_dir); + } + } + hive_dir.push_back(own_dir); + if (multilevel_lookup && pal::get_global_dotnet_dir(&global_dir)) + { + hive_dir.push_back(global_dir); + } - if (pal::directory_exists(sdk_path)) - { - trace::verbose(_X("CLI directory [%s] from global.json exists"), sdk_path.c_str()); - retval = sdk_path; - } - else + pal::string_t retval; + for (pal::string_t dir : hive_dir) + { + trace::verbose(_X("Searching SDK directory in [%s]"), dir.c_str()); + if (!global.empty()) + { + pal::string_t cli_version = resolve_cli_version(global); + if (!cli_version.empty()) { - trace::verbose(_X("CLI directory [%s] from global.json doesn't exist"), sdk_path.c_str()); + pal::string_t sdk_path = dir; + append_path(&sdk_path, _X("sdk")); + append_path(&sdk_path, cli_version.c_str()); + + if (pal::directory_exists(sdk_path)) + { + trace::verbose(_X("CLI directory [%s] from global.json exists"), sdk_path.c_str()); + retval = sdk_path; + } + else + { + trace::verbose(_X("CLI directory [%s] from global.json doesn't exist"), sdk_path.c_str()); + } } } + if (retval.empty()) + { + pal::string_t sdk_path = dir; + append_path(&sdk_path, _X("sdk")); + retval = resolve_sdk_version(sdk_path); + } + if (!retval.empty()) + { + cli_sdk->assign(retval); + trace::verbose(_X("Found CLI SDK in: %s"), cli_sdk->c_str()); + return true; + } } - if (retval.empty()) - { - pal::string_t sdk_path = own_dir; - append_path(&sdk_path, _X("sdk")); - retval = resolve_sdk_version(sdk_path); - } - cli_sdk->assign(retval); - trace::verbose(_X("Found CLI SDK in: %s"), cli_sdk->c_str()); - return !retval.empty(); + + trace::verbose(_X("It was not possible to find any SDK version")); + return false; } bool is_sdk_dir_present(const pal::string_t& own_dir) diff --git a/test/HostActivationTests/GivenThatICareAboutMultilevelSDKLookup.cs b/test/HostActivationTests/GivenThatICareAboutMultilevelSDKLookup.cs new file mode 100644 index 0000000000..993e081f08 --- /dev/null +++ b/test/HostActivationTests/GivenThatICareAboutMultilevelSDKLookup.cs @@ -0,0 +1,360 @@ +using System; +using System.IO; +using System.Linq; +using Microsoft.DotNet.InternalAbstractions; +using Xunit; + +namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSDKLookup +{ + public class GivenThatICareAboutMultilevelSDKLookup + { + private RepoDirectoriesProvider RepoDirectories; + private TestProjectFixture PreviouslyBuiltAndRestoredPortableTestProjectFixture; + + private string _currentWorkingDir; + private string _userDir; + private string _executableDir; + private string _cwdSdkBaseDir; + private string _userSdkBaseDir; + private string _exeSdkBaseDir; + private string _cwdSelectedMessage; + private string _userSelectedMessage; + private string _exeSelectedMessage; + private string _sdkDir; + + public GivenThatICareAboutMultilevelSDKLookup() + { + // From the artifacts dir, it's possible to find where the sharedFrameworkPublish folder is. We need + // to locate it because we'll copy its contents into other folders + string artifactsDir = Environment.GetEnvironmentVariable("TEST_ARTIFACTS"); + string builtDotnet = Path.Combine(artifactsDir, "..", "..", "intermediate", "sharedFrameworkPublish"); + + // The dotnetMultilevelSDKLookup dir will contain some folders and files that will be + // necessary to perform the tests + string baseMultilevelDir = Path.Combine(artifactsDir, "dotnetMultilevelSDKLookup"); + string multilevelDir = CalculateMultilevelDirectory(baseMultilevelDir); + + // The three tested locations will be the cwd, the user folder and the exe dir. Both cwd and exe dir + // are easily overwritten, so they will be placed inside the multilevel folder. The actual user location will + // be used during tests. + _currentWorkingDir = Path.Combine(multilevelDir, "cwd"); + if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows) + { + _userDir = Environment.GetEnvironmentVariable("USERPROFILE"); + } + else + { + _userDir = Environment.GetEnvironmentVariable("HOME"); + } + _executableDir = Path.Combine(multilevelDir, "exe"); + + // It's necessary to copy the entire publish folder to the exe dir because + // we'll need to build from it. The CopyDirectory method automatically creates the dest dir + CopyDirectory(builtDotnet, _executableDir); + + // SdkBaseDirs contain all available version folders + _cwdSdkBaseDir = Path.Combine(_currentWorkingDir, "sdk"); + _userSdkBaseDir = Path.Combine(_userDir, ".dotnet", RuntimeEnvironment.RuntimeArchitecture, "sdk"); + _exeSdkBaseDir = Path.Combine(_executableDir, "sdk"); + + // Create directories + Directory.CreateDirectory(_cwdSdkBaseDir); + Directory.CreateDirectory(_userSdkBaseDir); + Directory.CreateDirectory(_exeSdkBaseDir); + + // Restore and build PortableApp from exe dir + RepoDirectories = new RepoDirectoriesProvider(builtDotnet: _executableDir); + PreviouslyBuiltAndRestoredPortableTestProjectFixture = new TestProjectFixture("PortableApp", RepoDirectories) + .EnsureRestored(RepoDirectories.CorehostPackages, RepoDirectories.CorehostDummyPackages) + .BuildProject(); + var fixture = PreviouslyBuiltAndRestoredPortableTestProjectFixture; + + // Set a dummy framework version (9999.0.0) in the cwd sharedFx location. We will + // always pick the framework from cwd to avoid interference with the sharedFxLookup + // test folders in the user dir + string cwdDummyFxVersion = Path.Combine(_currentWorkingDir, "shared", "Microsoft.NETCore.App", "9999.0.0"); + string builtSharedFxDir = fixture.BuiltDotnet.GreatestVersionSharedFxPath; + CopyDirectory(builtSharedFxDir, cwdDummyFxVersion); + + // The actual SDK version can be obtained from the built fixture. We'll use it to + // locate the sdkDir from which we can get the files contained in the version folder + string sdkBaseDir = Path.Combine(fixture.SdkDotnet.BinPath, "sdk"); + + var sdkVersionDirs = Directory.EnumerateDirectories(sdkBaseDir); + string greatestVersionSdk = sdkVersionDirs + .OrderByDescending(p => p.ToLower()) + .First(); + + _sdkDir = Path.Combine(sdkBaseDir, greatestVersionSdk); + + // Trace messages used to identify from which folder the SDK was picked + _cwdSelectedMessage = $"Using dotnet SDK dll=[{_cwdSdkBaseDir}"; + _userSelectedMessage = $"Using dotnet SDK dll=[{_userSdkBaseDir}"; + _exeSelectedMessage = $"Using dotnet SDK dll=[{_exeSdkBaseDir}"; + } + + [Fact] + public void SdkLookup_Must_Verify_Folders_in_the_Correct_Order() + { + var fixture = PreviouslyBuiltAndRestoredPortableTestProjectFixture + .Copy(); + + var dotnet = fixture.BuiltDotnet; + + // Add a dummy version in the exe dir + AddAvailableSdkVersions(_exeSdkBaseDir, "9999.0.0-dummy"); + + // Specified CLI version: none + // CWD: empty + // User: empty + // Exe: 9999.0.0 + // Expected: 9999.0.0 from exe dir + dotnet.Exec("help") + .WorkingDirectory(_currentWorkingDir) + .EnvironmentVariable("COREHOST_TRACE", "1") + .CaptureStdOut() + .CaptureStdErr() + .Execute() + .Should() + .Pass() + .And + .HaveStdErrContaining(_exeSelectedMessage); + + // Add a dummy version in the user dir + AddAvailableSdkVersions(_userSdkBaseDir, "9999.0.0-dummy"); + + // Specified CLI version: none + // CWD: empty + // User: 9999.0.0 + // Exe: 9999.0.0 + // Expected: 9999.0.0 from user dir + dotnet.Exec("help") + .WorkingDirectory(_currentWorkingDir) + .EnvironmentVariable("COREHOST_TRACE", "1") + .CaptureStdOut() + .CaptureStdErr() + .Execute() + .Should() + .Pass() + .And + .HaveStdErrContaining(_userSelectedMessage); + + // Add a dummy version in the cwd + AddAvailableSdkVersions(_cwdSdkBaseDir, "9999.0.0-dummy"); + + // Specified CLI version: none + // CWD: 9999.0.0 + // User: 9999.0.0 + // Exe: 9999.0.0 + // Expected: 9999.0.0 from cwd + dotnet.Exec("help") + .WorkingDirectory(_currentWorkingDir) + .EnvironmentVariable("COREHOST_TRACE", "1") + .CaptureStdOut() + .CaptureStdErr() + .Execute() + .Should() + .Pass() + .And + .HaveStdErrContaining(_cwdSelectedMessage); + + // Remove dummy folders from user dir + DeleteAvailableSdkVersions(_userSdkBaseDir, "9999.0.0-dummy"); + } + + [Fact] + public void SdkLookup_Must_Look_For_Available_Versions_Before_Looking_Into_Another_Folder() + { + var fixture = PreviouslyBuiltAndRestoredPortableTestProjectFixture + .Copy(); + + var dotnet = fixture.BuiltDotnet; + + // Set specified CLI version = 9999.0.0-global-dummy + SetGlobalJsonVersion(); + + // Add some dummy versions + AddAvailableSdkVersions(_userSdkBaseDir, "9999.0.0", "9999.0.0-dummy"); + AddAvailableSdkVersions(_exeSdkBaseDir, "9999.0.0-dummy"); + + // Specified CLI version: 9999.0.0-global-dummy + // CWD: empty + // User: 9999.0.0, 9999.0.0-dummy + // Exe: 9999.0.0-dummy + // Expected: 9999.0.0 from user dir + dotnet.Exec("help") + .WorkingDirectory(_currentWorkingDir) + .EnvironmentVariable("COREHOST_TRACE", "1") + .CaptureStdOut() + .CaptureStdErr() + .Execute() + .Should() + .Pass() + .And + .HaveStdErrContaining(Path.Combine(_userSelectedMessage, "9999.0.0")); + + // Add some dummy versions + AddAvailableSdkVersions(_cwdSdkBaseDir, "9999.0.0"); + AddAvailableSdkVersions(_exeSdkBaseDir, "9999.0.0-global-dummy"); + + // Specified CLI version: 9999.0.0-global-dummy + // CWD: 9999.0.0 + // User: 9999.0.0, 9999.0.0-dummy + // Exe: 9999.0.0-dummy, 9999.0.0-global-dummy + // Expected: 9999.0.0 from cwd + dotnet.Exec("help") + .WorkingDirectory(_currentWorkingDir) + .EnvironmentVariable("COREHOST_TRACE", "1") + .CaptureStdOut() + .CaptureStdErr() + .Execute() + .Should() + .Pass() + .And + .HaveStdErrContaining(Path.Combine(_cwdSelectedMessage, "9999.0.0")); + + // Add a prerelease dummy version in the cwd + AddAvailableSdkVersions(_cwdSdkBaseDir, "9999.0.0-global-dummy"); + + // Specified CLI version: 9999.0.0-global-dummy + // CWD: 9999.0.0, 9999.0.0-global-dummy + // User: 9999.0.0, 9999.0.0-dummy + // Exe: 9999.0.0-dummy, 9999.0.0-global-dummy + // Expected: 9999.0.0-global-dummy from cwd + dotnet.Exec("help") + .WorkingDirectory(_currentWorkingDir) + .EnvironmentVariable("COREHOST_TRACE", "1") + .CaptureStdOut() + .CaptureStdErr() + .Execute() + .Should() + .Pass() + .And + .HaveStdErrContaining(Path.Combine(_cwdSelectedMessage, "9999.0.0-global-dummy")); + + // Remove dummy folders from user dir + DeleteAvailableSdkVersions(_userSdkBaseDir, "9999.0.0", "9999.0.0-dummy"); + } + + // This method adds a list of new sdk version folders in the specified + // sdkBaseDir. The files are copied from the _sdkDir. Also, the dotnet.runtimeconfig.json + // file is overwritten in order to use a dummy framework version (9999.0.0) + // Remarks: + // - If the sdkBaseDir does not exist, then a DirectoryNotFoundException + // is thrown. + // - If a specified version folder already exists, then it is deleted and replaced + // with the contents of the _builtSharedFxDir. + private void AddAvailableSdkVersions(string sdkBaseDir, params string[] availableVersions) + { + DirectoryInfo sdkBaseDirInfo = new DirectoryInfo(sdkBaseDir); + + if (!sdkBaseDirInfo.Exists) + { + throw new DirectoryNotFoundException(); + } + + string dummyRuntimeConfig = Path.Combine(RepoDirectories.RepoRoot, "TestAssets", "TestUtils", + "SDKLookup", "dotnet.runtimeconfig.json"); + + foreach (string version in availableVersions) + { + string newSdkDir = Path.Combine(sdkBaseDir, version); + CopyDirectory(_sdkDir, newSdkDir); + + string runtimeConfig = Path.Combine(newSdkDir, "dotnet.runtimeconfig.json"); + File.Copy(dummyRuntimeConfig, runtimeConfig, true); + } + } + + // This method removes a list of sdk version folders from the specified sdkBaseDir. + // Remarks: + // - If the sdkBaseDir does not exist, then a DirectoryNotFoundException + // is thrown. + // - If a specified version folder does not exist, then a DirectoryNotFoundException + // is thrown. + private void DeleteAvailableSdkVersions(string sdkBaseDir, params string[] availableVersions) + { + DirectoryInfo sdkBaseDirInfo = new DirectoryInfo(sdkBaseDir); + + if (!sdkBaseDirInfo.Exists) + { + throw new DirectoryNotFoundException(); + } + + foreach (string version in availableVersions) + { + string sdkDir = Path.Combine(sdkBaseDir, version); + if (!Directory.Exists(sdkDir)) + { + throw new DirectoryNotFoundException(); + } + Directory.Delete(sdkDir, true); + } + } + + // CopyDirectory recursively copies a directory. + // Remarks: + // - If the dest dir does not exist, then it is created. + // - If the dest dir exists, then it is substituted with the new one + // (original files and subfolders are deleted). + // - If the src dir does not exist, then a DirectoryNotFoundException + // is thrown. + private void CopyDirectory(string srcDir, string dstDir) + { + DirectoryInfo srcDirInfo = new DirectoryInfo(srcDir); + + if (!srcDirInfo.Exists) + { + throw new DirectoryNotFoundException(); + } + + DirectoryInfo dstDirInfo = new DirectoryInfo(dstDir); + + if (dstDirInfo.Exists) + { + dstDirInfo.Delete(true); + } + + dstDirInfo.Create(); + + foreach (FileInfo fileInfo in srcDirInfo.GetFiles()) + { + string newFile = Path.Combine(dstDir, fileInfo.Name); + fileInfo.CopyTo(newFile); + } + + foreach (DirectoryInfo subdirInfo in srcDirInfo.GetDirectories()) + { + string newDir = Path.Combine(dstDir, subdirInfo.Name); + CopyDirectory(subdirInfo.FullName, newDir); + } + } + + // Put a global.json file in the cwd in order to specify a CLI + // dummy version (9999.0.0-global-dummy) + public void SetGlobalJsonVersion() + { + string destFile = Path.Combine(_currentWorkingDir, "global.json"); + string srcFile = Path.Combine(RepoDirectories.RepoRoot, "TestAssets", "TestUtils", + "SDKLookup", "global.json"); + + File.Copy(srcFile, destFile, true); + } + + // MultilevelDirectory is %TEST_ARTIFACTS%\dotnetMultilevelSDKLookup\id. + // We must locate the first non existing id. + private string CalculateMultilevelDirectory(string baseMultilevelDir) + { + int count = 0; + string multilevelDir; + + do + { + multilevelDir = Path.Combine(baseMultilevelDir, count.ToString()); + count++; + } while (Directory.Exists(multilevelDir)); + + return multilevelDir; + } + } +}