Add hostfxr_get_dotnet_environment_info API (#48097)
authorMateo Torres-Ruiz <mateoatr@users.noreply.github.com>
Sat, 13 Feb 2021 00:48:48 +0000 (16:48 -0800)
committerGitHub <noreply@github.com>
Sat, 13 Feb 2021 00:48:48 +0000 (16:48 -0800)
* Add hostfxr_get_dotnet_environment_info

* Change access modifiers in structs

* Remove unnecessary context local

* Use global install location if no dotnet_root is specified

* Add framework tests

* Update framework_info comparison

* Remove commented code

* Remove unnecessary marshaling
Export fn

* Add hostfxr_get_dotnet_environment_info to hostfxr exports

* Fail if result is nullptr
Validate size of structs
Test that result_context isn't modified

* Remove version_as_str
Update tests

* Add tests for invalid args

src/installer/corehost/cli/fxr/framework_info.cpp
src/installer/corehost/cli/fxr/framework_info.h
src/installer/corehost/cli/fxr/hostfxr.cpp
src/installer/corehost/cli/fxr/standalone/hostfxr.def
src/installer/corehost/cli/fxr/standalone/hostfxr_unixexports.src
src/installer/corehost/cli/hostfxr.h
src/installer/tests/Assets/TestProjects/HostApiInvokerApp/HostFXR.cs
src/installer/tests/HostActivation.Tests/NativeHostApis.cs

index 838b2c1..0859f6e 100644 (file)
@@ -19,7 +19,17 @@ bool compare_by_name_and_version(const framework_info &a, const framework_info &
         return false;
     }
 
-    return a.version < b.version;
+    if (a.version < b.version)
+    {
+        return true;
+    }
+
+    if (a.version == b.version)
+    {
+        return a.hive_depth > b.hive_depth;
+    }
+
+    return false;
 }
 
 /*static*/ void framework_info::get_all_framework_infos(
@@ -30,6 +40,8 @@ bool compare_by_name_and_version(const framework_info &a, const framework_info &
     std::vector<pal::string_t> hive_dir;
     get_framework_and_sdk_locations(own_dir, &hive_dir);
 
+    int32_t hive_depth = 0;
+
     for (pal::string_t dir : hive_dir)
     {
         auto fx_shared_dir = dir;
@@ -68,13 +80,15 @@ bool compare_by_name_and_version(const framework_info &a, const framework_info &
                         {
                             trace::verbose(_X("Found FX version [%s]"), ver.c_str());
 
-                            framework_info info(fx_name, fx_dir, parsed);
+                            framework_info info(fx_name, fx_dir, parsed, hive_depth);
                             framework_infos->push_back(info);
                         }
                     }
                 }
             }
         }
+
+        hive_depth++;
     }
 
     std::sort(framework_infos->begin(), framework_infos->end(), compare_by_name_and_version);
index 28eb754..e4eea09 100644 (file)
@@ -9,10 +9,11 @@
 
 struct framework_info
 {
-    framework_info(pal::string_t name, pal::string_t path, fx_ver_t version)
+    framework_info(pal::string_t name, pal::string_t path, fx_ver_t version, int32_t hive_depth)
         : name(name)
         , path(path)
-        , version(version) { }
+        , version(version)
+        , hive_depth(hive_depth) { }
 
     static void get_all_framework_infos(
         const pal::string_t& own_dir,
@@ -24,6 +25,7 @@ struct framework_info
     pal::string_t name;
     pal::string_t path;
     fx_ver_t version;
+    int32_t hive_depth;
 };
 
 #endif // __FRAMEWORK_INFO_H_
index d761947..4283232 100644 (file)
@@ -14,6 +14,7 @@
 #include "hostfxr.h"
 #include "host_context.h"
 #include "bundle/info.h"
+#include <framework_info.h>
 
 namespace
 {
@@ -331,6 +332,138 @@ SHARED_API int32_t HOSTFXR_CALLTYPE hostfxr_get_available_sdks(
 }
 
 //
+// Returns available SDKs and frameworks.
+//
+// Resolves the existing SDKs and frameworks from a dotnet root directory (if
+// any), or the global default location. If multi-level lookup is enabled and
+// the dotnet root location is different than the global location, the SDKs and
+// frameworks will be enumerated from both locations.
+//
+// The SDKs are sorted in ascending order by version, multi-level lookup
+// locations are put before private ones.
+//
+// The frameworks are sorted in ascending order by name followed by version,
+// multi-level lookup locations are put before private ones.
+//
+// Parameters:
+//    dotnet_root
+//      The path to a directory containing a dotnet executable.
+//
+//    reserved
+//      Reserved for future parameters.
+//
+//    result
+//      Callback invoke to return the list of SDKs and frameworks.
+//      Structs and their elements are valid for the duration of the call.
+//
+//    result_context
+//      Additional context passed to the result callback.
+//
+// Return value:
+//   0 on success, otherwise failure.
+//
+// String encoding:
+//   Windows     - UTF-16 (pal::char_t is 2 byte wchar_t)
+//   Unix        - UTF-8  (pal::char_t is 1 byte char)
+//
+SHARED_API int32_t HOSTFXR_CALLTYPE hostfxr_get_dotnet_environment_info(
+    const pal::char_t* dotnet_root,
+    void* reserved,
+    hostfxr_get_dotnet_environment_info_result_fn result,
+    void* result_context)
+{
+    if (result == nullptr)
+    {
+        trace::error(_X("hostfxr_get_dotnet_environment_info received an invalid argument: result should not be null."));
+        return StatusCode::InvalidArgFailure;
+    }
+
+    if (reserved != nullptr)
+    {
+        trace::error(_X("hostfxr_get_dotnet_environment_info received an invalid argument: reserved should be null."));
+        return StatusCode::InvalidArgFailure;
+    }
+
+    pal::string_t dotnet_dir;
+    if (dotnet_root == nullptr)
+    {
+        if (pal::get_dotnet_self_registered_dir(&dotnet_dir) || pal::get_default_installation_dir(&dotnet_dir))
+        {
+            trace::info(_X("Using global installation location [%s]."), dotnet_dir.c_str());
+        }
+        else
+        {
+            trace::info(_X("No default dotnet installation could be obtained."));
+        }
+    }
+    else
+    {
+        dotnet_dir = dotnet_root;
+    }
+
+    std::vector<sdk_info> sdk_infos;
+    sdk_info::get_all_sdk_infos(dotnet_dir, &sdk_infos);
+
+    std::vector<hostfxr_dotnet_environment_sdk_info> environment_sdk_infos;
+    std::vector<pal::string_t> sdk_versions;
+    if (!sdk_infos.empty())
+    {
+        environment_sdk_infos.reserve(sdk_infos.size());
+        sdk_versions.reserve(sdk_infos.size());
+        for (const sdk_info& info : sdk_infos)
+        {
+            sdk_versions.push_back(info.version.as_str());
+            hostfxr_dotnet_environment_sdk_info sdk
+            {
+                sizeof(hostfxr_dotnet_environment_sdk_info),
+                sdk_versions.back().c_str(),
+                info.full_path.c_str()
+            };
+
+            environment_sdk_infos.push_back(sdk);
+        }
+    }
+
+    std::vector<framework_info> framework_infos;
+    framework_info::get_all_framework_infos(dotnet_dir, _X(""), &framework_infos);
+
+    std::vector<hostfxr_dotnet_environment_framework_info> environment_framework_infos;
+    std::vector<pal::string_t> framework_versions;
+    if (!framework_infos.empty())
+    {
+        environment_framework_infos.reserve(framework_infos.size());
+        framework_versions.reserve(framework_infos.size());
+        for (const framework_info& info : framework_infos)
+        {
+            framework_versions.push_back(info.version.as_str());
+            hostfxr_dotnet_environment_framework_info fw
+            {
+                sizeof(hostfxr_dotnet_environment_framework_info),
+                info.name.c_str(),
+                framework_versions.back().c_str(),
+                info.path.c_str()
+            };
+
+            environment_framework_infos.push_back(fw);
+        }
+    }
+
+    const hostfxr_dotnet_environment_info environment_info
+    {
+        sizeof(hostfxr_dotnet_environment_info),
+        _STRINGIFY(HOST_FXR_PKG_VER),
+        _STRINGIFY(REPO_COMMIT_HASH),
+        environment_sdk_infos.size(),
+        (environment_sdk_infos.empty()) ? nullptr : &environment_sdk_infos[0],
+        environment_framework_infos.size(),
+        (environment_framework_infos.empty()) ? nullptr : &environment_framework_infos[0]
+    };
+
+    result(&environment_info, result_context);
+    return StatusCode::Success;
+}
+
+//
 // Returns the native directories of the runtime based upon
 // the specified app.
 //
index e9a8602..3488a4f 100644 (file)
@@ -8,6 +8,7 @@ EXPORTS
     hostfxr_resolve_sdk
     hostfxr_resolve_sdk2
     hostfxr_get_available_sdks
+    hostfxr_get_dotnet_environment_info
     hostfxr_get_native_search_directories
     hostfxr_set_error_writer
     hostfxr_initialize_for_dotnet_command_line
index 105135d..1780b01 100644 (file)
@@ -7,6 +7,7 @@ hostfxr_main
 hostfxr_resolve_sdk
 hostfxr_resolve_sdk2
 hostfxr_get_available_sdks
+hostfxr_get_dotnet_environment_info
 hostfxr_get_native_search_directories
 hostfxr_set_error_writer
 hostfxr_initialize_for_dotnet_command_line
index e062193..2ef0af3 100644 (file)
@@ -285,4 +285,37 @@ typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_get_runtime_delegate_fn)(
 //
 typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_close_fn)(const hostfxr_handle host_context_handle);
 
+struct hostfxr_dotnet_environment_sdk_info
+{
+    size_t size;
+    const char_t* version;
+    const char_t* path;
+};
+
+typedef void(HOSTFXR_CALLTYPE* hostfxr_get_dotnet_environment_info_result_fn)(
+    const struct hostfxr_dotnet_environment_info* info,
+    void* result_context);
+
+struct hostfxr_dotnet_environment_framework_info
+{
+    size_t size;
+    const char_t* name;
+    const char_t* version;
+    const char_t* path;
+};
+
+struct hostfxr_dotnet_environment_info
+{
+    size_t size;
+
+    const char_t* hostfxr_version;
+    const char_t* hostfxr_commit_hash;
+
+    int32_t sdk_count;
+    const hostfxr_dotnet_environment_sdk_info* sdks;
+
+    int32_t framework_count;
+    const hostfxr_dotnet_environment_framework_info* frameworks;
+};
+
 #endif //__HOSTFXR_H__
index 6e67f50..05eb905 100644 (file)
@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Runtime.InteropServices;
 
 namespace HostApiInvokerApp
@@ -23,6 +24,38 @@ namespace HostApiInvokerApp
                 global_json_path = 1,
             }
 
+            [StructLayout(LayoutKind.Sequential, CharSet = Utils.OSCharSet)]
+            internal struct hostfxr_dotnet_environment_sdk_info
+            {
+                internal int size;
+                internal string version;
+                internal string path;
+            }
+
+            [StructLayout(LayoutKind.Sequential, CharSet = Utils.OSCharSet)]
+            internal struct hostfxr_dotnet_environment_framework_info
+            {
+                internal int size;
+                internal string name;
+                internal string version;
+                internal string path;
+            }
+
+            [StructLayout(LayoutKind.Sequential, CharSet = Utils.OSCharSet)]
+            internal struct hostfxr_dotnet_environment_info
+            {
+                internal int size;
+
+                internal string hostfxr_version;
+                internal string hostfxr_commit_hash;
+
+                internal int sdk_count;
+                internal IntPtr sdks;
+
+                internal int framework_count;
+                internal IntPtr frameworks;
+            }
+
             [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = Utils.OSCharSet)]
             internal delegate void hostfxr_resolve_sdk2_result_fn(
                 hostfxr_resolve_sdk2_result_key_t key,
@@ -53,6 +86,18 @@ namespace HostApiInvokerApp
             [DllImport(nameof(hostfxr), CharSet = Utils.OSCharSet, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
             internal static extern IntPtr hostfxr_set_error_writer(
                 hostfxr_error_writer_fn error_writer);
+
+            [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = Utils.OSCharSet)]
+            internal delegate void hostfxr_get_dotnet_environment_info_result_fn(
+                 IntPtr info,
+                 IntPtr result_context);
+
+            [DllImport(nameof(hostfxr), CharSet = Utils.OSCharSet, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
+            internal static extern int hostfxr_get_dotnet_environment_info(
+                string dotnet_root,
+                IntPtr reserved,
+                hostfxr_get_dotnet_environment_info_result_fn result,
+                IntPtr result_context);
         }
 
         /// <summary>
@@ -142,6 +187,84 @@ namespace HostApiInvokerApp
             }
         }
 
+        /// <summary>
+        /// Test that invokes native api hostfxr_get_dotnet_environment_info.
+        /// </summary>
+        /// <param name="args[0]">hostfxr_get_dotnet_environment_info</param>
+        /// <param name="args[1]">(Optional) Path to the directory with dotnet.exe</param>
+        static void Test_hostfxr_get_dotnet_environment_info(string[] args)
+        {
+            string dotnetExeDir = null;
+            if (args.Length >= 2)
+                dotnetExeDir = args[1];
+
+            string hostfxr_version;
+            string hostfxr_commit_hash;
+            List<hostfxr.hostfxr_dotnet_environment_sdk_info> sdks = new List<hostfxr.hostfxr_dotnet_environment_sdk_info>();
+            List<hostfxr.hostfxr_dotnet_environment_framework_info> frameworks = new List<hostfxr.hostfxr_dotnet_environment_framework_info>();
+
+            hostfxr.hostfxr_get_dotnet_environment_info_result_fn result_fn = (IntPtr info, IntPtr result_context) =>
+            {
+                hostfxr.hostfxr_dotnet_environment_info environment_info = Marshal.PtrToStructure<hostfxr.hostfxr_dotnet_environment_info>(info);
+
+                hostfxr_version = environment_info.hostfxr_version;
+                hostfxr_commit_hash = environment_info.hostfxr_commit_hash;
+                    
+                int env_info_size = Marshal.SizeOf(environment_info);
+                if (env_info_size != environment_info.size)
+                    throw new Exception($"Size field value of hostfxr_dotnet_environment_info struct is {environment_info.size} but {env_info_size} was expected.");
+                    
+                for (int i = 0; i < environment_info.sdk_count; i++)
+                {
+                    IntPtr pSdkInfo = new IntPtr(environment_info.sdks.ToInt64() + (i * Marshal.SizeOf<hostfxr.hostfxr_dotnet_environment_sdk_info>()));
+                    sdks.Add(Marshal.PtrToStructure<hostfxr.hostfxr_dotnet_environment_sdk_info>(pSdkInfo));
+
+                    if (Marshal.SizeOf(sdks[i]) != sdks[i].size)
+                        throw new Exception($"Size field value of hostfxr_dotnet_environment_sdk_info struct is {sdks[i].size} but {Marshal.SizeOf(sdks[i])} was expected.");
+                }
+
+                for (int i = 0; i < environment_info.framework_count; i++)
+                {
+                    IntPtr pFrameworkInfo = new IntPtr(environment_info.frameworks.ToInt64() + (i * Marshal.SizeOf<hostfxr.hostfxr_dotnet_environment_framework_info>()));
+                    frameworks.Add(Marshal.PtrToStructure<hostfxr.hostfxr_dotnet_environment_framework_info>(pFrameworkInfo));
+
+                    if (Marshal.SizeOf(frameworks[i]) != frameworks[i].size)
+                        throw new Exception($"Size field value of hostfxr_dotnet_environment_framework_info struct is {frameworks[i].size} but {Marshal.SizeOf(frameworks[i])} was expected.");
+                }
+
+                long result_context_as_int = result_context.ToInt64();
+                if (result_context_as_int != 42)
+                    throw new Exception($"Invalid result_context value: expected 42 but was {result_context_as_int}.");
+            };
+
+            if (dotnetExeDir == "test_invalid_result_ptr")
+                result_fn = null;
+
+            IntPtr reserved_ptr = IntPtr.Zero;
+            if (dotnetExeDir == "test_invalid_reserved_ptr")
+                reserved_ptr = new IntPtr(11);
+
+            int rc = hostfxr.hostfxr_get_dotnet_environment_info(
+                dotnet_root: dotnetExeDir,
+                reserved: reserved_ptr,
+                result: result_fn,
+                result_context: new IntPtr(42));
+
+            if (rc != 0)
+            {
+                Console.WriteLine($"hostfxr_get_dotnet_environment_info:Fail[{rc}]");
+            }
+
+            Console.WriteLine($"hostfxr_get_dotnet_environment_info sdk versions:[{string.Join(";", sdks.Select(s => s.version).ToList())}]");
+            Console.WriteLine($"hostfxr_get_dotnet_environment_info sdk paths:[{string.Join(";", sdks.Select(s => s.path).ToList())}]");
+
+            Console.WriteLine($"hostfxr_get_dotnet_environment_info framework names:[{string.Join(";", frameworks.Select(f => f.name).ToList())}]");
+            Console.WriteLine($"hostfxr_get_dotnet_environment_info framework versions:[{string.Join(";", frameworks.Select(f => f.version).ToList())}]");
+            Console.WriteLine($"hostfxr_get_dotnet_environment_info framework paths:[{string.Join(";", frameworks.Select(f => f.path).ToList())}]");
+
+            Console.WriteLine("hostfxr_get_dotnet_environment_info:Success");
+        }
+
         public static bool RunTest(string apiToTest, string[] args)
         {
             switch (apiToTest)
@@ -155,6 +278,9 @@ namespace HostApiInvokerApp
                 case nameof(Test_hostfxr_set_error_writer):
                     Test_hostfxr_set_error_writer(args);
                     break;
+                case nameof(hostfxr.hostfxr_get_dotnet_environment_info):
+                    Test_hostfxr_get_dotnet_environment_info(args);
+                    break;
                 default:
                     return false;
             }
index 27d3b27..a213579 100644 (file)
@@ -3,6 +3,7 @@
 
 using Microsoft.DotNet.Cli.Build;
 using System;
+using System.Collections.Generic;
 using System.IO;
 using System.Runtime.InteropServices;
 using Xunit;
@@ -66,12 +67,26 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
             public string SelfRegistered => Path.Combine(ExeDir, "sr");
             public string WorkingDir => Path.Combine(_fixture.TestProject.ProjectDirectory, "wd");
             public string ProgramFilesGlobalSdkDir => Path.Combine(ProgramFiles, "dotnet", "sdk");
+            public string ProgramFilesGlobalFrameworksDir => Path.Combine(ProgramFiles, "dotnet", "shared");
             public string SelfRegisteredGlobalSdkDir => Path.Combine(SelfRegistered, "sdk");
             public string LocalSdkDir => Path.Combine(ExeDir, "sdk");
+            public string LocalFrameworksDir => Path.Combine(ExeDir, "shared");
             public string GlobalJson => Path.Combine(WorkingDir, "global.json");
             public string[] ProgramFilesGlobalSdks = new[] { "4.5.6", "1.2.3", "2.3.4-preview" };
+            public List<(string fwName, string[] fwVersions)> ProgramFilesGlobalFrameworks =
+                new List<(string fwName, string[] fwVersions)>()
+                {
+                    ("HostFxr.Test.A", new[] { "1.2.3", "3.0.0" }),
+                    ("HostFxr.Test.B", new[] { "5.6.7-A" })
+                };
             public string[] SelfRegisteredGlobalSdks = new[] { "3.0.0", "15.1.4-preview", "5.6.7" };
             public string[] LocalSdks = new[] { "0.1.2", "5.6.7-preview", "1.2.3" };
+            public List<(string fwName, string[] fwVersions)> LocalFrameworks =
+                new List<(string fwName, string[] fwVersions)>()
+                {
+                    ("HostFxr.Test.B", new[] { "4.0.0", "5.6.7-A" }),
+                    ("HostFxr.Test.C", new[] { "3.0.0" })
+                };
 
             public SdkResolutionFixture(SharedTestState state)
             {
@@ -98,18 +113,29 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
                 {
                     Directory.CreateDirectory(Path.Combine(LocalSdkDir, sdk));
                 }
-            } 
+
+                foreach ((string fwName, string[] fwVersions) in ProgramFilesGlobalFrameworks)
+                {
+                    foreach (string fwVersion in fwVersions)
+                        Directory.CreateDirectory(Path.Combine(ProgramFilesGlobalFrameworksDir, fwName, fwVersion));
+                }
+                foreach ((string fwName, string[] fwVersions) in LocalFrameworks)
+                {
+                    foreach (string fwVersion in fwVersions)
+                        Directory.CreateDirectory(Path.Combine(LocalFrameworksDir, fwName, fwVersion));
+                }
+            }
         }
 
         [Fact]
         public void Hostfxr_get_available_sdks_with_multilevel_lookup()
         {
-            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
             {
                 // multilevel lookup is not supported on non-Windows
                 return;
             }
-            
+
             var f = new SdkResolutionFixture(sharedTestState);
 
             // With multi-level lookup (windows only): get local and global sdks sorted by ascending version,
@@ -244,6 +270,258 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
         }
 
         [Fact]
+        public void Hostfxr_get_dotnet_environment_info_dotnet_root_only()
+        {
+            var f = new SdkResolutionFixture(sharedTestState);
+            string expectedSdkVersions = string.Join(";", new[]
+            {
+                "0.1.2",
+                "1.2.3",
+                "5.6.7-preview"
+            });
+
+            string expectedSdkPaths = string.Join(';', new[]
+            {
+                 Path.Combine(f.LocalSdkDir, "0.1.2"),
+                 Path.Combine(f.LocalSdkDir, "1.2.3"),
+                 Path.Combine(f.LocalSdkDir, "5.6.7-preview"),
+            });
+
+            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")
+            });
+
+            f.Dotnet.Exec(f.AppDll, new[] { "hostfxr_get_dotnet_environment_info", f.ExeDir })
+            .CaptureStdOut()
+            .CaptureStdErr()
+            .Execute()
+            .Should().Pass()
+            .And.HaveStdOutContaining("hostfxr_get_dotnet_environment_info:Success")
+            .And.HaveStdOutContaining($"hostfxr_get_dotnet_environment_info sdk versions:[{expectedSdkVersions}]")
+            .And.HaveStdOutContaining($"hostfxr_get_dotnet_environment_info sdk paths:[{expectedSdkPaths}]")
+            .And.HaveStdOutContaining($"hostfxr_get_dotnet_environment_info framework names:[{expectedFrameworkNames}]")
+            .And.HaveStdOutContaining($"hostfxr_get_dotnet_environment_info framework versions:[{expectedFrameworkVersions}]")
+            .And.HaveStdOutContaining($"hostfxr_get_dotnet_environment_info framework paths:[{expectedFrameworkPaths}]");
+        }
+
+        [Fact]
+        public void Hostfxr_get_dotnet_environment_info_with_multilevel_lookup_with_dotnet_root()
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                // Only Windows supports multi-level lookup.
+                return;
+            }
+
+            var f = new SdkResolutionFixture(sharedTestState);
+            string expectedSdkVersions = string.Join(';', new[]
+            {
+                "0.1.2",
+                "1.2.3",
+                "1.2.3",
+                "2.3.4-preview",
+                "3.0.0",
+                "4.5.6",
+                "5.6.7-preview",
+                "5.6.7",
+                "15.1.4-preview"
+            });
+
+            string expectedSdkPaths = string.Join(';', new[]
+            {
+                Path.Combine(f.LocalSdkDir, "0.1.2"),
+                Path.Combine(f.ProgramFilesGlobalSdkDir, "1.2.3"),
+                Path.Combine(f.LocalSdkDir, "1.2.3"),
+                Path.Combine(f.ProgramFilesGlobalSdkDir, "2.3.4-preview"),
+                Path.Combine(f.SelfRegisteredGlobalSdkDir, "3.0.0"),
+                Path.Combine(f.ProgramFilesGlobalSdkDir, "4.5.6"),
+                Path.Combine(f.LocalSdkDir, "5.6.7-preview"),
+                Path.Combine(f.SelfRegisteredGlobalSdkDir, "5.6.7"),
+                Path.Combine(f.SelfRegisteredGlobalSdkDir, "15.1.4-preview"),
+            });
+
+            string expectedFrameworkNames = string.Join(';', new[]
+            {
+                "HostFxr.Test.A",
+                "HostFxr.Test.A",
+                "HostFxr.Test.B",
+                "HostFxr.Test.B",
+                "HostFxr.Test.B",
+                "HostFxr.Test.C"
+            });
+
+            string expectedFrameworkVersions = string.Join(';', new[]
+            {
+                "1.2.3",
+                "3.0.0",
+                "4.0.0",
+                "5.6.7-A",
+                "5.6.7-A",
+                "3.0.0"
+            });
+
+            string expectedFrameworkPaths = string.Join(';', new[]
+            {
+                Path.Combine(f.ProgramFilesGlobalFrameworksDir, "HostFxr.Test.A"),
+                Path.Combine(f.ProgramFilesGlobalFrameworksDir, "HostFxr.Test.A"),
+                Path.Combine(f.LocalFrameworksDir, "HostFxr.Test.B"),
+                Path.Combine(f.ProgramFilesGlobalFrameworksDir, "HostFxr.Test.B"),
+                Path.Combine(f.LocalFrameworksDir, "HostFxr.Test.B"),
+                Path.Combine(f.LocalFrameworksDir, "HostFxr.Test.C")
+            });
+
+            using (TestOnlyProductBehavior.Enable(f.Dotnet.GreatestVersionHostFxrFilePath))
+            {
+                f.Dotnet.Exec(f.AppDll, new[] { "hostfxr_get_dotnet_environment_info", f.ExeDir })
+                .EnvironmentVariable("TEST_MULTILEVEL_LOOKUP_PROGRAM_FILES", f.ProgramFiles)
+                .EnvironmentVariable("TEST_MULTILEVEL_LOOKUP_SELF_REGISTERED", f.SelfRegistered)
+                .CaptureStdOut()
+                .CaptureStdErr()
+                .Execute()
+                .Should().Pass()
+                .And.HaveStdOutContaining("hostfxr_get_dotnet_environment_info:Success")
+                .And.HaveStdOutContaining($"hostfxr_get_dotnet_environment_info sdk versions:[{expectedSdkVersions}]")
+                .And.HaveStdOutContaining($"hostfxr_get_dotnet_environment_info sdk paths:[{expectedSdkPaths}]")
+                .And.HaveStdOutContaining($"hostfxr_get_dotnet_environment_info framework names:[{expectedFrameworkNames}]")
+                .And.HaveStdOutContaining($"hostfxr_get_dotnet_environment_info framework versions:[{expectedFrameworkVersions}]")
+                .And.HaveStdOutContaining($"hostfxr_get_dotnet_environment_info framework paths:[{expectedFrameworkPaths}]");
+            }
+        }
+
+        [Fact]
+        public void Hostfxr_get_dotnet_environment_info_with_multilevel_lookup_only()
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                // Only Windows supports multi-level lookup.
+                return;
+            }
+
+            var f = new SdkResolutionFixture(sharedTestState);
+            string expectedSdkVersions = string.Join(';', new[]
+            {
+                "1.2.3",
+                "2.3.4-preview",
+                "3.0.0",
+                "4.5.6",
+                "5.6.7",
+                "15.1.4-preview"
+            });
+
+            string expectedSdkPaths = string.Join(';', new[]
+            {
+                Path.Combine(f.ProgramFilesGlobalSdkDir, "1.2.3"),
+                Path.Combine(f.ProgramFilesGlobalSdkDir, "2.3.4-preview"),
+                Path.Combine(f.SelfRegisteredGlobalSdkDir, "3.0.0"),
+                Path.Combine(f.ProgramFilesGlobalSdkDir, "4.5.6"),
+                Path.Combine(f.SelfRegisteredGlobalSdkDir, "5.6.7"),
+                Path.Combine(f.SelfRegisteredGlobalSdkDir, "15.1.4-preview"),
+            });
+
+            string expectedFrameworkNames = string.Join(';', new[]
+            {
+                "HostFxr.Test.A",
+                "HostFxr.Test.A",
+                "HostFxr.Test.B",
+            });
+
+            string expectedFrameworkVersions = string.Join(';', new[]
+            {
+                "1.2.3",
+                "3.0.0",
+                "5.6.7-A",
+            });
+
+            string expectedFrameworkPaths = string.Join(';', new[]
+            {
+                Path.Combine(f.ProgramFilesGlobalFrameworksDir, "HostFxr.Test.A"),
+                Path.Combine(f.ProgramFilesGlobalFrameworksDir, "HostFxr.Test.A"),
+                Path.Combine(f.ProgramFilesGlobalFrameworksDir, "HostFxr.Test.B"),
+            });
+
+            using (TestOnlyProductBehavior.Enable(f.Dotnet.GreatestVersionHostFxrFilePath))
+            {
+                // We pass f.WorkingDir so that we don't resolve dotnet_dir to the global installation
+                // in the native side.
+                f.Dotnet.Exec(f.AppDll, new[] { "hostfxr_get_dotnet_environment_info", f.WorkingDir })
+                .EnvironmentVariable("TEST_MULTILEVEL_LOOKUP_PROGRAM_FILES", f.ProgramFiles)
+                .EnvironmentVariable("TEST_MULTILEVEL_LOOKUP_SELF_REGISTERED", f.SelfRegistered)
+                .CaptureStdOut()
+                .CaptureStdErr()
+                .Execute()
+                .Should().Pass()
+                .And.HaveStdOutContaining("hostfxr_get_dotnet_environment_info:Success")
+                .And.HaveStdOutContaining($"hostfxr_get_dotnet_environment_info sdk versions:[{expectedSdkVersions}]")
+                .And.HaveStdOutContaining($"hostfxr_get_dotnet_environment_info sdk paths:[{expectedSdkPaths}]")
+                .And.HaveStdOutContaining($"hostfxr_get_dotnet_environment_info framework names:[{expectedFrameworkNames}]")
+                .And.HaveStdOutContaining($"hostfxr_get_dotnet_environment_info framework versions:[{expectedFrameworkVersions}]")
+                .And.HaveStdOutContaining($"hostfxr_get_dotnet_environment_info framework paths:[{expectedFrameworkPaths}]");
+            }
+        }
+
+        [Fact]
+        public void Hostfxr_get_dotnet_environment_info_global_install_path()
+        {
+            var f = new SdkResolutionFixture(sharedTestState);
+            
+            f.Dotnet.Exec(f.AppDll, new[] { "hostfxr_get_dotnet_environment_info" })
+            .CaptureStdOut()
+            .CaptureStdErr()
+            .Execute()
+            .Should().Pass()
+            .And.HaveStdOutContaining("hostfxr_get_dotnet_environment_info:Success");
+        }
+
+        [Fact]
+        public void Hostfxr_get_dotnet_environment_info_result_is_nullptr_fails()
+        {
+            var f = new SdkResolutionFixture(sharedTestState);
+
+            f.Dotnet.Exec(f.AppDll, new[] { "hostfxr_get_dotnet_environment_info", "test_invalid_result_ptr" })
+            .EnvironmentVariable("COREHOST_TRACE", "1")
+            .CaptureStdOut()
+            .CaptureStdErr()
+            .Execute()
+            .Should().Pass()
+            // 0x80008081 (InvalidArgFailure)
+            .And.HaveStdOutContaining("hostfxr_get_dotnet_environment_info:Fail[-2147450751]")
+            .And.HaveStdErrContaining("hostfxr_get_dotnet_environment_info received an invalid argument: result should not be null.");
+        }
+
+        [Fact]
+        public void Hostfxr_get_dotnet_environment_info_reserved_is_not_nullptr_fails()
+        {
+            var f = new SdkResolutionFixture(sharedTestState);
+
+            f.Dotnet.Exec(f.AppDll, new[] { "hostfxr_get_dotnet_environment_info", "test_invalid_reserved_ptr" })
+            .EnvironmentVariable("COREHOST_TRACE", "1")
+            .CaptureStdOut()
+            .CaptureStdErr()
+            .Execute()
+            .Should().Pass()
+            // 0x80008081 (InvalidArgFailure)
+            .And.HaveStdOutContaining("hostfxr_get_dotnet_environment_info:Fail[-2147450751]")
+            .And.HaveStdErrContaining("hostfxr_get_dotnet_environment_info received an invalid argument: reserved should be null.");
+        }
+
+        [Fact]
         public void Hostpolicy_corehost_set_error_writer_test()
         {
             var fixture = sharedTestState.HostApiInvokerAppFixture.Copy();
@@ -295,7 +573,7 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
                         RuntimeInformationExtensions.GetSharedLibraryFileNameForCurrentPlatform("hostfxr"));
 
                     FileUtils.CopyIntoDirectory(
-                        hostfxr, 
+                        hostfxr,
                         Path.GetDirectoryName(fixture.TestProject.AppDll));
                 }
             }