From 63a96c59ace83b339df0b3030ff77e73ac0401e1 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Wed, 27 Mar 2024 17:13:22 -0700 Subject: [PATCH 1/8] Store resolution failure information --- src/native/corehost/fxr/fx_resolver.cpp | 62 ++++++++++++++++--------- src/native/corehost/fxr/fx_resolver.h | 19 ++++++-- 2 files changed, 56 insertions(+), 25 deletions(-) diff --git a/src/native/corehost/fxr/fx_resolver.cpp b/src/native/corehost/fxr/fx_resolver.cpp index 1340af625423c..3b285fc870af1 100644 --- a/src/native/corehost/fxr/fx_resolver.cpp +++ b/src/native/corehost/fxr/fx_resolver.cpp @@ -184,7 +184,7 @@ namespace return best_match; } - fx_definition_t* resolve_framework_reference( + std::unique_ptr resolve_framework_reference( const fx_reference_t & fx_ref, const pal::string_t & oldest_requested_version, const pal::string_t & dotnet_dir, @@ -303,7 +303,7 @@ namespace trace::verbose(_X("Chose FX version [%s]"), selected_fx_dir.c_str()); - return new fx_definition_t(fx_ref.get_fx_name(), selected_fx_dir, oldest_requested_version, selected_fx_version); + return std::unique_ptr(new fx_definition_t(fx_ref.get_fx_name(), selected_fx_dir, oldest_requested_version, selected_fx_version)); } } @@ -320,7 +320,8 @@ namespace StatusCode fx_resolver_t::reconcile_fx_references( const fx_reference_t& fx_ref_a, const fx_reference_t& fx_ref_b, - /*out*/ fx_reference_t& effective_fx_ref) + /*out*/ fx_reference_t& effective_fx_ref, + resolution_failure_info& resolution_failure) { // Determine which framework reference is higher to do the compat check. The various tracing messages // make more sense if they're always written with higher/lower versions ordered in particular way. @@ -331,7 +332,8 @@ StatusCode fx_resolver_t::reconcile_fx_references( if (!lower_fx_ref.is_compatible_with_higher_version(higher_fx_ref.get_fx_version_number())) { // Error condition - not compatible with the other reference - display_incompatible_framework_error(higher_fx_ref.get_fx_version(), lower_fx_ref); + resolution_failure.incompatible_higher = higher_fx_ref; + resolution_failure.incompatible_lower = lower_fx_ref; return StatusCode::FrameworkCompatFailure; } @@ -399,7 +401,7 @@ StatusCode fx_resolver_t::read_framework( const runtime_config_t & config, const fx_reference_t * effective_parent_fx_ref, fx_definition_vector_t & fx_definitions, - const pal::char_t* app_display_name) + resolution_failure_info& resolution_failure) { // This reconciles duplicate references to minimize the number of resolve retries. update_newest_references(config); @@ -423,7 +425,7 @@ StatusCode fx_resolver_t::read_framework( // Reconcile the framework reference with the most up to date so far we have for the framework. // This does not read any physical framework folders yet. - rc = reconcile_fx_references(fx_ref, current_effective_fx_ref, new_effective_fx_ref); + rc = reconcile_fx_references(fx_ref, current_effective_fx_ref, new_effective_fx_ref, resolution_failure); if (rc != StatusCode::Success) return rc; @@ -438,17 +440,10 @@ StatusCode fx_resolver_t::read_framework( m_effective_fx_references[fx_name] = new_effective_fx_ref; // Resolve the effective framework reference against the existing physical framework folders - fx_definition_t* fx = resolve_framework_reference(new_effective_fx_ref, m_oldest_fx_references[fx_name].get_fx_version(), host_info.dotnet_root, disable_multilevel_lookup); + std::unique_ptr fx = resolve_framework_reference(new_effective_fx_ref, m_oldest_fx_references[fx_name].get_fx_version(), host_info.dotnet_root, disable_multilevel_lookup); if (fx == nullptr) { - trace::error( - INSTALL_OR_UPDATE_NET_ERROR_MESSAGE - _X("\n\n") - _X("App: %s\n") - _X("Architecture: %s"), - app_display_name != nullptr ? app_display_name : host_info.host_path.c_str(), - get_current_arch_name()); - display_missing_framework_error(fx_name, new_effective_fx_ref.get_fx_version(), pal::string_t(), host_info.dotnet_root, disable_multilevel_lookup); + resolution_failure.missing = std::move(new_effective_fx_ref); return StatusCode::FrameworkMissingFailure; } @@ -462,8 +457,6 @@ StatusCode fx_resolver_t::read_framework( // will change the effective reference from "2.1.0 LatestMajor" to "2.1.0 Minor" and restart the framework resolution process. // So during the second run we will resolve for example "2.2.0" which will be compatible with both framework references. - fx_definitions.push_back(std::unique_ptr(fx)); - // Recursively process the base frameworks pal::string_t config_file; pal::string_t dev_config_file; @@ -473,11 +466,12 @@ StatusCode fx_resolver_t::read_framework( runtime_config_t new_config = fx->get_runtime_config(); if (!new_config.is_valid()) { - trace::error(_X("Invalid framework config.json [%s]"), new_config.get_path().c_str()); + resolution_failure.invalid_config = std::move(fx); return StatusCode::InvalidConfigFile; } - rc = read_framework(host_info, disable_multilevel_lookup, override_settings, new_config, &new_effective_fx_ref, fx_definitions, app_display_name); + fx_definitions.push_back(std::move(fx)); + rc = read_framework(host_info, disable_multilevel_lookup, override_settings, new_config, &new_effective_fx_ref, fx_definitions, resolution_failure); if (rc != StatusCode::Success) return rc; } @@ -507,17 +501,19 @@ StatusCode fx_resolver_t::resolve_frameworks_for_app( const runtime_config_t::settings_t& override_settings, const runtime_config_t & app_config, fx_definition_vector_t & fx_definitions, - const pal::char_t* app_display_name) + const pal::char_t* app_display_name, + bool print_errors) { fx_resolver_t resolver; // Read the shared frameworks; retry is necessary when a framework is already resolved, but then a newer compatible version is processed. + resolution_failure_info resolution_failure; StatusCode rc = StatusCode::Success; int retry_count = 0; do { fx_definitions.resize(1); // Erase any existing frameworks for re-try - rc = resolver.read_framework(host_info, disable_multilevel_lookup, override_settings, app_config, /*effective_parent_fx_ref*/ nullptr, fx_definitions, app_display_name); + rc = resolver.read_framework(host_info, disable_multilevel_lookup, override_settings, app_config, /*effective_parent_fx_ref*/ nullptr, fx_definitions, resolution_failure); } while (rc == StatusCode::FrameworkCompatRetry && retry_count++ < Max_Framework_Resolve_Retries); assert(retry_count < Max_Framework_Resolve_Retries); @@ -526,6 +522,30 @@ StatusCode fx_resolver_t::resolve_frameworks_for_app( { display_summary_of_frameworks(fx_definitions, resolver.m_effective_fx_references); } + else if (print_errors) + { + switch (rc) + { + case StatusCode::FrameworkMissingFailure: + trace::error( + INSTALL_OR_UPDATE_NET_ERROR_MESSAGE + _X("\n\n") + _X("App: %s\n") + _X("Architecture: %s"), + app_display_name != nullptr ? app_display_name : host_info.host_path.c_str(), + get_current_arch_name()); + display_missing_framework_error(resolution_failure.missing.get_fx_name(), resolution_failure.missing.get_fx_version(), pal::string_t(), host_info.dotnet_root, disable_multilevel_lookup); + break; + case StatusCode::FrameworkCompatFailure: + display_incompatible_framework_error(resolution_failure.incompatible_higher.get_fx_version(), resolution_failure.incompatible_lower); + break; + case StatusCode::InvalidConfigFile: + trace::error(_X("Invalid framework config.json [%s]"), resolution_failure.invalid_config->get_runtime_config().get_path().c_str()); + break; + default: + break; + } + } return rc; } diff --git a/src/native/corehost/fxr/fx_resolver.h b/src/native/corehost/fxr/fx_resolver.h index 018294148f30d..0b06c40474ddd 100644 --- a/src/native/corehost/fxr/fx_resolver.h +++ b/src/native/corehost/fxr/fx_resolver.h @@ -13,14 +13,24 @@ struct host_startup_info_t; class fx_resolver_t { +public: + struct resolution_failure_info + { + fx_reference_t missing; + fx_reference_t incompatible_lower; + fx_reference_t incompatible_higher; + std::unique_ptr invalid_config; + }; + public: static StatusCode resolve_frameworks_for_app( const host_startup_info_t& host_info, bool disable_multilevel_lookup, const runtime_config_t::settings_t& override_settings, const runtime_config_t& app_config, - fx_definition_vector_t& fx_definitions, - const pal::char_t* app_display_name = nullptr); + /*in_out*/ fx_definition_vector_t& fx_definitions, + const pal::char_t* app_display_name = nullptr, + bool print_errors = true); static bool is_config_compatible_with_frameworks( const runtime_config_t& config, @@ -38,12 +48,13 @@ class fx_resolver_t const runtime_config_t& config, const fx_reference_t * effective_parent_fx_ref, fx_definition_vector_t& fx_definitions, - const pal::char_t* app_display_name); + resolution_failure_info& resolution_failure); static StatusCode reconcile_fx_references( const fx_reference_t& fx_ref_a, const fx_reference_t& fx_ref_b, - /*out*/ fx_reference_t& effective_fx_ref); + /*out*/ fx_reference_t& effective_fx_ref, + resolution_failure_info& resolution_failure); static void display_missing_framework_error( const pal::string_t& fx_name, From 9907366b9d03d27a8c99047e8de507d8dd80a8b3 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Wed, 27 Mar 2024 17:38:02 -0700 Subject: [PATCH 2/8] Store some parameters as members on fx_resolver_t --- src/native/corehost/fxr/fx_muxer.cpp | 4 ++-- src/native/corehost/fxr/fx_resolver.cpp | 32 ++++++++++++------------- src/native/corehost/fxr/fx_resolver.h | 17 +++++++------ 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/native/corehost/fxr/fx_muxer.cpp b/src/native/corehost/fxr/fx_muxer.cpp index cbaf90aa69cba..02c80c590948a 100644 --- a/src/native/corehost/fxr/fx_muxer.cpp +++ b/src/native/corehost/fxr/fx_muxer.cpp @@ -465,7 +465,7 @@ namespace } else { - rc = fx_resolver_t::resolve_frameworks_for_app(host_info, app_config.get_is_multilevel_lookup_disabled(), override_settings, app_config, fx_definitions, mode == host_mode_t::muxer ? app_candidate.c_str() : nullptr); + rc = fx_resolver_t::resolve_frameworks_for_app(host_info.dotnet_root, override_settings, app_config, fx_definitions, mode == host_mode_t::muxer ? app_candidate.c_str() : host_info.host_path.c_str()); if (rc != StatusCode::Success) { return rc; @@ -619,7 +619,7 @@ namespace return StatusCode::InvalidConfigFile; } - rc = fx_resolver_t::resolve_frameworks_for_app(host_info, app_config.get_is_multilevel_lookup_disabled(), override_settings, app_config, fx_definitions); + rc = fx_resolver_t::resolve_frameworks_for_app(host_info.dotnet_root, override_settings, app_config, fx_definitions, host_info.host_path.c_str()); if (rc != StatusCode::Success) return rc; diff --git a/src/native/corehost/fxr/fx_resolver.cpp b/src/native/corehost/fxr/fx_resolver.cpp index 3b285fc870af1..4f0f70107b838 100644 --- a/src/native/corehost/fxr/fx_resolver.cpp +++ b/src/native/corehost/fxr/fx_resolver.cpp @@ -395,12 +395,10 @@ void fx_resolver_t::update_newest_references( // FrameworkMissingFailure - the resolution failed because the requested framework doesn't exist on disk. // InvalidConfigFile - reading of a runtime config for some of the processed frameworks has failed. StatusCode fx_resolver_t::read_framework( - const host_startup_info_t & host_info, - bool disable_multilevel_lookup, - const runtime_config_t::settings_t& override_settings, - const runtime_config_t & config, - const fx_reference_t * effective_parent_fx_ref, - fx_definition_vector_t & fx_definitions, + const pal::string_t& dotnet_root, + const runtime_config_t& config, + const fx_reference_t* effective_parent_fx_ref, + fx_definition_vector_t& fx_definitions, resolution_failure_info& resolution_failure) { // This reconciles duplicate references to minimize the number of resolve retries. @@ -440,7 +438,7 @@ StatusCode fx_resolver_t::read_framework( m_effective_fx_references[fx_name] = new_effective_fx_ref; // Resolve the effective framework reference against the existing physical framework folders - std::unique_ptr fx = resolve_framework_reference(new_effective_fx_ref, m_oldest_fx_references[fx_name].get_fx_version(), host_info.dotnet_root, disable_multilevel_lookup); + std::unique_ptr fx = resolve_framework_reference(new_effective_fx_ref, m_oldest_fx_references[fx_name].get_fx_version(), dotnet_root, m_disable_multilevel_lookup); if (fx == nullptr) { resolution_failure.missing = std::move(new_effective_fx_ref); @@ -461,7 +459,7 @@ StatusCode fx_resolver_t::read_framework( pal::string_t config_file; pal::string_t dev_config_file; get_runtime_config_paths(fx->get_dir(), fx_name, &config_file, &dev_config_file); - fx->parse_runtime_config(config_file, dev_config_file, override_settings); + fx->parse_runtime_config(config_file, dev_config_file, m_override_settings); runtime_config_t new_config = fx->get_runtime_config(); if (!new_config.is_valid()) @@ -471,7 +469,7 @@ StatusCode fx_resolver_t::read_framework( } fx_definitions.push_back(std::move(fx)); - rc = read_framework(host_info, disable_multilevel_lookup, override_settings, new_config, &new_effective_fx_ref, fx_definitions, resolution_failure); + rc = read_framework(dotnet_root, new_config, &new_effective_fx_ref, fx_definitions, resolution_failure); if (rc != StatusCode::Success) return rc; } @@ -496,15 +494,15 @@ StatusCode fx_resolver_t::read_framework( } StatusCode fx_resolver_t::resolve_frameworks_for_app( - const host_startup_info_t & host_info, - bool disable_multilevel_lookup, + const pal::string_t& dotnet_root, const runtime_config_t::settings_t& override_settings, - const runtime_config_t & app_config, - fx_definition_vector_t & fx_definitions, + const runtime_config_t& app_config, + fx_definition_vector_t& fx_definitions, const pal::char_t* app_display_name, bool print_errors) { - fx_resolver_t resolver; + bool disable_multilevel_lookup = app_config.get_is_multilevel_lookup_disabled(); + fx_resolver_t resolver{ disable_multilevel_lookup, override_settings }; // Read the shared frameworks; retry is necessary when a framework is already resolved, but then a newer compatible version is processed. resolution_failure_info resolution_failure; @@ -513,7 +511,7 @@ StatusCode fx_resolver_t::resolve_frameworks_for_app( do { fx_definitions.resize(1); // Erase any existing frameworks for re-try - rc = resolver.read_framework(host_info, disable_multilevel_lookup, override_settings, app_config, /*effective_parent_fx_ref*/ nullptr, fx_definitions, resolution_failure); + rc = resolver.read_framework(dotnet_root, app_config, /*effective_parent_fx_ref*/ nullptr, fx_definitions, resolution_failure); } while (rc == StatusCode::FrameworkCompatRetry && retry_count++ < Max_Framework_Resolve_Retries); assert(retry_count < Max_Framework_Resolve_Retries); @@ -532,9 +530,9 @@ StatusCode fx_resolver_t::resolve_frameworks_for_app( _X("\n\n") _X("App: %s\n") _X("Architecture: %s"), - app_display_name != nullptr ? app_display_name : host_info.host_path.c_str(), + app_display_name, get_current_arch_name()); - display_missing_framework_error(resolution_failure.missing.get_fx_name(), resolution_failure.missing.get_fx_version(), pal::string_t(), host_info.dotnet_root, disable_multilevel_lookup); + display_missing_framework_error(resolution_failure.missing.get_fx_name(), resolution_failure.missing.get_fx_version(), pal::string_t(), dotnet_root, disable_multilevel_lookup); break; case StatusCode::FrameworkCompatFailure: display_incompatible_framework_error(resolution_failure.incompatible_higher.get_fx_version(), resolution_failure.incompatible_lower); diff --git a/src/native/corehost/fxr/fx_resolver.h b/src/native/corehost/fxr/fx_resolver.h index 0b06c40474ddd..4a6d4d9eb0357 100644 --- a/src/native/corehost/fxr/fx_resolver.h +++ b/src/native/corehost/fxr/fx_resolver.h @@ -24,12 +24,11 @@ class fx_resolver_t public: static StatusCode resolve_frameworks_for_app( - const host_startup_info_t& host_info, - bool disable_multilevel_lookup, + const pal::string_t& dotnet_root, const runtime_config_t::settings_t& override_settings, const runtime_config_t& app_config, /*in_out*/ fx_definition_vector_t& fx_definitions, - const pal::char_t* app_display_name = nullptr, + const pal::char_t* app_display_name, bool print_errors = true); static bool is_config_compatible_with_frameworks( @@ -37,14 +36,15 @@ class fx_resolver_t const std::unordered_map &existing_framework_versions_by_name); private: - fx_resolver_t() = default; + fx_resolver_t(bool disable_multilevel_lookup, const runtime_config_t::settings_t& override_settings) + : m_disable_multilevel_lookup{disable_multilevel_lookup} + , m_override_settings{override_settings} + { } void update_newest_references( const runtime_config_t& config); StatusCode read_framework( - const host_startup_info_t& host_info, - bool disable_multilevel_lookup, - const runtime_config_t::settings_t& override_settings, + const pal::string_t& dotnet_root, const runtime_config_t& config, const fx_reference_t * effective_parent_fx_ref, fx_definition_vector_t& fx_definitions, @@ -90,6 +90,9 @@ class fx_resolver_t // to fill the "oldest reference" for each resolved framework in the end. It does not affect the behavior // of the algorithm. fx_name_to_fx_reference_map_t m_oldest_fx_references; + + bool m_disable_multilevel_lookup; + const runtime_config_t::settings_t& m_override_settings; }; #endif // __FX_RESOLVER_H__ From 642b8c35c451dac273e76628a6cf45b74d2f7ce2 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 12 Apr 2024 15:43:33 -0700 Subject: [PATCH 3/8] Add hostfxr_resolve_frameworks_for_runtime_config API --- src/native/corehost/fxr/fx_resolver.cpp | 63 +++++---- src/native/corehost/fxr/fx_resolver.h | 11 +- src/native/corehost/fxr/hostfxr.cpp | 121 ++++++++++++++++++ .../corehost/fxr/standalone/hostfxr.def | 1 + .../fxr/standalone/hostfxr_unixexports.src | 1 + src/native/corehost/hostfxr.h | 52 ++++++++ 6 files changed, 218 insertions(+), 31 deletions(-) diff --git a/src/native/corehost/fxr/fx_resolver.cpp b/src/native/corehost/fxr/fx_resolver.cpp index 4f0f70107b838..e78bd63eca109 100644 --- a/src/native/corehost/fxr/fx_resolver.cpp +++ b/src/native/corehost/fxr/fx_resolver.cpp @@ -493,19 +493,16 @@ StatusCode fx_resolver_t::read_framework( return StatusCode::Success; } -StatusCode fx_resolver_t::resolve_frameworks_for_app( +StatusCode fx_resolver_t::resolve_frameworks( const pal::string_t& dotnet_root, const runtime_config_t::settings_t& override_settings, const runtime_config_t& app_config, fx_definition_vector_t& fx_definitions, - const pal::char_t* app_display_name, - bool print_errors) + resolution_failure_info& resolution_failure) { - bool disable_multilevel_lookup = app_config.get_is_multilevel_lookup_disabled(); - fx_resolver_t resolver{ disable_multilevel_lookup, override_settings }; + fx_resolver_t resolver{ app_config.get_is_multilevel_lookup_disabled(), override_settings }; // Read the shared frameworks; retry is necessary when a framework is already resolved, but then a newer compatible version is processed. - resolution_failure_info resolution_failure; StatusCode rc = StatusCode::Success; int retry_count = 0; do @@ -520,29 +517,39 @@ StatusCode fx_resolver_t::resolve_frameworks_for_app( { display_summary_of_frameworks(fx_definitions, resolver.m_effective_fx_references); } - else if (print_errors) + + return rc; +} + +StatusCode fx_resolver_t::resolve_frameworks_for_app( + const pal::string_t& dotnet_root, + const runtime_config_t::settings_t& override_settings, + const runtime_config_t& app_config, + fx_definition_vector_t& fx_definitions, + const pal::char_t* app_display_name) +{ + resolution_failure_info resolution_failure; + StatusCode rc = resolve_frameworks(dotnet_root, override_settings, app_config, fx_definitions, resolution_failure); + switch (rc) { - switch (rc) - { - case StatusCode::FrameworkMissingFailure: - trace::error( - INSTALL_OR_UPDATE_NET_ERROR_MESSAGE - _X("\n\n") - _X("App: %s\n") - _X("Architecture: %s"), - app_display_name, - get_current_arch_name()); - display_missing_framework_error(resolution_failure.missing.get_fx_name(), resolution_failure.missing.get_fx_version(), pal::string_t(), dotnet_root, disable_multilevel_lookup); - break; - case StatusCode::FrameworkCompatFailure: - display_incompatible_framework_error(resolution_failure.incompatible_higher.get_fx_version(), resolution_failure.incompatible_lower); - break; - case StatusCode::InvalidConfigFile: - trace::error(_X("Invalid framework config.json [%s]"), resolution_failure.invalid_config->get_runtime_config().get_path().c_str()); - break; - default: - break; - } + case StatusCode::FrameworkMissingFailure: + trace::error( + INSTALL_OR_UPDATE_NET_ERROR_MESSAGE + _X("\n\n") + _X("App: %s\n") + _X("Architecture: %s"), + app_display_name, + get_current_arch_name()); + display_missing_framework_error(resolution_failure.missing.get_fx_name(), resolution_failure.missing.get_fx_version(), pal::string_t(), dotnet_root, app_config.get_is_multilevel_lookup_disabled()); + break; + case StatusCode::FrameworkCompatFailure: + display_incompatible_framework_error(resolution_failure.incompatible_higher.get_fx_version(), resolution_failure.incompatible_lower); + break; + case StatusCode::InvalidConfigFile: + trace::error(_X("Invalid framework config.json [%s]"), resolution_failure.invalid_config->get_runtime_config().get_path().c_str()); + break; + default: + break; } return rc; diff --git a/src/native/corehost/fxr/fx_resolver.h b/src/native/corehost/fxr/fx_resolver.h index 4a6d4d9eb0357..466decd846fc9 100644 --- a/src/native/corehost/fxr/fx_resolver.h +++ b/src/native/corehost/fxr/fx_resolver.h @@ -9,7 +9,6 @@ #include "fx_definition.h" class runtime_config_t; -struct host_startup_info_t; class fx_resolver_t { @@ -23,13 +22,19 @@ class fx_resolver_t }; public: + static StatusCode resolve_frameworks( + const pal::string_t& dotnet_root, + const runtime_config_t::settings_t& override_settings, + const runtime_config_t& app_config, + /*in_out*/ fx_definition_vector_t& fx_definitions, + resolution_failure_info& resolution_failure); + static StatusCode resolve_frameworks_for_app( const pal::string_t& dotnet_root, const runtime_config_t::settings_t& override_settings, const runtime_config_t& app_config, /*in_out*/ fx_definition_vector_t& fx_definitions, - const pal::char_t* app_display_name, - bool print_errors = true); + const pal::char_t* app_display_name); static bool is_config_compatible_with_frameworks( const runtime_config_t& config, diff --git a/src/native/corehost/fxr/hostfxr.cpp b/src/native/corehost/fxr/hostfxr.cpp index a386cbe376216..d595e03d93ea9 100644 --- a/src/native/corehost/fxr/hostfxr.cpp +++ b/src/native/corehost/fxr/hostfxr.cpp @@ -7,6 +7,7 @@ #include "utils.h" #include "fx_ver.h" #include "fx_muxer.h" +#include "fx_resolver.h" #include "error_codes.h" #include "runtime_config.h" #include "sdk_info.h" @@ -571,6 +572,126 @@ namespace } } +SHARED_API int32_t HOSTFXR_CALLTYPE hostfxr_resolve_frameworks_for_runtime_config( + const char_t* runtime_config_path, + /*opt*/ const hostfxr_initialize_parameters* parameters, + /*opt*/ hostfxr_resolve_frameworks_result_fn callback, + /*opt*/ void* result_context) +{ + trace_hostfxr_entry_point(_X("hostfxr_resolve_frameworks_for_runtime_config")); + if (trace::is_enabled()) + { + trace::info(_X(" runtime_config_path=%s"), runtime_config_path == nullptr ? _X("") : runtime_config_path); + if (parameters == nullptr) + { + trace::info(_X(" parameters=")); + } + else + { + trace::info( + _X(" parameters={") + _X(" host_path=%s\n") + _X(" dotnet_root=%s\n") + _X(" }"), + parameters->host_path == nullptr ? _X("") : parameters->host_path, + parameters->dotnet_root == nullptr ? _X("") : parameters->dotnet_root); + } + } + + if (runtime_config_path == nullptr) + { + trace::error(_X("hostfxr_resolve_frameworks_for_runtime_config received an invalid argument: runtime_config_path should not be null.")); + return StatusCode::InvalidArgFailure; + } + + pal::string_t runtime_config = runtime_config_path; + if (runtime_config.empty() || !pal::realpath(&runtime_config)) + { + trace::error(_X("The specified runtimeconfig.json [%s] does not exist"), runtime_config.c_str()); + return StatusCode::InvalidConfigFile; + } + + host_startup_info_t host_info{}; + int rc = populate_startup_info(parameters, host_info); + if (rc != StatusCode::Success) + return rc; + + fx_definition_vector_t fx_definitions; + auto app = new fx_definition_t(); + fx_definitions.push_back(std::unique_ptr(app)); + + const runtime_config_t::settings_t override_settings; + app->parse_runtime_config(runtime_config, _X(""), override_settings); + + const runtime_config_t app_config = app->get_runtime_config(); + + // Resolve frameworks for framework-dependent apps. + // Self-contained apps assume the framework is next to the app, so we just treat it as success. + fx_resolver_t::resolution_failure_info failure_info; + rc = app_config.get_is_framework_dependent() + ? fx_resolver_t::resolve_frameworks(host_info.dotnet_root, override_settings, app_config, fx_definitions, failure_info) + : StatusCode::Success; + + if (callback) + { + std::vector resolved; + std::vector unresolved; + + pal::string_t config_dir; + if (app_config.get_is_framework_dependent()) + { + for (const auto& fx : fx_definitions) + { + // Skip the app itself + if (fx.get() == app) + continue; + + resolved.push_back({ sizeof(hostfxr_framework_result), fx->get_name().c_str(), fx->get_requested_version().c_str(), fx->get_found_version().c_str(), fx->get_dir().c_str() }); + } + + switch ((StatusCode)rc) + { + case StatusCode::FrameworkMissingFailure: + unresolved.push_back({ sizeof(hostfxr_framework_result), failure_info.missing.get_fx_name().c_str(), failure_info.missing.get_fx_version().c_str(), nullptr, nullptr }); + break; + case StatusCode::FrameworkCompatFailure: + unresolved.push_back({ sizeof(hostfxr_framework_result), failure_info.incompatible_lower.get_fx_name().c_str(), failure_info.incompatible_lower.get_fx_version().c_str(), nullptr, nullptr }); + unresolved.push_back({ sizeof(hostfxr_framework_result), failure_info.incompatible_higher.get_fx_name().c_str(), failure_info.incompatible_higher.get_fx_version().c_str(), nullptr, nullptr }); + break; + case StatusCode::InvalidConfigFile: + assert(failure_info.invalid_config != nullptr); + unresolved.push_back({ sizeof(hostfxr_framework_result), failure_info.invalid_config->get_name().c_str(), failure_info.invalid_config->get_requested_version().c_str(), failure_info.invalid_config->get_found_version().c_str(), failure_info.invalid_config->get_dir().c_str() }); + break; + default: + break; + } + } + else + { + // For self-contained apps, add all the included frameworks as resolved frameworks assumed to be next to the config + config_dir = get_directory(runtime_config); + remove_trailing_dir_separator(&config_dir); + for (const fx_reference_t& fx : app_config.get_included_frameworks()) + { + resolved.push_back({ sizeof(hostfxr_framework_result), fx.get_fx_name().c_str(), fx.get_fx_version().c_str(), fx.get_fx_version().c_str(), config_dir.c_str() }); + } + } + + const hostfxr_resolve_frameworks_result result + { + sizeof(hostfxr_resolve_frameworks_result), + resolved.size(), + resolved.empty() ? nullptr : resolved.data(), + unresolved.size(), + unresolved.empty() ? nullptr : unresolved.data() + }; + + callback(&result, result_context); + } + + return rc; +} + SHARED_API int32_t HOSTFXR_CALLTYPE hostfxr_initialize_for_dotnet_command_line( int argc, const pal::char_t *argv[], diff --git a/src/native/corehost/fxr/standalone/hostfxr.def b/src/native/corehost/fxr/standalone/hostfxr.def index 3488a4f21f270..6ce3efc37371a 100644 --- a/src/native/corehost/fxr/standalone/hostfxr.def +++ b/src/native/corehost/fxr/standalone/hostfxr.def @@ -13,6 +13,7 @@ EXPORTS hostfxr_set_error_writer hostfxr_initialize_for_dotnet_command_line hostfxr_initialize_for_runtime_config + hostfxr_resolve_frameworks_for_runtime_config hostfxr_run_app hostfxr_get_runtime_delegate hostfxr_get_runtime_property_value diff --git a/src/native/corehost/fxr/standalone/hostfxr_unixexports.src b/src/native/corehost/fxr/standalone/hostfxr_unixexports.src index 1780b0157eb36..c5f9425e69c39 100644 --- a/src/native/corehost/fxr/standalone/hostfxr_unixexports.src +++ b/src/native/corehost/fxr/standalone/hostfxr_unixexports.src @@ -12,6 +12,7 @@ hostfxr_get_native_search_directories hostfxr_set_error_writer hostfxr_initialize_for_dotnet_command_line hostfxr_initialize_for_runtime_config +hostfxr_resolve_frameworks_for_runtime_config hostfxr_run_app hostfxr_get_runtime_delegate hostfxr_get_runtime_property_value diff --git a/src/native/corehost/hostfxr.h b/src/native/corehost/hostfxr.h index a19636b9e3fd0..0562113325c18 100644 --- a/src/native/corehost/hostfxr.h +++ b/src/native/corehost/hostfxr.h @@ -363,4 +363,56 @@ typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_get_dotnet_environment_info_fn)( hostfxr_get_dotnet_environment_info_result_fn result, void* result_context); +struct hostfxr_framework_result +{ + size_t size; + const char_t* name; + const char_t* requested_version; + const char_t* resolved_version; + const char_t* resolved_path; +}; + +struct hostfxr_resolve_frameworks_result +{ + size_t size; + + size_t resolved_count; + const struct hostfxr_framework_result* resolved_frameworks; + + size_t unresolved_count; + const struct hostfxr_framework_result* unresolved_frameworks; +}; + +typedef void (HOSTFXR_CALLTYPE* hostfxr_resolve_frameworks_result_fn)( + const hostfxr_resolve_frameworks_result* result, + void* result_context); + +// +// Resolves frameworks for a runtime config +// +// Parameters: +// runtime_config_path +// Path to the .runtimeconfig.json file +// parameters +// Optional. Additional parameters for initialization. +// If null or dotnet_root is null, the root corresponding to the running hostfx is used. +// callback +// Optional. Result callback invoked with result of the resolution. +// Structs and their elements are valid for the duration of the call. +// result_context +// Optional. 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) +// +typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_resolve_frameworks_for_runtime_config_fn)( + const char_t* runtime_config_path, + /*opt*/ const hostfxr_initialize_parameters* parameters, + /*opt*/ hostfxr_resolve_frameworks_result_fn callback, + /*opt*/ void* result_context); + #endif //__HOSTFXR_H__ From 6346f93a1af354c0419e8d8f6e860aff90d5cf1b Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 19 Apr 2024 15:42:21 -0700 Subject: [PATCH 4/8] Add tests --- .../Projects/HostApiInvokerApp/HostFXR.cs | 124 ++++++- .../HostActivation.Tests/NativeHostApis.cs | 340 +++++++++++++++++- src/installer/tests/TestUtils/Constants.cs | 2 + 3 files changed, 458 insertions(+), 8 deletions(-) diff --git a/src/installer/tests/Assets/Projects/HostApiInvokerApp/HostFXR.cs b/src/installer/tests/Assets/Projects/HostApiInvokerApp/HostFXR.cs index 3dc91d5ea69f7..1a67e91e1d8c7 100644 --- a/src/installer/tests/Assets/Projects/HostApiInvokerApp/HostFXR.cs +++ b/src/installer/tests/Assets/Projects/HostApiInvokerApp/HostFXR.cs @@ -8,7 +8,7 @@ namespace HostApiInvokerApp { - public static class HostFXR + public static unsafe class HostFXR { internal static class hostfxr { @@ -59,6 +59,34 @@ internal struct hostfxr_dotnet_environment_info internal IntPtr frameworks; } + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + internal struct hostfxr_framework_result + { + public nuint size; + public string name; + public string requested_version; + public string resolved_version; + public string resolved_path; + }; + + [StructLayout(LayoutKind.Sequential)] + internal struct hostfxr_resolve_frameworks_result + { + public nuint size; + public nuint resolved_count; + public IntPtr resolved_frameworks; + public nuint unresolved_count; + public IntPtr unresolved_frameworks; + }; + + [StructLayout(LayoutKind.Sequential)] + internal struct hostfxr_initialize_parameters + { + public nuint size; + public IntPtr host_path; + public IntPtr dotnet_root; + } + [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Auto)] internal delegate void hostfxr_resolve_sdk2_result_fn( hostfxr_resolve_sdk2_result_key_t key, @@ -101,6 +129,18 @@ internal static extern int hostfxr_get_dotnet_environment_info( IntPtr reserved, hostfxr_get_dotnet_environment_info_result_fn result, IntPtr result_context); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void hostfxr_resolve_frameworks_result_fn( + IntPtr result, + IntPtr result_context); + + [DllImport(nameof(hostfxr), CharSet = CharSet.Auto, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + internal static extern int hostfxr_resolve_frameworks_for_runtime_config( + string runtime_config_path, + hostfxr_initialize_parameters* parameters, + hostfxr_resolve_frameworks_result_fn callback, + IntPtr result_context); } /// @@ -250,6 +290,85 @@ static void Test_hostfxr_get_dotnet_environment_info(string[] args) Console.WriteLine($"{api} framework paths:[{string.Join(";", frameworks.Select(f => f.path).ToList())}]"); } + /// + /// Test that invokes hostfxr_resolve_frameworks_for_runtime_config. + /// + /// Path to runtime config file + /// (Optional) Path to the directory with dotnet.exe + static unsafe void Test_hostfxr_resolve_frameworks_for_runtime_config(string[] args) + { + if (args.Length < 1) + throw new ArgumentException($"Invalid arguments. Expected: {nameof(hostfxr.hostfxr_resolve_frameworks_for_runtime_config)} []"); + + string runtimeConfigPath = args[0]; + string dotnetRoot = null; + if (args.Length >= 2) + dotnetRoot = args[1]; + + List resolved = new(); + List unresolved = new(); + + IntPtr resultContext = new IntPtr(123); + + hostfxr.hostfxr_resolve_frameworks_result_fn callback = (IntPtr resultPtr, IntPtr contextPtr) => + { + hostfxr.hostfxr_resolve_frameworks_result result = Marshal.PtrToStructure(resultPtr); + + if (result.size != (nuint)sizeof(hostfxr.hostfxr_resolve_frameworks_result)) + throw new Exception($"Unexpected {nameof(hostfxr.hostfxr_resolve_frameworks_result)}.size: {result.size}. Expected: {sizeof(hostfxr.hostfxr_resolve_frameworks_result)}."); + + if (contextPtr != resultContext) + throw new Exception($"Unexpected result_context value: {contextPtr}. Expected: {resultContext}."); + + for (int i = 0; i < (int)result.resolved_count; i++) + { + nint ptr = result.resolved_frameworks + i * Marshal.SizeOf(); + resolved.Add(Marshal.PtrToStructure(ptr)); + } + + for (int i = 0; i < (int)result.unresolved_count; i++) + { + nint ptr = result.unresolved_frameworks + i * Marshal.SizeOf(); + unresolved.Add(Marshal.PtrToStructure(ptr)); + } + }; + + int rc; + hostfxr.hostfxr_initialize_parameters parameters = new() + { + size = (nuint)sizeof(hostfxr.hostfxr_initialize_parameters), + host_path = IntPtr.Zero, + dotnet_root = dotnetRoot != null ? Marshal.StringToCoTaskMemAuto(dotnetRoot) : IntPtr.Zero + }; + try + { + rc = hostfxr.hostfxr_resolve_frameworks_for_runtime_config( + runtime_config_path: runtimeConfigPath, + parameters: ¶meters, + callback: callback, + result_context: resultContext); + } + finally + { + Marshal.FreeCoTaskMem(parameters.dotnet_root); + } + + string api = nameof(hostfxr.hostfxr_resolve_frameworks_for_runtime_config); + LogResult(api, rc); + + Console.WriteLine($"{api} resolved_count: {resolved.Count}"); + foreach (var framework in resolved) + { + Console.WriteLine($"{api} resolved_framework: name={framework.name}, version={framework.resolved_version}, path=[{framework.resolved_path}]"); + } + + Console.WriteLine($"{api} unresolved_count: {unresolved.Count}"); + foreach (var framework in unresolved) + { + Console.WriteLine($"{api} unresolved_framework: name={framework.name}, requested_version={framework.requested_version}, path=[{framework.resolved_path}]"); + } + } + private static void LogResult(string apiName, int rc) => Console.WriteLine(rc == 0 ? $"{apiName}:Success" : $"{apiName}:Fail[0x{rc:x}]"); @@ -269,6 +388,9 @@ public static bool RunTest(string apiToTest, string[] args) case nameof(hostfxr.hostfxr_get_dotnet_environment_info): Test_hostfxr_get_dotnet_environment_info(args); break; + case nameof(hostfxr.hostfxr_resolve_frameworks_for_runtime_config): + Test_hostfxr_resolve_frameworks_for_runtime_config(args); + break; default: return false; } diff --git a/src/installer/tests/HostActivation.Tests/NativeHostApis.cs b/src/installer/tests/HostActivation.Tests/NativeHostApis.cs index c865567ec23c8..b48667ef4012b 100644 --- a/src/installer/tests/HostActivation.Tests/NativeHostApis.cs +++ b/src/installer/tests/HostActivation.Tests/NativeHostApis.cs @@ -11,6 +11,14 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation { + internal class ApiNames + { + public const string hostfxr_get_available_sdks = nameof(hostfxr_get_available_sdks); + public const string hostfxr_resolve_sdk2 = nameof(hostfxr_resolve_sdk2); + public const string hostfxr_get_dotnet_environment_info = nameof(hostfxr_get_dotnet_environment_info); + public const string hostfxr_resolve_frameworks_for_runtime_config = nameof(hostfxr_resolve_frameworks_for_runtime_config); + } + public class NativeHostApis : IClassFixture { private SharedTestState sharedTestState; @@ -20,13 +28,6 @@ public NativeHostApis(SharedTestState fixture) sharedTestState = fixture; } - private class ApiNames - { - public const string hostfxr_get_available_sdks = nameof(hostfxr_get_available_sdks); - public const string hostfxr_resolve_sdk2 = nameof(hostfxr_resolve_sdk2); - public const string hostfxr_get_dotnet_environment_info = nameof(hostfxr_get_dotnet_environment_info); - } - internal sealed class SdkAndFrameworkFixture : IDisposable { private readonly TestArtifact _artifact; @@ -410,6 +411,316 @@ public void Hostfxr_get_dotnet_environment_info_reserved_is_not_nullptr_fails() .And.HaveStdErrContaining($"{api} received an invalid argument: reserved should be null."); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Hostfxr_resolve_frameworks_for_runtime_config(bool isMissing) + { + string api = ApiNames.hostfxr_resolve_frameworks_for_runtime_config; + using (TestArtifact artifact = TestArtifact.Create(api)) + { + (string Name, string Version) requested = ("Framework", "1.2.3"); + string configPath = Path.Combine(artifact.Location, "test.runtimeconfig.json"); + RuntimeConfig.FromFile(configPath) + .WithFramework(requested.Name, requested.Version) + .Save(); + + var builder = new DotNetBuilder(artifact.Location, TestContext.BuiltDotNet.BinPath, "dotnet"); + if (!isMissing) + builder.AddFramework(requested.Name, requested.Version, c => { }); + + DotNetCli dotnet = builder.Build(); + var result = TestContext.BuiltDotNet.Exec(sharedTestState.HostApiInvokerApp.AppDll, api, configPath, dotnet.BinPath) + .CaptureStdOut() + .CaptureStdErr() + .Execute(); + result.Should().Pass() + .And.NotHaveStdErr() + .And.ReturnStatusCode(api, isMissing ? Constants.ErrorCode.FrameworkMissingFailure : Constants.ErrorCode.Success); + if (isMissing) + { + result.Should().ReturnUnresolvedFramework(requested.Name, requested.Version); + } + else + { + result.Should().ReturnResolvedFramework(requested.Name, requested.Version, GetFrameworkPath(requested.Name, requested.Version, dotnet.BinPath)); + } + } + } + + [Fact] + public void Hostfxr_resolve_frameworks_for_runtime_config_SelfContained() + { + string api = ApiNames.hostfxr_resolve_frameworks_for_runtime_config; + using (TestArtifact artifact = TestArtifact.Create(api)) + { + (string Name, string Version)[] includedFrameworks = new[] { ("Framework", "1.0.0"), ("OtherFramework", "2.3.4"), ("Another", "5.6.7") }; + string configPath = Path.Combine(artifact.Location, "test.runtimeconfig.json"); + RuntimeConfig config = RuntimeConfig.FromFile(configPath); + foreach (var framework in includedFrameworks) + { + config.WithIncludedFramework(framework.Name, framework.Version); + } + + config.Save(); + + var result = TestContext.BuiltDotNet.Exec(sharedTestState.HostApiInvokerApp.AppDll, api, configPath) + .CaptureStdOut() + .CaptureStdErr() + .Execute(); + result.Should().Pass() + .And.NotHaveStdErr() + .And.ReturnStatusCode(api, Constants.ErrorCode.Success); + foreach (var framework in includedFrameworks) + { + // All frameworks included in a self-contained config are resolved to be next to the config + result.Should().ReturnResolvedFramework(framework.Name, framework.Version, artifact.Location); + } + } + } + + // This test only does basic validation the host API with roll-forward settings. Logic is shared for this API + // and framework resolution for running an app. More complex scenarios are covered in FrameworkResolution tests. + [Theory] + [InlineData(false, Constants.RollForwardSetting.Disable)] + [InlineData(false, Constants.RollForwardSetting.LatestPatch)] + [InlineData(false, Constants.RollForwardSetting.Minor)] + [InlineData(false, Constants.RollForwardSetting.LatestMinor)] + [InlineData(false, Constants.RollForwardSetting.Major)] + [InlineData(false, Constants.RollForwardSetting.LatestMajor)] + [InlineData(false, null)] // Default: Minor + [InlineData(true, Constants.RollForwardSetting.Disable)] + [InlineData(true, Constants.RollForwardSetting.LatestPatch)] + [InlineData(true, Constants.RollForwardSetting.Minor)] + [InlineData(true, Constants.RollForwardSetting.LatestMinor)] + [InlineData(true, Constants.RollForwardSetting.Major)] + [InlineData(true, Constants.RollForwardSetting.LatestMajor)] + [InlineData(true, null)] // Default: Minor + public void Hostfxr_resolve_frameworks_for_runtime_config_RollForward(bool isMissing, string rollForward) + { + string api = ApiNames.hostfxr_resolve_frameworks_for_runtime_config; + using (TestArtifact artifact = TestArtifact.Create(api)) + { + (string Name, string Version) requested = ("Framework", "1.2.3"); + string configPath = Path.Combine(artifact.Location, "test.runtimeconfig.json"); + RuntimeConfig.FromFile(configPath) + .WithFramework(requested.Name, requested.Version) + .WithRollForward(rollForward) + .Save(); + + // Use version that matches or doesn't based on roll-forward settings and expected result + Version requestedVersion = Version.Parse(requested.Version); + Version version = rollForward switch + { + Constants.RollForwardSetting.Disable + => isMissing + ? new (requestedVersion.Major, requestedVersion.Minor, requestedVersion.Build + 1) + : requestedVersion, + Constants.RollForwardSetting.LatestPatch + => isMissing + ? new (requestedVersion.Major, requestedVersion.Minor + 1, requestedVersion.Build) + : new (requestedVersion.Major, requestedVersion.Minor, requestedVersion.Build + 1), + Constants.RollForwardSetting.Minor or Constants.RollForwardSetting.LatestMinor or null + => isMissing + ? new (requestedVersion.Major + 1, requestedVersion.Minor, requestedVersion.Build) + : new (requestedVersion.Major, requestedVersion.Minor + 1, requestedVersion.Build), + Constants.RollForwardSetting.Major or Constants.RollForwardSetting.LatestMajor + => isMissing + ? new (requestedVersion.Major - 1, requestedVersion.Minor, requestedVersion.Build) + : new (requestedVersion.Major + 1, requestedVersion.Minor, requestedVersion.Build), + _ => throw new ArgumentException($"Invalid roll forward setting: {rollForward}") + }; + + string actualVersion = version.ToString(3); + DotNetCli dotnet = new DotNetBuilder(artifact.Location, TestContext.BuiltDotNet.BinPath, "dotnet") + .AddFramework(requested.Name, actualVersion, c => { }) + .Build(); + + var result = TestContext.BuiltDotNet.Exec(sharedTestState.HostApiInvokerApp.AppDll, api, configPath, dotnet.BinPath) + .CaptureStdOut() + .CaptureStdErr() + .Execute(); + result.Should().Pass() + .And.NotHaveStdErr() + .And.ReturnStatusCode(api, isMissing ? Constants.ErrorCode.FrameworkMissingFailure : Constants.ErrorCode.Success); + if (isMissing) + { + result.Should().ReturnUnresolvedFramework(requested.Name, requested.Version); + } + else + { + result.Should().ReturnResolvedFramework(requested.Name, actualVersion, GetFrameworkPath(requested.Name, actualVersion, dotnet.BinPath)); + } + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Hostfxr_resolve_frameworks_for_runtime_config_NoDotnetRoot(bool isMissing) + { + string api = ApiNames.hostfxr_resolve_frameworks_for_runtime_config; + using (TestArtifact artifact = TestArtifact.Create(api)) + { + string requestedVersion; + if (isMissing) + { + // Request a higher major framework version than the one available relative to the running hostfxr + Version existingVersion = Version.Parse(TestContext.MicrosoftNETCoreAppVersion.Contains('-') + ? TestContext.MicrosoftNETCoreAppVersion[..TestContext.MicrosoftNETCoreAppVersion.IndexOf('-')] + : TestContext.MicrosoftNETCoreAppVersion); + Version newerVersion = new Version(existingVersion.Major + 1, existingVersion.Minor, existingVersion.Build); + requestedVersion = newerVersion.ToString(3); + } + else + { + // Request the framework version that is available relative to the running hostfxr + requestedVersion = TestContext.MicrosoftNETCoreAppVersion; + } + + (string Name, string Version) requested = (Constants.MicrosoftNETCoreApp, requestedVersion); + + + string configPath = Path.Combine(artifact.Location, "test.runtimeconfig.json"); + RuntimeConfig.FromFile(configPath) + .WithFramework(requested.Name, requested.Version) + .Save(); + + // API should use the running hostfxr when dotnet root is not specified + var result = TestContext.BuiltDotNet.Exec(sharedTestState.HostApiInvokerApp.AppDll, api, configPath) + .CaptureStdOut() + .CaptureStdErr() + .Execute(); + result.Should().Pass() + .And.NotHaveStdErr() + .And.ReturnStatusCode(api, isMissing ? Constants.ErrorCode.FrameworkMissingFailure : Constants.ErrorCode.Success); + if (isMissing) + { + result.Should().ReturnUnresolvedFramework(requested.Name, requested.Version); + } + else + { + result.Should().ReturnResolvedFramework(requested.Name, requested.Version, GetFrameworkPath(requested.Name, requested.Version, TestContext.BuiltDotNet.BinPath)); + } + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Hostfxr_resolve_frameworks_for_runtime_config_MultipleFrameworks(bool isMissing) + { + string api = ApiNames.hostfxr_resolve_frameworks_for_runtime_config; + using (TestArtifact artifact = TestArtifact.Create(api)) + { + (string Name, string Version)[] requestedFrameworks = new[] { ("Framework", "1.0.0"), ("OtherFramework", "2.3.4"), ("Another", "5.6.7") }; + (string Name, string Version)[] expectedFrameworks = isMissing ? requestedFrameworks[..^1] : requestedFrameworks; + + string configPath = Path.Combine(artifact.Location, "test.runtimeconfig.json"); + RuntimeConfig config = RuntimeConfig.FromFile(configPath); + foreach (var framework in requestedFrameworks) + { + config.WithFramework(framework.Name, framework.Version); + } + + config.Save(); + + var builder = new DotNetBuilder(artifact.Location, TestContext.BuiltDotNet.BinPath, "dotnet"); + foreach (var framework in expectedFrameworks) + { + builder.AddFramework(framework.Name, framework.Version, c => { }); + } + + DotNetCli dotnet = builder.Build(); + + var result = TestContext.BuiltDotNet.Exec(sharedTestState.HostApiInvokerApp.AppDll, api, configPath, dotnet.BinPath) + .CaptureStdOut() + .CaptureStdErr() + .Execute(); + result.Should().Pass() + .And.NotHaveStdErr() + .And.ReturnStatusCode(api, isMissing ? Constants.ErrorCode.FrameworkMissingFailure : Constants.ErrorCode.Success); + foreach (var framework in expectedFrameworks) + { + result.Should().ReturnResolvedFramework(framework.Name, framework.Version, GetFrameworkPath(framework.Name, framework.Version, dotnet.BinPath)); + } + + if (isMissing) + { + var missingFramework = requestedFrameworks[^1]; + result.Should().ReturnUnresolvedFramework(missingFramework.Name, missingFramework.Version); + } + } + } + + [Fact] + public void Hostfxr_resolve_frameworks_for_runtime_config_IncompatibleFrameworks() + { + string api = ApiNames.hostfxr_resolve_frameworks_for_runtime_config; + using (TestArtifact artifact = TestArtifact.Create(api)) + { + (string Name, string Version) incompatibleLower = ("OtherFramework", "1.2.3"); + (string Name, string Version) incompatibleHigher = (incompatibleLower.Name, "2.0.0"); + (string Name, string Version)[] requested = new[] { ("Framework", "3.0.0"), incompatibleLower }; + + string configPath = Path.Combine(artifact.Location, "test.runtimeconfig.json"); + RuntimeConfig config = RuntimeConfig.FromFile(configPath); + foreach (var framework in requested) + { + config.WithFramework(framework.Name, framework.Version); + } + + config.Save(); + + var expectedFramework = requested[0]; + DotNetCli dotnet = new DotNetBuilder(artifact.Location, TestContext.BuiltDotNet.BinPath, "dotnet") + .AddFramework(expectedFramework.Name, expectedFramework.Version, + c => c.WithFramework(incompatibleHigher.Name, incompatibleHigher.Version)) + .Build(); + + TestContext.BuiltDotNet.Exec(sharedTestState.HostApiInvokerApp.AppDll, api, configPath, dotnet.BinPath) + .CaptureStdOut() + .CaptureStdErr() + .Execute() + .Should().Pass() + .And.NotHaveStdErr() + .And.ReturnStatusCode(api, Constants.ErrorCode.FrameworkCompatFailure) + .And.ReturnResolvedFramework(expectedFramework.Name, expectedFramework.Version, GetFrameworkPath(expectedFramework.Name, expectedFramework.Version, dotnet.BinPath)) + .And.ReturnUnresolvedFramework(incompatibleLower.Name, incompatibleLower.Version) + .And.ReturnUnresolvedFramework(incompatibleHigher.Name, incompatibleHigher.Version); + } + } + + [Fact] + public void Hostfxr_resolve_frameworks_for_runtime_config_InvalidConfig() + { + string api = ApiNames.hostfxr_resolve_frameworks_for_runtime_config; + using (TestArtifact artifact = TestArtifact.Create(api)) + { + (string Name, string Version) requested = ("Framework", "1.2.3"); + string configPath = Path.Combine(artifact.Location, "test.runtimeconfig.json"); + RuntimeConfig.FromFile(configPath) + .WithFramework(requested.Name, requested.Version) + .Save(); + + DotNetCli dotnet = new DotNetBuilder(artifact.Location, TestContext.BuiltDotNet.BinPath, "dotnet") + .AddFramework(requested.Name, requested.Version, c => { }) + .Build(); + + string frameworkPath = Path.Combine(dotnet.BinPath, "shared", requested.Name, requested.Version); + File.WriteAllText(Path.Combine(frameworkPath, $"{requested.Name}.runtimeconfig.json"), "{}"); + + TestContext.BuiltDotNet.Exec(sharedTestState.HostApiInvokerApp.AppDll, api, configPath, dotnet.BinPath) + .CaptureStdOut() + .CaptureStdErr() + .Execute() + .Should().Pass() + .And.NotHaveStdErr() + .And.ReturnStatusCode(api, Constants.ErrorCode.InvalidConfigFile) + .And.ReturnUnresolvedFramework(requested.Name, requested.Version, frameworkPath); + } + } + [Fact] public void Hostpolicy_corehost_set_error_writer_test() { @@ -443,6 +754,9 @@ public void HostRuntimeContract_bundle_probe() .And.HaveStdOutContaining("host_runtime_contract.bundle_probe is not set"); } + private static string GetFrameworkPath(string name, string version, string dotnetRoot) + => Path.Combine(dotnetRoot, "shared", name, version); + public class SharedTestState : IDisposable { public TestApp HostApiInvokerApp { get; } @@ -490,5 +804,17 @@ public static AndConstraint ReturnStatusCode(this Comma ? assertion.HaveStdOutContaining($"{apiName}:Success") : assertion.HaveStdOutContaining($"{apiName}:Fail[0x{statusCode:x}]"); } + + public static AndConstraint ReturnResolvedFramework(this CommandResultAssertions assertion, string name, string version, string path) + { + string api = ApiNames.hostfxr_resolve_frameworks_for_runtime_config; + return assertion.HaveStdOutContaining($"{api} resolved_framework: name={name}, version={version}, path=[{path}]"); + } + public static AndConstraint ReturnUnresolvedFramework(this CommandResultAssertions assertion, string name, string version, string path = "") + { + string api = ApiNames.hostfxr_resolve_frameworks_for_runtime_config; + return assertion.HaveStdOutContaining($"{api} unresolved_framework: name={name}, requested_version={version}, path=[{path}]") + .And.NotHaveStdOutContaining($"{api} resolved_framework: name={name}"); + } } } diff --git a/src/installer/tests/TestUtils/Constants.cs b/src/installer/tests/TestUtils/Constants.cs index 2a5ce2f53f559..f79889dc17aee 100644 --- a/src/installer/tests/TestUtils/Constants.cs +++ b/src/installer/tests/TestUtils/Constants.cs @@ -119,8 +119,10 @@ public static class ErrorCode public const int ResolverInitFailure = unchecked((int)0x8000808b); public const int ResolverResolveFailure = unchecked((int)0x8000808c); public const int LibHostInvalidArgs = unchecked((int)0x80008092); + public const int InvalidConfigFile = unchecked((int)0x80008093); public const int AppArgNotRunnable = unchecked((int)0x80008094); public const int FrameworkMissingFailure = unchecked((int)0x80008096); + public const int FrameworkCompatFailure = unchecked((int)0x8000809c); public const int BundleExtractionFailure = unchecked((int)0x8000809f); public const int COMPlusException = unchecked((int)0xe0434352); From bc0c10ce7230baa1df1df5ac31e646c5a7aa2c68 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Tue, 30 Apr 2024 09:58:16 -0700 Subject: [PATCH 5/8] Fix tests on non-Windows --- .../Assets/Projects/HostApiInvokerApp/Program.cs | 16 ++++++++++++++-- .../tests/HostActivation.Tests/NativeHostApis.cs | 16 ++++++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/installer/tests/Assets/Projects/HostApiInvokerApp/Program.cs b/src/installer/tests/Assets/Projects/HostApiInvokerApp/Program.cs index f14003995e6a2..d586de4aeb7af 100644 --- a/src/installer/tests/Assets/Projects/HostApiInvokerApp/Program.cs +++ b/src/installer/tests/Assets/Projects/HostApiInvokerApp/Program.cs @@ -30,6 +30,10 @@ public static void MainCore(string[] args) { Console.WriteLine("Hello World!"); Console.WriteLine(string.Join(Environment.NewLine, args)); + if (args.Length == 0) + { + throw new Exception("Invalid number of arguments passed"); + } // If requested, test multilevel lookup using fake Global SDK directories: // 1. using a fake ProgramFiles location @@ -39,22 +43,30 @@ public static void MainCore(string[] args) string testMultilevelLookupProgramFiles = Environment.GetEnvironmentVariable("TEST_MULTILEVEL_LOOKUP_PROGRAM_FILES"); string testMultilevelLookupSelfRegistered = Environment.GetEnvironmentVariable("TEST_MULTILEVEL_LOOKUP_SELF_REGISTERED"); + string hostfxrPath; if (testMultilevelLookupProgramFiles != null && testMultilevelLookupSelfRegistered != null) { Environment.SetEnvironmentVariable("_DOTNET_TEST_GLOBALLY_REGISTERED_PATH", testMultilevelLookupSelfRegistered); Environment.SetEnvironmentVariable("ProgramFiles", testMultilevelLookupProgramFiles); Environment.SetEnvironmentVariable("ProgramFiles(x86)", testMultilevelLookupProgramFiles); Environment.SetEnvironmentVariable("DOTNET_MULTILEVEL_LOOKUP", "1"); + hostfxrPath = AppContext.GetData("HOSTFXR_PATH_TEST_BEHAVIOR") as string; } else { // never rely on machine state in test if we're not faking the multi-level lookup Environment.SetEnvironmentVariable("DOTNET_MULTILEVEL_LOOKUP", "0"); + hostfxrPath = AppContext.GetData("HOSTFXR_PATH") as string; } - if (args.Length == 0) + if (hostfxrPath is not null) { - throw new Exception("Invalid number of arguments passed"); + NativeLibrary.SetDllImportResolver(typeof(Program).Assembly, (libraryName, assembly, searchPath) => + { + return libraryName == nameof(HostFXR.hostfxr) + ? NativeLibrary.Load(libraryName, assembly, searchPath) + : default; + }); } string apiToTest = args[0]; diff --git a/src/installer/tests/HostActivation.Tests/NativeHostApis.cs b/src/installer/tests/HostActivation.Tests/NativeHostApis.cs index b48667ef4012b..685d7e1404dac 100644 --- a/src/installer/tests/HostActivation.Tests/NativeHostApis.cs +++ b/src/installer/tests/HostActivation.Tests/NativeHostApis.cs @@ -768,14 +768,6 @@ public class SharedTestState : IDisposable public SharedTestState() { - HostApiInvokerApp = TestApp.CreateFromBuiltAssets("HostApiInvokerApp"); - - if (!OperatingSystem.IsWindows()) - { - // On non-Windows, we can't just P/Invoke to already loaded hostfxr, so copy it next to the app dll. - File.Copy(Binaries.HostFxr.FilePath, Path.Combine(HostApiInvokerApp.Location, Binaries.HostFxr.FileName)); - } - // Make a copy of the built .NET, as we will enable test-only behaviour copiedDotnet = TestArtifact.CreateFromCopy(nameof(NativeHostApis), TestContext.BuiltDotNet.BinPath); TestBehaviorEnabledDotNet = new DotNetCli(copiedDotnet.Location); @@ -784,6 +776,14 @@ public SharedTestState() // as we just delete the entire copy after the tests run. _ = TestOnlyProductBehavior.Enable(TestBehaviorEnabledDotNet.GreatestVersionHostFxrFilePath); + HostApiInvokerApp = TestApp.CreateFromBuiltAssets("HostApiInvokerApp"); + + // On non-Windows, we can't just P/Invoke to already loaded hostfxr, so provide the app with + // paths to hostfxr so that it can handle resolving the library. + RuntimeConfig.FromFile(HostApiInvokerApp.RuntimeConfigJson) + .WithProperty("HOSTFXR_PATH", TestContext.BuiltDotNet.GreatestVersionHostFxrFilePath) + .WithProperty("HOSTFXR_PATH_TEST_BEHAVIOR", TestBehaviorEnabledDotNet.GreatestVersionHostFxrFilePath); + SdkAndFrameworkFixture = new SdkAndFrameworkFixture(); } From 13e96d772725f18c13ef3d248f1588ce83748312 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Tue, 30 Apr 2024 16:26:38 -0700 Subject: [PATCH 6/8] Update comments: Unix -> Non-Windows --- src/native/corehost/fxr/hostfxr.cpp | 8 ++++---- src/native/corehost/hostfxr.h | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/native/corehost/fxr/hostfxr.cpp b/src/native/corehost/fxr/hostfxr.cpp index d595e03d93ea9..fd20a1b4ad349 100644 --- a/src/native/corehost/fxr/hostfxr.cpp +++ b/src/native/corehost/fxr/hostfxr.cpp @@ -114,7 +114,7 @@ SHARED_API int HOSTFXR_CALLTYPE hostfxr_main(const int argc, const pal::char_t* // // String encoding: // Windows - UTF-16 (pal::char_t is 2 byte wchar_t) -// Unix - UTF-8 (pal::char_t is 1 byte char) +// Non-Windows - UTF-8 (pal::char_t is 1 byte char) // SHARED_API int32_t HOSTFXR_CALLTYPE hostfxr_resolve_sdk( const pal::char_t* exe_dir, @@ -234,7 +234,7 @@ typedef void (HOSTFXR_CALLTYPE *hostfxr_resolve_sdk2_result_fn)( // // String encoding: // Windows - UTF-16 (pal::char_t is 2 byte wchar_t) -// Unix - UTF-8 (pal::char_t is 1 byte char) +// Non-Windows - UTF-8 (pal::char_t is 1 byte char) // SHARED_API int32_t HOSTFXR_CALLTYPE hostfxr_resolve_sdk2( const pal::char_t* exe_dir, @@ -317,7 +317,7 @@ typedef void (HOSTFXR_CALLTYPE *hostfxr_get_available_sdks_result_fn)( // // String encoding: // Windows - UTF-16 (pal::char_t is 2 byte wchar_t) -// Unix - UTF-8 (pal::char_t is 1 byte char) +// Non-Windows - UTF-8 (pal::char_t is 1 byte char) // SHARED_API int32_t HOSTFXR_CALLTYPE hostfxr_get_available_sdks( const pal::char_t* exe_dir, @@ -492,7 +492,7 @@ SHARED_API int32_t HOSTFXR_CALLTYPE hostfxr_get_dotnet_environment_info( // // String encoding: // Windows - UTF-16 (pal::char_t is 2 byte wchar_t) -// Unix - UTF-8 (pal::char_t is 1 byte char) +// Non-Windows - UTF-8 (pal::char_t is 1 byte char) // SHARED_API int32_t HOSTFXR_CALLTYPE hostfxr_get_native_search_directories(const int argc, const pal::char_t* argv[], pal::char_t buffer[], int32_t buffer_size, int32_t* required_buffer_size) { diff --git a/src/native/corehost/hostfxr.h b/src/native/corehost/hostfxr.h index 0562113325c18..0c316a59b1159 100644 --- a/src/native/corehost/hostfxr.h +++ b/src/native/corehost/hostfxr.h @@ -355,7 +355,7 @@ struct hostfxr_dotnet_environment_info // // String encoding: // Windows - UTF-16 (pal::char_t is 2 byte wchar_t) -// Unix - UTF-8 (pal::char_t is 1 byte char) +// Non-Windows - UTF-8 (pal::char_t is 1 byte char) // typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_get_dotnet_environment_info_fn)( const char_t* dotnet_root, @@ -403,11 +403,11 @@ typedef void (HOSTFXR_CALLTYPE* hostfxr_resolve_frameworks_result_fn)( // Optional. Additional context passed to the result callback. // // Return value: -// 0 on success, otherwise failure. +// 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) +// Windows - UTF-16 (pal::char_t is 2-byte wchar_t) +// Non-Windows - UTF-8 (pal::char_t is 1-byte char) // typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_resolve_frameworks_for_runtime_config_fn)( const char_t* runtime_config_path, From 06e005f7b043a3fbb4e3b04f598b68bad08fd095 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Tue, 30 Apr 2024 16:27:09 -0700 Subject: [PATCH 7/8] Make HostApiInvokerApp error early on no args --- .../tests/Assets/Projects/HostApiInvokerApp/Program.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/installer/tests/Assets/Projects/HostApiInvokerApp/Program.cs b/src/installer/tests/Assets/Projects/HostApiInvokerApp/Program.cs index d586de4aeb7af..7f72753f7296d 100644 --- a/src/installer/tests/Assets/Projects/HostApiInvokerApp/Program.cs +++ b/src/installer/tests/Assets/Projects/HostApiInvokerApp/Program.cs @@ -28,12 +28,12 @@ public static int Main(string[] args) public static void MainCore(string[] args) { - Console.WriteLine("Hello World!"); - Console.WriteLine(string.Join(Environment.NewLine, args)); if (args.Length == 0) - { - throw new Exception("Invalid number of arguments passed"); - } + throw new Exception($"{nameof(HostApiInvokerApp)} requires at least one argument specifying the API to test."); + + Console.WriteLine("Arguments:"); + foreach (string arg in args) + Console.WriteLine($" {arg}"); // If requested, test multilevel lookup using fake Global SDK directories: // 1. using a fake ProgramFiles location From a4c392360f3f0f0e5e0e8f8b835babd36a50fa62 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Thu, 9 May 2024 13:36:32 -0700 Subject: [PATCH 8/8] Update src/native/corehost/fxr/hostfxr.cpp Co-authored-by: Aaron Robinson --- src/native/corehost/fxr/hostfxr.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native/corehost/fxr/hostfxr.cpp b/src/native/corehost/fxr/hostfxr.cpp index fd20a1b4ad349..1ae8bcd8faf11 100644 --- a/src/native/corehost/fxr/hostfxr.cpp +++ b/src/native/corehost/fxr/hostfxr.cpp @@ -679,7 +679,7 @@ SHARED_API int32_t HOSTFXR_CALLTYPE hostfxr_resolve_frameworks_for_runtime_confi const hostfxr_resolve_frameworks_result result { - sizeof(hostfxr_resolve_frameworks_result), + sizeof(result), resolved.size(), resolved.empty() ? nullptr : resolved.data(), unresolved.size(),