New hostfxr API to support new MSBuild SDK resolver scenarios (dotnet/core-setup...
authorNick Guerrera <nicholg@microsoft.com>
Thu, 16 Aug 2018 19:37:07 +0000 (12:37 -0700)
committerGitHub <noreply@github.com>
Thu, 16 Aug 2018 19:37:07 +0000 (12:37 -0700)
1. hostfxr_resolve_sdk2

Deprecates hostfxr_resolve_sdk and adds two new capabilities:

  * Return the path to global.json if global.json is used

    This allows the msbuild sdk resolver to customize its error handling and
    logging appropriately. It will also be used to enable msbuild to fall back
    to most recent compatible SDK when the most recent SDK chosen without
    global.json is incompatible with the current msbuild version.

  * Add disallow_prerelease flag

    When set, this will prevent prerelease SDKs from being resolved unless a
    prerelease version was specified in global.json. This will be used to
    prevent prerelease VS installations from impacting side-by-side releases.

2. hostfxr_get_available_sdks

Returns all available SDKs in sorted order by version.

Allows MSBuild SDK resolver to fall back to latest compatible SDK when there is
no global.json and latest SDK is incompatible with current version of MSBuild.

Commit migrated from https://github.com/dotnet/core-setup/commit/84f06c2fe54d25d86d48ae3b0daae5f3f40cc19f

src/installer/corehost/cli/fxr/hostfxr.cpp
src/installer/corehost/cli/fxr/sdk_info.cpp
src/installer/corehost/cli/fxr/sdk_info.h
src/installer/corehost/cli/fxr/sdk_resolver.cpp
src/installer/corehost/cli/fxr/sdk_resolver.h
src/installer/corehost/error_codes.h
src/installer/test/Assets/TestProjects/HostApiInvokerApp/HostApiInvokerApp.csproj
src/installer/test/Assets/TestProjects/HostApiInvokerApp/Program.cs
src/installer/test/HostActivationTests/GivenThatICareAboutNativeHostApis.cs

index 994fc50..10a8dba 100644 (file)
@@ -10,6 +10,7 @@
 #include "error_codes.h"
 #include "libhost.h"
 #include "runtime_config.h"
+#include "sdk_info.h"
 #include "sdk_resolver.h"
 
 typedef int(*corehost_load_fn) (const host_interface_t* init);
@@ -184,6 +185,7 @@ SHARED_API int hostfxr_main(const int argc, const pal::char_t* argv[])
     return muxer.execute(pal::string_t(), argc, argv, startup_info, nullptr, 0, nullptr);
 }
 
+// [OBSOLETE] Replaced by hostfxr_resolve_sdk2
 //
 // Determines the directory location of the SDK accounting for
 // global.json and multi-level lookup policy.
@@ -274,6 +276,185 @@ SHARED_API int32_t hostfxr_resolve_sdk(
     return cli_sdk.size() + 1;
 }
 
+enum hostfxr_resolve_sdk2_flags_t : int32_t
+{
+    disallow_prerelease = 0x1,
+};
+
+enum class hostfxr_resolve_sdk2_result_key_t : int32_t
+{
+    resolved_sdk_dir = 0,
+    global_json_path = 1,
+};
+
+typedef void (*hostfxr_resolve_sdk2_result_fn)(
+    hostfxr_resolve_sdk2_result_key_t key,
+    const pal::char_t* value);
+
+//
+// Determines the directory location of the SDK accounting for
+// global.json and multi-level lookup policy.
+//
+// Invoked via MSBuild SDK resolver to locate SDK props and targets
+// from an msbuild other than the one bundled by the CLI.
+//
+// Parameters:
+//    exe_dir
+//      The main directory where SDKs are located in sdk\[version]
+//      sub-folders. Pass the directory of a dotnet executable to
+//      mimic how that executable would search in its own directory.
+//      It is also valid to pass nullptr or empty, in which case
+//      multi-level lookup can still search other locations if 
+//      it has not been disabled by the user's environment.
+//
+//    working_dir
+//      The directory where the search for global.json (which can
+//      control the resolved SDK version) starts and proceeds
+//      upwards. 
+//
+//   flags
+//      Bitwise flags that influence resolution.
+//         disallow_prerelease (0x1)
+//           do not allow resolution to return a prerelease SDK version 
+//           unless  prerelease version was specified via global.json.
+//
+//   result
+//      Callback invoked to return values. It can be invoked more
+//      than once. String values passed are valid only for the
+//      duration of a call.
+//
+//      If resolution succeeds, result will be invoked with
+//      resolved_sdk_dir key and the value will hold the
+//      path to the resolved SDK director, otherwise it will
+//      be null.
+//
+//      If global.json is used then result will be invoked with
+//      global_json_path key and the value  will hold the path
+//      to global.json. If there was no global.json found,
+//      or the contents of global.json did not impact resolution
+//      (e.g. no version specified), then result will not be
+//      invoked with global_json_path key.
+//
+// Return value:
+//   0 on success, otherwise failure
+//   0x8000809b - SDK could not be resolved (SdkResolverResolveFailure)
+// 
+// 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_resolve_sdk2(
+    const pal::char_t* exe_dir,
+    const pal::char_t* working_dir,
+    int32_t flags,
+    hostfxr_resolve_sdk2_result_fn result)
+{
+    trace::setup();
+
+    trace::info(_X("--- Invoked hostfxr [commit hash: %s] hostfxr_resolve_sdk2"), _STRINGIFY(REPO_COMMIT_HASH));
+
+    if (exe_dir == nullptr)
+    {
+        exe_dir = _X("");
+    }
+
+    if (working_dir == nullptr)
+    {
+        working_dir = _X("");
+    }
+
+    pal::string_t resolved_sdk_dir;
+    pal::string_t global_json_path;
+
+    bool success = sdk_resolver_t::resolve_sdk_dotnet_path(
+        exe_dir, 
+        working_dir,
+        &resolved_sdk_dir,
+        (flags & hostfxr_resolve_sdk2_flags_t::disallow_prerelease) != 0,
+        &global_json_path);
+
+    if (success)
+    {
+        result(
+            hostfxr_resolve_sdk2_result_key_t::resolved_sdk_dir,
+            resolved_sdk_dir.c_str());
+    }
+
+    if (!global_json_path.empty())
+    {
+        result(
+            hostfxr_resolve_sdk2_result_key_t::global_json_path,
+            global_json_path.c_str());
+    }
+
+    return success
+        ? StatusCode::Success 
+        : StatusCode::SdkResolverResolveFailure;
+}
+
+
+typedef void (*hostfxr_get_available_sdks_result_fn)(
+    int32_t sdk_count,
+    const pal::char_t *sdk_dirs[]);
+
+//
+// Returns the list of all available SDKs ordered by ascending version.
+//
+// Invoked by MSBuild resolver when the latest SDK used without global.json
+// present is incompatible with the current MSBuild version. It will select
+// the compatible SDK that is closest to the end of this list.
+//
+// Parameters:
+//    exe_dir
+//      The path to the dotnet executable.
+//
+//    result
+//      Callback invoke to return the list of SDKs by their directory paths.
+//      String array and its elements are valid for the duration of the call.
+//
+// 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_get_available_sdks(
+    const pal::char_t* exe_dir,
+    hostfxr_get_available_sdks_result_fn result)
+{
+    trace::setup();
+
+    trace::info(_X("--- Invoked hostfxr [commit hash: %s] hostfxr_get_available_sdks"), _STRINGIFY(REPO_COMMIT_HASH));
+
+    if (exe_dir == nullptr)
+    {
+        exe_dir = _X("");
+    }
+
+    std::vector<sdk_info> sdk_infos;
+    sdk_info::get_all_sdk_infos(exe_dir, &sdk_infos);
+
+    if (sdk_infos.empty())
+    {
+        result(0, nullptr);
+    }
+    else
+    {
+        std::vector<const pal::char_t*> sdk_dirs;
+        sdk_dirs.reserve(sdk_infos.size());
+
+        for (const auto& sdk_info : sdk_infos)
+        {
+            sdk_dirs.push_back(sdk_info.full_path.c_str());
+        }
+
+        result(sdk_dirs.size(), &sdk_dirs[0]);
+    }
+    
+    return StatusCode::Success;
+}
+
 //
 // Returns the native directories of the runtime based upon
 // the specified app.
index dc93def..d927f13 100644 (file)
@@ -7,9 +7,37 @@
 #include "trace.h"
 #include "utils.h"
 
-bool compare_by_version(const sdk_info &a, const sdk_info &b)
+bool compare_by_version_ascending_then_hive_depth_descending(const sdk_info &a, const sdk_info &b)
 {
-    return a.version < b.version;
+    if (a.version < b.version)
+    {
+        return true;
+    }
+
+    // With multi-level lookup enabled, it is possible to find two SDKs with
+    // the same version. For that edge case, we make the ordering put SDKs
+    // from farther away (global location) hives earlier than closer ones
+    // (current dotnet exe location). Without this tie-breaker, the ordering
+    // would be non-deterministic.
+    //
+    // Furthermore,  nearer earlier than farther is so that the MSBuild resolver
+    // can do a linear search from the end of the list to the front to find the
+    // best compatible SDK.
+    //
+    // Example:
+    //    * dotnet dir has version 4.0, 5.0, 6.0
+    //    * global dir has 5.0
+    //    * 6.0 is incompatible with calling msbuild
+    //    * 5.0 is compatible with calling msbuild
+    //
+    // MSBuild should select 5.0 from dotnet dir (matching probe order) in muxer
+    // and not 5.0 from global dir.
+    if (a.version == b.version)
+    {
+        return a.hive_depth > b.hive_depth;
+    }
+
+    return false;
 }
 
 void sdk_info::get_all_sdk_infos(
@@ -37,17 +65,19 @@ void sdk_info::get_all_sdk_infos(
         }
     }
 
+    int32_t hive_depth = 0;
+
     for (pal::string_t dir : hive_dir)
     {
-        auto sdk_dir = dir;
-        trace::verbose(_X("Gathering SDK locations in [%s]"), sdk_dir.c_str());
+        auto base_dir = dir;
+        trace::verbose(_X("Gathering SDK locations in [%s]"), base_dir.c_str());
 
-        append_path(&sdk_dir, _X("sdk"));
+        append_path(&base_dir, _X("sdk"));
 
-        if (pal::directory_exists(sdk_dir))
+        if (pal::directory_exists(base_dir))
         {
             std::vector<pal::string_t> versions;
-            pal::readdir_onlydirectories(sdk_dir, &versions);
+            pal::readdir_onlydirectories(base_dir, &versions);
             for (const auto& ver : versions)
             {
                 // Make sure we filter out any non-version folders.
@@ -56,15 +86,20 @@ void sdk_info::get_all_sdk_infos(
                 {
                     trace::verbose(_X("Found SDK version [%s]"), ver.c_str());
 
-                    sdk_info info(sdk_dir, parsed);
+                    auto full_dir = base_dir;
+                    append_path(&full_dir, ver.c_str());
+
+                    sdk_info info(base_dir, full_dir, parsed, hive_depth);
 
                     sdk_infos->push_back(info);
                 }
             }
         }
+
+        hive_depth++;
     }
 
-    std::sort(sdk_infos->begin(), sdk_infos->end(), compare_by_version);
+    std::sort(sdk_infos->begin(), sdk_infos->end(), compare_by_version_ascending_then_hive_depth_descending);
 }
 
 /*static*/ bool sdk_info::print_all_sdks(const pal::string_t& own_dir, const pal::string_t& leading_whitespace)
@@ -73,7 +108,7 @@ void sdk_info::get_all_sdk_infos(
     get_all_sdk_infos(own_dir, &sdk_infos);
     for (sdk_info info : sdk_infos)
     {
-        trace::println(_X("%s%s [%s]"), leading_whitespace.c_str(), info.version.as_str().c_str(), info.path.c_str());
+        trace::println(_X("%s%s [%s]"), leading_whitespace.c_str(), info.version.as_str().c_str(), info.base_path.c_str());
     }
 
     return sdk_infos.size() > 0;
index 1a43ccb..1c296fc 100644 (file)
@@ -8,9 +8,11 @@
 
 struct sdk_info
 {
-    sdk_info(pal::string_t path, fx_ver_t version)
-        : path(path)
-        , version(version) { }
+    sdk_info(const pal::string_t& base_path, const pal::string_t& full_path, const fx_ver_t& version, int32_t hive_depth)
+        : base_path(base_path)
+        , full_path(full_path)
+        , version(version)
+        , hive_depth(hive_depth) { }
 
     static void get_all_sdk_infos(
         const pal::string_t& own_dir,
@@ -18,8 +20,10 @@ struct sdk_info
 
     static bool print_all_sdks(const pal::string_t& own_dir, const pal::string_t& leading_whitespace);
 
-    pal::string_t path;
+    pal::string_t base_path;
+    pal::string_t full_path;
     fx_ver_t version;
+    int32_t hive_depth;
 };
 
 #endif // __SDK_INFO_H_
index 3e7c3d6..61b19e4 100644 (file)
@@ -64,7 +64,7 @@ pal::string_t resolve_cli_version(const pal::string_t& global_json)
     return retval;
 }
 
-pal::string_t resolve_sdk_version(pal::string_t sdk_path, bool parse_only_production, pal::string_t global_cli_version)
+pal::string_t resolve_sdk_version(pal::string_t sdk_path, bool disallow_prerelease, pal::string_t global_cli_version)
 {
     fx_ver_t specified(-1, -1, -1);
 
@@ -76,6 +76,12 @@ pal::string_t resolve_sdk_version(pal::string_t sdk_path, bool parse_only_produc
             trace::error(_X("The specified SDK version '%s' could not be parsed"), global_cli_version.c_str());
             return pal::string_t();
         }
+
+        // Always consider prereleases when the version specified in global.json is itself a prerelease
+        if (specified.is_prerelease())
+        {
+            disallow_prerelease = false;
+        }
     }
 
     trace::verbose(_X("--- Resolving SDK version from SDK dir [%s]"), sdk_path.c_str());
@@ -90,7 +96,7 @@ pal::string_t resolve_sdk_version(pal::string_t sdk_path, bool parse_only_produc
         trace::verbose(_X("Considering version... [%s]"), version.c_str());
 
         fx_ver_t ver(-1, -1, -1);
-        if (fx_ver_t::parse(version, &ver, parse_only_production))
+        if (fx_ver_t::parse(version, &ver, disallow_prerelease))
         {
             if (global_cli_version.empty() ||
                 // If a global cli version is specified:
@@ -130,15 +136,16 @@ bool sdk_resolver_t::resolve_sdk_dotnet_path(const pal::string_t& dotnet_root, p
     return resolve_sdk_dotnet_path(dotnet_root, cwd, cli_sdk);
 }
 
-bool higher_sdk_version(const pal::string_t& new_version, pal::string_t* version, bool parse_only_production)
+bool higher_sdk_version(const pal::string_t& new_version, pal::string_t* version)
 {
+    bool disallow_prerelease = false;
     bool retval = false;
     fx_ver_t ver(-1, -1, -1);
     fx_ver_t new_ver(-1, -1, -1);
 
-    if (fx_ver_t::parse(new_version, &new_ver, parse_only_production))
+    if (fx_ver_t::parse(new_version, &new_ver, disallow_prerelease))
     {
-        if (!fx_ver_t::parse(*version, &ver, parse_only_production) || (new_ver > ver))
+        if (!fx_ver_t::parse(*version, &ver, disallow_prerelease) || (new_ver > ver))
         {
             version->assign(new_version);
             retval = true;
@@ -148,7 +155,12 @@ bool higher_sdk_version(const pal::string_t& new_version, pal::string_t* version
     return retval;
 }
 
-bool sdk_resolver_t::resolve_sdk_dotnet_path(const pal::string_t& dotnet_root, const pal::string_t& cwd, pal::string_t* cli_sdk)
+bool sdk_resolver_t::resolve_sdk_dotnet_path(
+    const pal::string_t& dotnet_root, 
+    const pal::string_t& cwd, 
+    pal::string_t* cli_sdk,
+    bool disallow_prerelease,
+    pal::string_t* global_json_path)
 {
     pal::string_t global;
 
@@ -214,18 +226,22 @@ bool sdk_resolver_t::resolve_sdk_dotnet_path(const pal::string_t& dotnet_root, c
         trace::verbose(_X("Searching SDK directory in [%s]"), dir.c_str());
         pal::string_t current_sdk_path = dir;
         append_path(&current_sdk_path, _X("sdk"));
-        bool parse_only_production = false;  // false -- implies both production and prerelease.
 
         if (global_cli_version.empty())
         {
-            pal::string_t new_cli_version = resolve_sdk_version(current_sdk_path, parse_only_production, global_cli_version);
-            if (higher_sdk_version(new_cli_version, &cli_version, parse_only_production))
+            pal::string_t new_cli_version = resolve_sdk_version(current_sdk_path, disallow_prerelease, global_cli_version);
+            if (higher_sdk_version(new_cli_version, &cli_version))
             {
                 sdk_path = current_sdk_path;
             }
         }
         else
         {
+            if (global_json_path != nullptr)
+            {
+                global_json_path->assign(global);
+            }
+
             pal::string_t probing_sdk_path = current_sdk_path;
             append_path(&probing_sdk_path, global_cli_version.c_str());
 
@@ -239,8 +255,8 @@ bool sdk_resolver_t::resolve_sdk_dotnet_path(const pal::string_t& dotnet_root, c
             }
             else
             {
-                pal::string_t new_cli_version = resolve_sdk_version(current_sdk_path, parse_only_production, global_cli_version);
-                if (higher_sdk_version(new_cli_version, &cli_version, parse_only_production))
+                pal::string_t new_cli_version = resolve_sdk_version(current_sdk_path, disallow_prerelease, global_cli_version);
+                if (higher_sdk_version(new_cli_version, &cli_version))
                 {
                     sdk_path = current_sdk_path;
                 }
index cce2c74..1e5da8c 100644 (file)
@@ -12,6 +12,8 @@ public:
 
     static bool resolve_sdk_dotnet_path(
         const pal::string_t& dotnet_root,
-        const pal::string_t& cwd, 
-        pal::string_t* cli_sdk);
+        const pal::string_t& cwd,
+        pal::string_t* cli_sdk,
+        bool disallow_prerelease = false,
+        pal::string_t* global_json_path = nullptr);
 };
index 220d2de..995c193 100644 (file)
@@ -32,5 +32,6 @@ enum StatusCode
     HostApiBufferTooSmall       = 0x80008098,
     LibHostUnknownCommand       = 0x80008099,
     LibHostAppRootFindFailure   = 0x8000809a,
+    SdkResolverResolveFailure   = 0x8000809b,
 };
 #endif // __ERROR_CODES_H__
index 83efdd4..15f04f3 100644 (file)
@@ -4,6 +4,8 @@
     <TargetFramework>$(NETCoreAppFramework)</TargetFramework>
     <OutputType>Exe</OutputType>
     <RuntimeFrameworkVersion>$(MNAVersion)</RuntimeFrameworkVersion>
+    <LangVersion>Latest</LangVersion>
+    <DefineConstants Condition="'$(OS)' == 'Windows_NT'">WINDOWS;$(DefineConstants)</DefineConstants>
   </PropertyGroup>
 
   <ItemGroup>
index 21a6583..1da6db1 100644 (file)
@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Runtime.InteropServices;
 using System.Text;
 
@@ -6,12 +7,75 @@ namespace StandaloneApp
 {
     public static class Program
     {
-        [DllImport("hostfxr", CharSet = CharSet.Unicode)]
-        static extern uint hostfxr_get_native_search_directories(int argc, IntPtr argv, StringBuilder buffer, int bufferSize, ref int required_buffer_size);
+#if WINDOWS
+       const CharSet OSCharSet = CharSet.Unicode;
+#else
+       const CharSet OSCharSet = CharSet.Ansi; // actually UTF8 on Unix
+#endif
+
+        [DllImport("hostfxr", CharSet = OSCharSet)]
+        static extern uint hostfxr_get_native_search_directories(
+            int argc, 
+            [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)]
+            string[] argv, 
+            StringBuilder buffer, 
+            int bufferSize, 
+            ref int required_buffer_size);
+
+        [Flags]
+        internal enum hostfxr_resolve_sdk2_flags_t : int
+        {
+            disallow_prerelease = 0x1,
+        }
+
+        internal enum hostfxr_resolve_sdk2_result_key_t : int
+        {
+            resolved_sdk_dir = 0,
+            global_json_path = 1,
+        }
+
+        [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = OSCharSet)]
+        internal delegate void hostfxr_resolve_sdk2_result_fn(
+            hostfxr_resolve_sdk2_result_key_t key,
+            string value);
+
+        [DllImport("hostfxr", CharSet = OSCharSet, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
+        internal static extern int hostfxr_resolve_sdk2(
+            string exe_dir,
+            string working_dir,
+            hostfxr_resolve_sdk2_flags_t flags,
+            hostfxr_resolve_sdk2_result_fn result);
+
+        [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = OSCharSet)]
+        internal delegate void hostfxr_get_available_sdks_result_fn(
+            int sdk_count,
+            [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)]
+            string[] sdk_dirs);
+
+        [DllImport("hostfxr", CharSet = OSCharSet, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
+        internal static extern int hostfxr_get_available_sdks(
+            string exe_dir,
+            hostfxr_get_available_sdks_result_fn result);
 
         const uint HostApiBufferTooSmall = 0x80008098;
 
-        public static void Main(string[] args)
+        public static int Main(string[] args)
+        {
+            // write exception details to stdout so tha they can be seen in test assertion failures.
+            try
+            {
+                MainCore(args);
+            }
+            catch (Exception ex)
+            {
+                Console.WriteLine(ex);
+                return -1;
+            }
+
+            return 0;
+        }
+
+        public static void MainCore(string[] args)
         {
             Console.WriteLine("Hello World!");
             Console.WriteLine(string.Join(Environment.NewLine, args));
@@ -19,19 +83,46 @@ namespace StandaloneApp
             // A small operation involving NewtonSoft.Json to ensure the assembly is loaded properly
             var t = typeof(Newtonsoft.Json.JsonReader);
 
+            // Enable tracing so that test assertion failures are easier to diagnose.
+            Environment.SetEnvironmentVariable("COREHOST_TRACE", "1");
+
+            // If requested, test multilevel lookup using fake ProgramFiles location.
+            // Note that this has to be set here and not in the calling test process because 
+            // %ProgramFiles% gets reset on process creation.
+            string testMultilevelLookupProgramFiles = Environment.GetEnvironmentVariable(
+                "TEST_MULTILEVEL_LOOKUP_PROGRAM_FILES");
+
+            if (testMultilevelLookupProgramFiles != null)
+            {
+                Environment.SetEnvironmentVariable("ProgramFiles", testMultilevelLookupProgramFiles);
+                Environment.SetEnvironmentVariable("ProgramFiles(x86)", testMultilevelLookupProgramFiles);
+                Environment.SetEnvironmentVariable("DOTNET_MULTILEVEL_LOOKUP", "1");
+            }
+            else
+            {
+                // never rely on machine state in test if we're not faking the multi-level lookup
+                Environment.SetEnvironmentVariable("DOTNET_MULTILEVEL_LOOKUP", "0");
+            }
+
             if (args.Length == 0)
             {
                 throw new Exception("Invalid number of arguments passed");
             }
 
             string apiToTest = args[0];
-            if (apiToTest == "hostfxr_get_native_search_directories")
+            switch (apiToTest)
             {
-                Test_hostfxr_get_native_search_directories(args);
-            }
-            else
-            {
-                throw new Exception("Invalid args[0]");
+                case nameof(hostfxr_get_native_search_directories):
+                    Test_hostfxr_get_native_search_directories(args);
+                    break;
+                case nameof(hostfxr_resolve_sdk2):
+                    Test_hostfxr_resolve_sdk2(args);
+                    break;
+                case nameof(hostfxr_get_available_sdks):
+                    Test_hostfxr_get_available_sdks(args);
+                    break;
+                default:
+                    throw new ArgumentException($"Invalid API to test passed as args[0]): {apiToTest}");
             }
         }
 
@@ -45,17 +136,12 @@ namespace StandaloneApp
         {
             if (args.Length != 3)
             {
-                throw new Exception("Invalid number of arguments passed");
+                throw new ArgumentException("Invalid number of arguments passed");
             }
 
             string pathToDotnet = args[1];
             string pathToApp = args[2];
-
-            IntPtr[] argv = new IntPtr[2];
-            argv[0] = Marshal.StringToHGlobalUni(pathToDotnet);
-            argv[1] = Marshal.StringToHGlobalUni(pathToApp);
-
-            GCHandle gch = GCHandle.Alloc(argv, GCHandleType.Pinned);
+            string[] argv = new[] { pathToDotnet, pathToApp };
 
             // Start with 0 bytes allocated to test re-entry and required_buffer_size
             StringBuilder buffer = new StringBuilder(0);
@@ -64,7 +150,7 @@ namespace StandaloneApp
             uint rc = 0;
             for (int i = 0; i < 2; i++)
             {
-                rc = hostfxr_get_native_search_directories(argv.Length, gch.AddrOfPinnedObject(), buffer, buffer.Capacity + 1, ref required_buffer_size);
+                rc = hostfxr_get_native_search_directories(argv.Length, argv, buffer, buffer.Capacity + 1, ref required_buffer_size);
                 if (rc != HostApiBufferTooSmall)
                 {
                     break;
@@ -73,16 +159,71 @@ namespace StandaloneApp
                 buffer = new StringBuilder(required_buffer_size);
             }
 
-            gch.Free();
-            for (int i = 0; i < argv.Length; ++i)
+            if (rc == 0)
             {
-                Marshal.FreeHGlobal(argv[i]);
+                Console.WriteLine("hostfxr_get_native_search_directories:Success");
+                Console.WriteLine($"hostfxr_get_native_search_directories buffer:[{buffer}]");
             }
+            else
+            {
+                Console.WriteLine($"hostfxr_get_native_search_directories:Fail[{rc}]");
+            }
+        }
+
+        /// <summary>
+        /// Test invoking the native hostfxr api hostfxr_resolve_sdk2
+        /// </summary>
+        /// <param name="args[0]">hostfxr_get_available_sdks</param>
+        /// <param name="args[1]">Directory of dotnet executable</param>
+        /// <param name="args[2]">Working directory where search for global.json begins</param>
+        /// <param name="args[3]">Flags</param>
+        static void Test_hostfxr_resolve_sdk2(string[] args)
+        {
+            if (args.Length != 4)
+            {
+                throw new ArgumentException("Invalid number of arguments passed");
+            }
+
+            var data = new List<(hostfxr_resolve_sdk2_result_key_t, string)>();
+            int rc = hostfxr_resolve_sdk2(
+                exe_dir: args[1],
+                working_dir: args[2],
+                flags: Enum.Parse<hostfxr_resolve_sdk2_flags_t>(args[3]),
+                result: (key, value) => data.Add((key, value)));
 
             if (rc == 0)
             {
-                Console.WriteLine("hostfxr_get_native_search_directories:Success");
-                Console.WriteLine($"hostfxr_get_native_search_directories buffer:[{buffer}]");
+                Console.WriteLine("hostfxr_resolve_sdk2:Success");
+            }
+            else
+            {
+                Console.WriteLine($"hostfxr_resolve_sdk2:Fail[{rc}]");
+            }
+
+            Console.WriteLine($"hostfxr_resolve_sdk2 data:[{string.Join(';', data)}]");
+        }
+
+        /// <summary>
+        /// Test invoking the native hostfxr api hostfxr_get_available_sdks
+        /// </summary>
+        /// <param name="args[0]">hostfxr_get_available_sdks</param>
+        /// <param name="args[1]">Directory of dotnet executable</param>
+        static void Test_hostfxr_get_available_sdks(string[] args)
+        {
+            if (args.Length != 2)
+            {
+                throw new ArgumentException("Invalid number of arguments passed");
+            }
+
+            string[] sdks = null;
+            int rc = hostfxr_get_available_sdks(
+                exe_dir: args[1], 
+                (sdk_count, sdk_dirs) => sdks = sdk_dirs);
+
+            if (rc == 0)
+            {
+                Console.WriteLine("hostfxr_get_available_sdks:Success");
+                Console.WriteLine($"hostfxr_get_available_sdks sdks:[{string.Join(';', sdks)}]");
             }
             else
             {
index 8e9f966..ccea62f 100644 (file)
@@ -7,6 +7,8 @@ using System.Runtime.InteropServices;
 using Xunit;
 using FluentAssertions;
 using Microsoft.DotNet.CoreSetup.Test;
+using System.Collections.Generic;
+using Microsoft.DotNet.Cli.Build;
 
 namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.NativeHostApis
 {
@@ -22,13 +24,6 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.NativeHostApis
         [Fact]
         public void Muxer_activation_of_Publish_Output_Portable_DLL_hostfxr_get_native_search_directories_Succeeds()
         {
-            // Currently the native API is used only on Windows, although it has been manually tested on Unix.
-            // Limit OS here to avoid issues with DllImport not being able to find the shared library.
-            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
-            {
-                return;
-            }
-
             var fixture = sharedTestState.PreviouslyPublishedAndRestoredPortableApiTestProjectFixture.Copy();
             var dotnet = fixture.BuiltDotnet;
             var appDll = fixture.TestProject.AppDll;
@@ -43,6 +38,7 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.NativeHostApis
 
             dotnet.Exec(appDll, args)
                 .CaptureStdOut()
+                .CaptureStdErr()
                 .Execute()
                 .Should()
                 .Pass()
@@ -97,6 +93,181 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.NativeHostApis
                 .NotHaveStdErrContaining("Waiting for breadcrumb thread to exit...");
         }
 
+        private class SdkResolutionFixture
+        {
+            private readonly TestProjectFixture _fixture;
+
+            public DotNetCli Dotnet => _fixture.BuiltDotnet;
+            public string AppDll => _fixture.TestProject.AppDll;
+            public string ExeDir => Path.Combine(_fixture.TestProject.ProjectDirectory, "ed");
+            public string ProgramFiles => Path.Combine(ExeDir, "pf");
+            public string WorkingDir => Path.Combine(_fixture.TestProject.ProjectDirectory, "wd");
+            public string GlobalSdkDir => Path.Combine(ProgramFiles, "dotnet", "sdk");
+            public string LocalSdkDir => Path.Combine(ExeDir, "sdk");
+            public string GlobalJson => Path.Combine(WorkingDir, "global.json");
+            public string[] GlobalSdks = new[] { "4.5.6", "1.2.3", "2.3.4-preview" };
+            public string[] LocalSdks = new[] { "0.1.2", "5.6.7-preview", "1.2.3" };
+
+            public SdkResolutionFixture(SharedTestState state)
+            {
+                _fixture = state.PreviouslyPublishedAndRestoredPortableApiTestProjectFixture.Copy();
+
+                Directory.CreateDirectory(WorkingDir);
+
+                // start with an empty global.json, it will be ignored, but prevent one lying on disk 
+                // on a given machine from impacting the test.
+                File.WriteAllText(GlobalJson, "{}");
+
+                foreach (string sdk in GlobalSdks)
+                {
+                    Directory.CreateDirectory(Path.Combine(GlobalSdkDir, sdk));
+                }
+
+                foreach (string sdk in LocalSdks)
+                {
+                    Directory.CreateDirectory(Path.Combine(LocalSdkDir, sdk));
+                }
+            } 
+        }
+
+        [Fact]
+        public void Hostfxr_get_available_sdks_with_multilevel_lookup()
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 
+            {
+                // multilevel lookup is not supported on non-Windows
+                return;
+            }
+            
+            var f = new SdkResolutionFixture(sharedTestState);
+
+            // With multi-level lookup (windows onnly): get local and global sdks sorted by ascending version,
+            // with global sdk coming before local sdk when versions are equal
+            string expectedList = string.Join(';', new[]
+            {
+                Path.Combine(f.LocalSdkDir, "0.1.2"),
+                Path.Combine(f.GlobalSdkDir, "1.2.3"),
+                Path.Combine(f.LocalSdkDir, "1.2.3"),
+                Path.Combine(f.GlobalSdkDir, "2.3.4-preview"),
+                Path.Combine(f.GlobalSdkDir, "4.5.6"),
+                Path.Combine(f.LocalSdkDir, "5.6.7-preview"),
+            });
+
+            f.Dotnet.Exec(f.AppDll, new[] { "hostfxr_get_available_sdks", f.ExeDir })
+                .EnvironmentVariable("TEST_MULTILEVEL_LOOKUP_PROGRAM_FILES", f.ProgramFiles)
+                .CaptureStdOut()
+                .CaptureStdErr()
+                .Execute()
+                .Should()
+                .Pass()
+                .And
+                .HaveStdOutContaining("hostfxr_get_available_sdks:Success")
+                .And
+                .HaveStdOutContaining($"hostfxr_get_available_sdks sdks:[{expectedList}]");
+        }
+
+        [Fact]
+        public void Hostfxr_get_available_sdks_without_multilevel_lookup()
+        {
+            // Without multi-level lookup: get only sdks sorted by ascending version
+
+            var f = new SdkResolutionFixture(sharedTestState);
+
+            string expectedList = 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"),
+            });
+
+            f.Dotnet.Exec(f.AppDll, new[] { "hostfxr_get_available_sdks", f.ExeDir })
+                .CaptureStdOut()
+                .CaptureStdErr()
+                .Execute()
+                .Should()
+                .Pass()
+                .And
+                .HaveStdOutContaining("hostfxr_get_available_sdks:Success")
+                .And
+                .HaveStdOutContaining($"hostfxr_get_available_sdks sdks:[{expectedList}]");
+        }
+
+        [Fact]
+        public void Hostfxr_resolve_sdk2_without_global_json_or_flags()
+        {
+            // with no global.json and no flags, pick latest SDK
+
+            var f = new SdkResolutionFixture(sharedTestState);
+
+            string expectedData = string.Join(';', new[]
+            {
+                ("resolved_sdk_dir", Path.Combine(f.LocalSdkDir, "5.6.7-preview")),
+            });
+
+            f.Dotnet.Exec(f.AppDll, new[] { "hostfxr_resolve_sdk2", f.ExeDir, f.WorkingDir, "0" })
+                .CaptureStdOut()
+                .CaptureStdErr()
+                .Execute()
+                .Should()
+                .Pass()
+                .And
+                .HaveStdOutContaining("hostfxr_resolve_sdk2:Success")
+                .And
+                .HaveStdOutContaining($"hostfxr_resolve_sdk2 data:[{expectedData}]");
+        }
+
+        [Fact]
+        public void Hostfxr_resolve_sdk2_without_global_json_and_disallowing_previews()
+        {
+            // Without global.json and disallowing previews, pick latest non-preview
+
+            var f = new SdkResolutionFixture(sharedTestState);
+
+            string expectedData = string.Join(';', new[]
+            {
+                ("resolved_sdk_dir", Path.Combine(f.LocalSdkDir, "1.2.3"))
+            });
+
+            f.Dotnet.Exec(f.AppDll, new[] { "hostfxr_resolve_sdk2", f.ExeDir, f.WorkingDir, "disallow_prerelease" })
+                .CaptureStdOut()
+                .CaptureStdErr()
+                .Execute()
+                .Should()
+                .Pass()
+                .And
+                .HaveStdOutContaining("hostfxr_resolve_sdk2:Success")
+                .And
+                .HaveStdOutContaining($"hostfxr_resolve_sdk2 data:[{expectedData}]");
+        }
+
+        [Fact]
+        public void Hostfxr_resolve_sdk2_with_global_json_and_disallowing_previews()
+        {
+            // With global.json specifying a preview, roll forward to preview 
+            // since flag has no impact if global.json specifies a preview.
+            // Also check that global.json that impacted resolution is reported.
+
+            var f = new SdkResolutionFixture(sharedTestState);
+
+            File.WriteAllText(f.GlobalJson, "{ \"sdk\": { \"version\": \"5.6.6-preview\" } }");
+            string expectedData = string.Join(';', new[]
+            {
+                ("resolved_sdk_dir", Path.Combine(f.LocalSdkDir, "5.6.7-preview")),
+                ("global_json_path", f.GlobalJson),
+            });
+
+            f.Dotnet.Exec(f.AppDll, new[] { "hostfxr_resolve_sdk2", f.ExeDir, f.WorkingDir, "disallow_prerelease" })
+                .CaptureStdOut()
+                .CaptureStdErr()
+                .Execute()
+                .Should()
+                .Pass()
+                .And
+                .HaveStdOutContaining("hostfxr_resolve_sdk2:Success")
+                .And
+                .HaveStdOutContaining($"hostfxr_resolve_sdk2 data:[{expectedData}]");
+        }
+
         public class SharedTestState : IDisposable
         {
             public TestProjectFixture PreviouslyPublishedAndRestoredPortableApiTestProjectFixture { get; set; }
@@ -129,6 +300,16 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.NativeHostApis
                         "opt",
                         "corebreadcrumbs");
                     Directory.CreateDirectory(BreadcrumbLocation);
+
+                    // On non-Windows, we can't just P/Invoke to already loaded hostfxr, so copy it next to the app dll.
+                    var fixture = PreviouslyPublishedAndRestoredPortableApiTestProjectFixture;
+                    var hostfxr = Path.Combine(
+                        fixture.BuiltDotnet.GreatestVersionHostFxrPath, 
+                        $"{fixture.SharedLibraryPrefix}hostfxr{fixture.SharedLibraryExtension}");
+
+                    File.Copy(
+                        hostfxr, 
+                        Path.GetDirectoryName(fixture.TestProject.AppDll));
                 }
             }