Add global registered install location to Linux and macOS (dotnet/core-setup#6124)
authorVitek Karas <vitek.karas@microsoft.com>
Fri, 3 May 2019 20:52:39 +0000 (13:52 -0700)
committerGitHub <noreply@github.com>
Fri, 3 May 2019 20:52:39 +0000 (13:52 -0700)
The product now reads the /etc/dotnet/install_location file and use its first line as the path to the install location.

Adds test-only env. variables to be able to test the behavior around picking global install location.

Changes windows-only tests to run on Linux/macOS as well now.

Added some tests.
Improved the test infra around global registered location.

Commit migrated from https://github.com/dotnet/core-setup/commit/40aeca5ec6b0344367ad887e94cd62fefed17dbd

src/installer/corehost/cli/test/nativehost/nativehost.cpp
src/installer/corehost/common/pal.h
src/installer/corehost/common/pal.unix.cpp
src/installer/corehost/common/pal.windows.cpp
src/installer/test/HostActivationTests/Constants.cs
src/installer/test/HostActivationTests/MultilevelSDKLookup.cs
src/installer/test/HostActivationTests/NativeHosting/Nethost.cs
src/installer/test/HostActivationTests/PortableAppActivation.cs
src/installer/test/HostActivationTests/RegisteredInstallKeyOverride.cs [deleted file]
src/installer/test/HostActivationTests/RegisteredInstallLocationOverride.cs [new file with mode: 0644]
src/installer/test/TestUtils/Command.cs

index bb21fcb..0e95f4d 100644 (file)
@@ -40,16 +40,6 @@ int main(const int argc, const pal::char_t *argv[])
         if (argc >= 3)
             assembly_path = argv[2];
 
-#if defined(_WIN32)
-        pal::string_t testOverride;
-        if (pal::getenv(_X("TEST_OVERRIDE_PROGRAMFILES"), &testOverride))
-        {
-            std::cout << tostr(testOverride).data() << std::endl;
-            ::SetEnvironmentVariableW(_X("ProgramFiles"), testOverride.c_str());
-            ::SetEnvironmentVariableW(_X("ProgramFiles(x86)"), testOverride.c_str());
-        }
-#endif
-
         if (argc >= 4)
         {
             pal::string_t to_load = argv[3];
index 344fd2a..890844c 100644 (file)
@@ -246,11 +246,15 @@ namespace pal
     bool getenv(const char_t* name, string_t* recv);
     bool get_default_servicing_directory(string_t* recv);
 
-    //On Linux, there are no global locations
-    //On Windows there will be up to 2 global locations
-    bool get_global_dotnet_dirs(std::vector<pal::string_t>* recv);
+    // Returns the globally registered install location (if any)
     bool get_dotnet_self_registered_dir(pal::string_t* recv);
+
+    // Returns the default install location for a given platform
     bool get_default_installation_dir(pal::string_t* recv);
+
+    // Returns the global locations to search for SDK/Frameworks - used when multi-level lookup is enabled
+    bool get_global_dotnet_dirs(std::vector<pal::string_t>* recv);
+
     bool get_default_breadcrumb_store(string_t* recv);
     bool is_path_rooted(const string_t& path);
 
index 2ea3fbc..9a2a4b6 100644 (file)
@@ -301,12 +301,73 @@ bool pal::get_global_dotnet_dirs(std::vector<pal::string_t>* recv)
 
 bool pal::get_dotnet_self_registered_dir(pal::string_t* recv)
 {
-    // No support for global directories in Unix.
-    return false;
+    recv->clear();
+
+    //  ***Used only for testing***
+    pal::string_t environment_override;
+    if (pal::getenv(_X("_DOTNET_TEST_GLOBALLY_REGISTERED_PATH"), &environment_override))
+    {
+        recv->assign(environment_override);
+        return true;
+    }
+    //  ***************************
+
+    pal::string_t install_location_file_path = _X("/etc/dotnet/install_location");
+
+    //  ***Used only for testing***
+    pal::string_t environment_install_location_override;
+    if (pal::getenv(_X("_DOTNET_TEST_INSTALL_LOCATION_FILE_PATH"), &environment_install_location_override))
+    {
+        install_location_file_path = environment_install_location_override;
+    }
+    //  ***************************
+
+    trace::verbose(_X("Looking for install_location file in '%s'."), install_location_file_path.c_str());
+    FILE* install_location_file = pal::file_open(install_location_file_path, "r");
+    if (install_location_file == nullptr)
+    {
+        trace::verbose(_X("The install_location file failed to open."));
+        return false;
+    }
+
+    bool result = false;
+
+    char buf[PATH_MAX];
+    char* install_location = fgets(buf, sizeof(buf), install_location_file);
+    if (install_location != nullptr)
+    {
+        size_t len = pal::strlen(install_location);
+
+        // fgets includes the newline character in the string - so remove it.
+        if (len > 0 && len < PATH_MAX && install_location[len - 1] == '\n')
+        {
+            install_location[len - 1] = '\0';
+        }
+
+        trace::verbose(_X("Using install location '%s'."), install_location);
+        *recv = install_location;
+        result = true;
+    }
+    else
+    {
+        trace::verbose(_X("The install_location file first line could not be read."));
+    }
+
+    fclose(install_location_file);
+    return result;
 }
 
 bool pal::get_default_installation_dir(pal::string_t* recv)
 {
+    //  ***Used only for testing***
+    pal::string_t environmentOverride;
+    if (pal::getenv(_X("_DOTNET_TEST_DEFAULT_INSTALL_PATH"), &environmentOverride))
+    {
+        recv->assign(environmentOverride);
+        return true;
+    }
+    //  ***************************
+
 #if defined(__APPLE__)
      recv->assign(_X("/usr/local/share/dotnet"));
 #else
index 933f8d6..37d815c 100644 (file)
@@ -221,6 +221,15 @@ bool pal::get_default_servicing_directory(string_t* recv)
 
 bool pal::get_default_installation_dir(pal::string_t* recv)
 {
+    //  ***Used only for testing***
+    pal::string_t environmentOverride;
+    if (pal::getenv(_X("_DOTNET_TEST_DEFAULT_INSTALL_PATH"), &environmentOverride))
+    {
+        recv->assign(environmentOverride);
+        return true;
+    }
+    //  ***************************
+
     pal::char_t* program_files_dir;
     if (pal::is_running_in_wow64())
     {
index 5fe7b9a..d8a43d2 100644 (file)
@@ -44,8 +44,10 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
 
         public static class TestOnlyEnvironmentVariables
         {
+            public const string DefaultInstallPath = "_DOTNET_TEST_DEFAULT_INSTALL_PATH";
             public const string RegistryPath = "_DOTNET_TEST_REGISTRY_PATH";
             public const string GloballyRegisteredPath = "_DOTNET_TEST_GLOBALLY_REGISTERED_PATH";
+            public const string InstallLocationFilePath = "_DOTNET_TEST_INSTALL_LOCATION_FILE_PATH";
         }
     }
 }
index 1dbf21e..eacd611 100644 (file)
@@ -510,9 +510,9 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
                 return;
             }
 
-            using (var regKeyOverride = new RegisteredInstallKeyOverride())
+            using (var registeredInstallLocationOverride = new RegisteredInstallLocationOverride())
             {
-                regKeyOverride.SetInstallLocation(_regDir, RepoDirectories.BuildArchitecture);
+                registeredInstallLocationOverride.SetInstallLocation(_regDir, RepoDirectories.BuildArchitecture);
 
                 // Add SDK versions
                 AddAvailableSdkVersions(_regSdkBaseDir, "9999.0.4");
@@ -528,7 +528,7 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
                     .WithUserProfile(_userDir)
                     .Environment(s_DefaultEnvironment)
                     .EnvironmentVariable("DOTNET_MULTILEVEL_LOOKUP", "1")
-                    .EnvironmentVariable(Constants.TestOnlyEnvironmentVariables.RegistryPath, regKeyOverride.KeyPath)
+                    .ApplyRegisteredInstallLocationOverride(registeredInstallLocationOverride)
                     .CaptureStdOut()
                     .CaptureStdErr()
                     .Execute()
index dfc8611..ca82d6c 100644 (file)
@@ -65,32 +65,27 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.NativeHosting
         [InlineData(true, false, true)]
         public void GetHostFxrPath_GlobalInstallation(bool useAssemblyPath, bool useRegisteredLocation, bool isValid)
         {
-            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
-            {
-                // We don't have a good way of hooking into how the product looks for global installations yet.
-                return;
-            }
-
             // Overide the registry key for self-registered global installs.
             // If using the registered location, set the install location value to the valid/invalid root.
             // If not using the registered location, do not set the value. When the value does not exist,
             // the product falls back to the default install location.
             CommandResult result;
-            string installRoot = Path.Combine(isValid ? sharedState.ValidInstallRoot : sharedState.InvalidInstallRoot);
-            using (var regKeyOverride = new RegisteredInstallKeyOverride())
+            string installLocation = Path.Combine(isValid ? sharedState.ValidInstallRoot : sharedState.InvalidInstallRoot, "dotnet");
+            using (var registeredInstallLocationOverride = new RegisteredInstallLocationOverride())
             {
                 if (useRegisteredLocation)
                 {
-                    regKeyOverride.SetInstallLocation(Path.Combine(installRoot, "dotnet"), sharedState.RepoDirectories.BuildArchitecture);
+                    registeredInstallLocationOverride.SetInstallLocation(installLocation, sharedState.RepoDirectories.BuildArchitecture);
                 }
 
-                string programFilesOverride = useRegisteredLocation ? sharedState.InvalidInstallRoot : installRoot;
                 result = Command.Create(sharedState.NativeHostPath, $"{GetHostFxrPath} {(useAssemblyPath ? sharedState.TestAssemblyPath : string.Empty)}")
                     .CaptureStdErr()
                     .CaptureStdOut()
                     .EnvironmentVariable("COREHOST_TRACE", "1")
-                    .EnvironmentVariable(Constants.TestOnlyEnvironmentVariables.RegistryPath, regKeyOverride.KeyPath)
-                    .EnvironmentVariable("TEST_OVERRIDE_PROGRAMFILES", programFilesOverride)
+                    .ApplyRegisteredInstallLocationOverride(registeredInstallLocationOverride)
+                    .EnvironmentVariable( // Redirect the default install location to a test directory
+                        Constants.TestOnlyEnvironmentVariables.DefaultInstallPath,
+                        useRegisteredLocation ? sharedState.InvalidInstallRoot : installLocation)
                     .Execute();
             }
 
@@ -142,6 +137,57 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.NativeHosting
                 .And.HaveStdErrContaining($"Found previously loaded library {HostFxrName}");
         }
 
+        [Theory]
+        [InlineData("{0}", true)]
+        [InlineData("{0}\n", true)]
+        [InlineData("{0}\nSome other text", true)]
+        [InlineData("", false)]
+        [InlineData("\n{0}", false)]
+        [InlineData(" {0}", false)]
+        [InlineData("{0} \n", false)]
+        [InlineData("{0} ", false)]
+        public void GetHostFxrPath_InstallLocationFile(string value, bool shouldPass)
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                // This test targets the install_location config file which is only used on Linux and macOS.
+                return;
+            }
+
+            string installLocation = Path.Combine(sharedState.ValidInstallRoot, "dotnet");
+
+            using (RegisteredInstallLocationOverride registeredInstallLocationOverride = new RegisteredInstallLocationOverride())
+            {
+                File.WriteAllText(registeredInstallLocationOverride.PathValueOverride, string.Format(value, installLocation));
+
+                CommandResult result = Command.Create(sharedState.NativeHostPath, GetHostFxrPath)
+                    .CaptureStdErr()
+                    .CaptureStdOut()
+                    .EnvironmentVariable("COREHOST_TRACE", "1")
+                    .ApplyRegisteredInstallLocationOverride(registeredInstallLocationOverride)
+                    .EnvironmentVariable( // Redirect the default install location to an invalid location so that it doesn't cause the test to pass 
+                        Constants.TestOnlyEnvironmentVariables.DefaultInstallPath,
+                        sharedState.InvalidInstallRoot)
+                    .Execute();
+
+                result.Should().HaveStdErrContaining($"Looking for install_location file in '{registeredInstallLocationOverride.PathValueOverride}'.");
+
+                if (shouldPass)
+                {
+                    result.Should().Pass()
+                        .And.HaveStdErrContaining($"Using install location '{installLocation}'.")
+                        .And.HaveStdOutContaining($"hostfxr_path: {sharedState.HostFxrPath}".ToLower());
+                }
+                else
+                {
+                    result.Should().Fail()
+                        .And.ExitWith(1)
+                        .And.HaveStdOutContaining($"{GetHostFxrPath} failed: 0x{CoreHostLibMissingFailure.ToString("x")}")
+                        .And.HaveStdErrContaining($"The required library {HostFxrName} could not be found");
+                }
+            }
+        }
+
         public class SharedTestState : SharedTestStateBase
         {
             public string HostFxrPath { get; }
index 0f2b993..eaab9d8 100644 (file)
@@ -7,17 +7,15 @@ using System;
 using System.IO;
 using System.Security.Cryptography;
 using System.Text;
-using System.Runtime.InteropServices;
-using Microsoft.Win32;
 using Xunit;
 
 namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
 {
     public class PortableAppActivation : IClassFixture<PortableAppActivation.SharedTestState>
     {
-        private SharedTestState sharedTestState;
+        private readonly SharedTestState sharedTestState;
 
-        public PortableAppActivation(PortableAppActivation.SharedTestState fixture)
+        public PortableAppActivation(SharedTestState fixture)
         {
             sharedTestState = fixture;
         }
@@ -74,7 +72,7 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
                 .Copy();
 
             var dotnet = fixture.BuiltDotnet;
-            var appDll = fixture.TestProject.AppDll.Replace(Path.DirectorySeparatorChar,Path.AltDirectorySeparatorChar);
+            var appDll = fixture.TestProject.AppDll.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
 
             dotnet.Exec(appDll)
                 .CaptureStdErr()
@@ -93,11 +91,11 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
 
             var dotnet = fixture.BuiltDotnet;
             var appDll = fixture.TestProject.AppDll;
-            
+
             dotnet.Exec("exec", "--runtimeconfig", runtimeConfig, appDll)
                 .CaptureStdErr()
                 .CaptureStdOut()
-                .Execute(fExpectedToFail:true)
+                .Execute(fExpectedToFail: true)
                 .Should().Fail();
         }
 
@@ -174,7 +172,7 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
                 .Execute()
                 .Should().Pass()
                 .And.HaveStdOutContaining("Hello World");
-            
+
         }
 
         [Fact]
@@ -235,7 +233,7 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
             dotnet.Exec("exec", "--depsfile", depsJson, appDll)
                 .CaptureStdErr()
                 .CaptureStdOut()
-                .Execute(fExpectedToFail:true)
+                .Execute(fExpectedToFail: true)
                 .Should().Fail();
         }
 
@@ -297,14 +295,11 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
                 .And.HaveStdOutContaining($"Framework Version:{sharedTestState.RepoDirectories.MicrosoftNETCoreAppVersion}");
         }
 
-        [Fact]
-        public void Framework_Dependent_AppHost_From_Global_Registry_Location_Succeeds()
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public void Framework_Dependent_AppHost_From_Global_Location_Succeeds(bool useRegisteredLocation)
         {
-            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
-            {
-                return;
-            }
-
             var fixture = sharedTestState.PortableAppFixture_Published
                 .Copy();
 
@@ -335,16 +330,20 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
             // Get the framework location that was built
             string builtDotnet = fixture.BuiltDotnet.BinPath;
 
-            using (var regKeyOverride = new RegisteredInstallKeyOverride())
+            using (var registeredInstallLocationOverride = new RegisteredInstallLocationOverride())
             {
                 string architecture = fixture.CurrentRid.Split('-')[1];
-                regKeyOverride.SetInstallLocation(builtDotnet, architecture);
+                if (useRegisteredLocation)
+                {
+                    registeredInstallLocationOverride.SetInstallLocation(builtDotnet, architecture);
+                }
 
                 // Verify running with the default working directory
                 Command.Create(appExe)
                     .CaptureStdErr()
                     .CaptureStdOut()
-                    .EnvironmentVariable(Constants.TestOnlyEnvironmentVariables.RegistryPath, regKeyOverride.KeyPath)
+                    .ApplyRegisteredInstallLocationOverride(registeredInstallLocationOverride)
+                    .EnvironmentVariable(Constants.TestOnlyEnvironmentVariables.DefaultInstallPath, useRegisteredLocation ? null : builtDotnet)
                     .Execute()
                     .Should().Pass()
                     .And.HaveStdOutContaining("Hello World")
@@ -352,10 +351,11 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
 
                 // Verify running from within the working directory
                 Command.Create(appExe)
-                    .WorkingDirectory(fixture.TestProject.OutputDirectory)
-                    .EnvironmentVariable(Constants.TestOnlyEnvironmentVariables.RegistryPath, regKeyOverride.KeyPath)
                     .CaptureStdErr()
                     .CaptureStdOut()
+                    .WorkingDirectory(fixture.TestProject.OutputDirectory)
+                    .ApplyRegisteredInstallLocationOverride(registeredInstallLocationOverride)
+                    .EnvironmentVariable(Constants.TestOnlyEnvironmentVariables.DefaultInstallPath, useRegisteredLocation ? null : builtDotnet)
                     .Execute()
                     .Should().Pass()
                     .And.HaveStdOutContaining("Hello World")
@@ -428,8 +428,8 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
                 Directory.CreateDirectory(storeoutputDirectory);
             }
 
-            testProjectFixture.StoreProject(outputDirectory :storeoutputDirectory);
-            
+            testProjectFixture.StoreProject(outputDirectorystoreoutputDirectory);
+
             return storeoutputDirectory;
         }
 
diff --git a/src/installer/test/HostActivationTests/RegisteredInstallKeyOverride.cs b/src/installer/test/HostActivationTests/RegisteredInstallKeyOverride.cs
deleted file mode 100644 (file)
index 6226ac4..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using Microsoft.Win32;
-using System;
-
-namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
-{
-    public class RegisteredInstallKeyOverride : IDisposable
-    {
-        public string KeyPath { get; }
-
-        private readonly RegistryKey parentKey;
-        private readonly RegistryKey key;
-        private readonly string keyName;
-
-        public RegisteredInstallKeyOverride()
-        {
-            // To test registered installs, we need a registry key which is:
-            // - writable without admin access - so that the tests don't require admin to run
-            // - redirected in WOW64 - so that there are both 32-bit and 64-bit versions of the key
-            //   This is because the product stores the info in the 32-bit hive only and even 64-bit
-            //   product must look into the 32-bit hive.
-            //   Without the redirection we would not be able to test that the product always looks
-            //   into 32-bit only.
-            // Per this page https://docs.microsoft.com/en-us/windows/desktop/WinProg64/shared-registry-keys
-            // a user writable redirected key is for example HKCU\Software\Classes\Interface
-            // so we're going to use that one - it's not super clean as the key stores COM interfaces,
-            // but we should not corrupt anything by adding a special subkey even if it's left behind.
-            //
-            // Note: If you want to inspect the values written by the test and/or modify them manually
-            //   you have to navigate to HKCU\Software\Classes\Wow6432Node\Interface on a 64-bit OS.
-            using (RegistryKey hkcu = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry32))
-            {
-                parentKey = hkcu.CreateSubKey(@"Software\Classes\Interface");
-                keyName = "_DOTNET_Test" + System.Diagnostics.Process.GetCurrentProcess().Id.ToString();
-                key = parentKey.CreateSubKey(keyName);
-                KeyPath = key.Name;
-            }
-        }
-
-        public void SetInstallLocation(string installLocation, string architecture)
-        {
-            using (RegistryKey dotnetLocationKey = key.CreateSubKey($@"Setup\InstalledVersions\{architecture}"))
-            {
-                dotnetLocationKey.SetValue("InstallLocation", installLocation);
-            }
-        }
-
-        public void Dispose()
-        {
-            parentKey.DeleteSubKeyTree(keyName, throwOnMissingSubKey: false);
-            key.Dispose();
-            parentKey.Dispose();
-        }
-    }
-}
diff --git a/src/installer/test/HostActivationTests/RegisteredInstallLocationOverride.cs b/src/installer/test/HostActivationTests/RegisteredInstallLocationOverride.cs
new file mode 100644 (file)
index 0000000..c244169
--- /dev/null
@@ -0,0 +1,121 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.DotNet.Cli.Build.Framework;
+using Microsoft.Win32;
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
+{
+    public class RegisteredInstallLocationOverride : IDisposable
+    {
+        public string PathValueOverride { get; }
+
+        // Windows only
+        private readonly RegistryKey parentKey;
+        private readonly RegistryKey key;
+        private readonly string keyName;
+
+        // Linux/macOS only
+
+        public RegisteredInstallLocationOverride()
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                // To test registered installs, we need a registry key which is:
+                // - writable without admin access - so that the tests don't require admin to run
+                // - redirected in WOW64 - so that there are both 32-bit and 64-bit versions of the key
+                //   This is because the product stores the info in the 32-bit hive only and even 64-bit
+                //   product must look into the 32-bit hive.
+                //   Without the redirection we would not be able to test that the product always looks
+                //   into 32-bit only.
+                // Per this page https://docs.microsoft.com/en-us/windows/desktop/WinProg64/shared-registry-keys
+                // a user writable redirected key is for example HKCU\Software\Classes\Interface
+                // so we're going to use that one - it's not super clean as the key stores COM interfaces,
+                // but we should not corrupt anything by adding a special subkey even if it's left behind.
+                //
+                // Note: If you want to inspect the values written by the test and/or modify them manually
+                //   you have to navigate to HKCU\Software\Classes\Wow6432Node\Interface on a 64-bit OS.
+                using (RegistryKey hkcu = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry32))
+                {
+                    parentKey = hkcu.CreateSubKey(@"Software\Classes\Interface");
+                    keyName = "_DOTNET_Test" + Process.GetCurrentProcess().Id.ToString();
+                    key = parentKey.CreateSubKey(keyName);
+                    PathValueOverride = key.Name;
+                }
+            }
+            else
+            {
+                // On Linux/macOS the install location is registered in a file which is normally
+                // located in /etc/dotnet/install_location
+                // So we need to redirect it to a different place here.
+                string directory = Path.Combine(TestArtifact.TestArtifactsPath, "installLocationOverride");
+                Directory.CreateDirectory(directory);
+                PathValueOverride = Path.Combine(directory, "install_location." + Process.GetCurrentProcess().Id.ToString());
+                File.WriteAllText(PathValueOverride, "");
+            }
+        }
+
+        public void SetInstallLocation(string installLocation, string architecture)
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                using (RegistryKey dotnetLocationKey = key.CreateSubKey($@"Setup\InstalledVersions\{architecture}"))
+                {
+                    dotnetLocationKey.SetValue("InstallLocation", installLocation);
+                }
+            }
+            else
+            {
+                File.WriteAllText(PathValueOverride, installLocation);
+            }
+        }
+
+        public void Dispose()
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                parentKey.DeleteSubKeyTree(keyName, throwOnMissingSubKey: false);
+                key.Dispose();
+                parentKey.Dispose();
+            }
+            else
+            {
+                if (File.Exists(PathValueOverride))
+                {
+                    File.Delete(PathValueOverride);
+                }
+            }
+        }
+    }
+
+    public static class RegisteredInstallLocationExtensions
+    {
+        public static Command ApplyRegisteredInstallLocationOverride(
+            this Command command,
+            RegisteredInstallLocationOverride registeredInstallLocationOverride)
+        {
+            if (registeredInstallLocationOverride == null)
+            {
+                return command;
+            }
+
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                return command.EnvironmentVariable(
+                    Constants.TestOnlyEnvironmentVariables.RegistryPath,
+                    registeredInstallLocationOverride.PathValueOverride);
+            }
+            else
+            {
+                return command.EnvironmentVariable(
+                    Constants.TestOnlyEnvironmentVariables.InstallLocationFilePath,
+                    registeredInstallLocationOverride.PathValueOverride);
+            }
+        }
+    }
+}
index 3c4ce3e..0e25f81 100644 (file)
@@ -271,7 +271,11 @@ namespace Microsoft.DotNet.Cli.Build.Framework
 
         public Command EnvironmentVariable(string name, string value)
         {
-            Process.StartInfo.Environment[name] = value;
+            if (value != null)
+            {
+                Process.StartInfo.Environment[name] = value;
+            }
+
             return this;
         }