From d45239c86e046d65a6c3ad01d68ee75c0ec1464e Mon Sep 17 00:00:00 2001 From: Vitek Karas Date: Fri, 9 Nov 2018 23:38:34 -0800 Subject: [PATCH] Introduce the component dependency resolution entry point. (dotnet/core-setup#4720) Introduce the component resolve entry point. - Refactors argument init to use the same code for app and components - Remove usage of the global init structure from args init - makes it much clearer what is used where. - Other small refactorings - Adds basic doc with notes and open questions - should eventually become the real design doc for this feature. Commit migrated from https://github.com/dotnet/core-setup/commit/2fbde0a1c6787b1eee2bd57589dae9b0e0f6a2ea --- .../host-component-dependencies-resolution.md | 44 +++ src/installer/corehost/cli/args.cpp | 94 +++-- src/installer/corehost/cli/args.h | 16 +- src/installer/corehost/cli/deps_resolver.cpp | 32 +- src/installer/corehost/cli/deps_resolver.h | 33 +- src/installer/corehost/cli/fxr/fx_muxer.cpp | 21 +- src/installer/corehost/cli/hostpolicy.cpp | 162 +++++++- src/installer/corehost/common/utils.cpp | 19 + src/installer/corehost/common/utils.h | 1 + .../ComponentWithDependencies/Component.cs | 6 + .../ComponentDependency/ComponentDependency.csproj | 8 + .../ComponentDependency/Dependency.cs | 6 + .../ComponentWithDependencies.csproj | 14 + .../ComponentWithNoDependencies/Component.cs | 6 + .../ComponentWithNoDependencies.csproj | 8 + .../ComponentWithResources/Component.cs | 6 + .../ComponentWithResources.csproj | 8 + .../ComponentWithResources/Resource.en.resx | 123 ++++++ .../ComponentWithResources/Resource.resx | 123 ++++++ .../TestProjects/HostApiInvokerApp/HostPolicy.cs | 81 ++++ .../TestProjects/HostApiInvokerApp/Program.cs | 4 + ...nThatICareAboutComponentDependencyResolution.cs | 432 +++++++++++++++++++++ .../HostActivationTests/HostActivationTests.csproj | 1 + .../test/HostActivationTests/SharedFramework.cs | 12 +- .../Assertions/CommandResultExtensions.cs | 10 + 25 files changed, 1161 insertions(+), 109 deletions(-) create mode 100644 docs/installer/design-docs/host-component-dependencies-resolution.md create mode 100644 src/installer/test/Assets/TestProjects/ComponentWithDependencies/Component.cs create mode 100644 src/installer/test/Assets/TestProjects/ComponentWithDependencies/ComponentDependency/ComponentDependency.csproj create mode 100644 src/installer/test/Assets/TestProjects/ComponentWithDependencies/ComponentDependency/Dependency.cs create mode 100644 src/installer/test/Assets/TestProjects/ComponentWithDependencies/ComponentWithDependencies.csproj create mode 100644 src/installer/test/Assets/TestProjects/ComponentWithNoDependencies/Component.cs create mode 100644 src/installer/test/Assets/TestProjects/ComponentWithNoDependencies/ComponentWithNoDependencies.csproj create mode 100644 src/installer/test/Assets/TestProjects/ComponentWithResources/Component.cs create mode 100644 src/installer/test/Assets/TestProjects/ComponentWithResources/ComponentWithResources.csproj create mode 100644 src/installer/test/Assets/TestProjects/ComponentWithResources/Resource.en.resx create mode 100644 src/installer/test/Assets/TestProjects/ComponentWithResources/Resource.resx create mode 100644 src/installer/test/Assets/TestProjects/HostApiInvokerApp/HostPolicy.cs create mode 100644 src/installer/test/HostActivationTests/GivenThatICareAboutComponentDependencyResolution.cs diff --git a/docs/installer/design-docs/host-component-dependencies-resolution.md b/docs/installer/design-docs/host-component-dependencies-resolution.md new file mode 100644 index 0000000..7307f17 --- /dev/null +++ b/docs/installer/design-docs/host-component-dependencies-resolution.md @@ -0,0 +1,44 @@ +# Component dependency resolution support in host + +This feature provides support for component dependency resolution. The planned usage is going to be implementing a managed public API which will provide a building block for custom AssemblyLoadContext implementation or for any other solution which needs to be able to resolve dependencies for a given component. In .NET Core the SDK produces `.deps.json` to describe component dependencies. The host already uses this during app startup to determine the list of static dependencies for the app itself. This new capability is to enable to use the same code/logic for resolving dependencies of dynamically accessed/loaded components. + +The host components (mostly `hostpolicy`) already contain code which does `.deps.json` parsing and resolution for the app start, so to avoid code duplication and maintain consistent behavior the same code is used for component dependency resolution as well. + +## Frameworks handling +As of now the dependency resolution does NOT process frameworks. It only resolves dependencies directly described in the `.deps.json` of the component being processed. +For typical plugin scenarios resolving framework assemblies is not desirable, as the plugin will very likely want to share the framework with the app, and thus the ALC based on this component dependency resolution should fall back to the app for framework assemblies. Since there's no good way to detect which assembly is framework, it's better to not return them from the resolution for this scenario. +In the future it's possible expand the functionality to also support full framework resolution, but it should be an explicit choice (opt-in). The scenario for this would be using this feature to resolve dependencies for the `MetadataLoadContext` for inspection not loading. In which case it might make sense to load a full "app" using this functionality (including separate frameworks and so on). + +Consequence of this is that we're not processing `.runtimeconfig.json` at all. For most cases this doesn't matter as typical `classlib` projects won't have `.runtimeconfig.json` generated by the SDK. Whenever the full framework resolution functionality is implemented, the feature would have to start processing `.runtimeconfig.json` as well. + +## Relationship to DependencyModel APIs +This feature certainly provides a somewhat duplicate functionality to the existing DependencyModel. With several core differences though: +* DependencyModel is an OM-like API fully describing the `.deps.json` format. It's not simple to use it for dependency resolution (although it's possible). +* Using DependencyModel in a similar fashion would require additional code handling things like servicing, store and RID fallbacks. +* DependencyModel exposes additional information like compile-only assets which don't play any role in dependency resolution. +* DependencyModel comes with its own dependencies (namely Newtonsoft.Json) and thus is harder to use in some scenarios (or might event prevent its usage in certain cases). +* It would not be practical to layer DependencyModel on top of this feature since it needs to expose the full picture of `.deps.json`. The dependency resolution only handles runtime assets and only limits its view to the current RID. + +## Notes +* The component dependency resolution uses the same servicing and shared store paths as the app. So it's up to the app to set these up either via config or environment variables. +* There are several settings which are inherited from the app for the purpose of dependency resolution of the components. Note that for simple components most of these are not actually used for anything + * probe_paths - reused + * tfm - reused + * host_mode - reused + * RID fallback graph from the root framework - reused +* Just like settings, there's a set of environment variables which are used in the same way as the app would use them + * `DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX` - used just like in the app + * `ProgramFiles` and `ProgramFiles(x86")` - used to find servicing root and the shared store + * `DOTNET_SHARED_STORE` - used to find the shared store - just like the app + * `DOTNET_MULTILEVEL_LOOKUP` - used to enable multi-level lookup - used just like in the app +* Right now this feature doesn't process `.runtimeconfig.json` or `.runtimeconfig.dev.json`. Most dynamically loaded components don't have these anyway, since SDK doesn't generate these for the `classlib` project type. The only meaningful piece of info from these which could be used is the set of probing paths to use. Currently the same set ofprobing paths as for the app is used. With the changes in .NET Core 3 where `dotnet build` will copy all static dependencies locally, the importance of additional probing paths should be very low anyway. + +## Open questions +* Which probing paths (as per [host-probing](host-probing.md)) should be used for the component. Currently it's the same set as for the app. Which means that scenarios like self-contained app consuming `dotnet build` produced component won't work since self-contained apps typically have no probing paths (specifically they don't setup NuGet paths in any way). +* Review the list of settings which are reused from the app (see above) +* Review the list of environment variables - if we should use the same as the app or not +* Currently we don't consider frameworks for the app when computing probing paths for resolving assets from the component's `.deps.json`. This is a different behavior from the app startup where these are considered. Is it important - needed? +* Error reporting: If the native code fails currently it only returns error code. So next to no information. It writes detailed error message into the stderr (even with tracing disabled) which is also wrong since it pollutes the process output. It should be captured and returned to the caller, so that the managed code can include it in the exception. There's also consideration for localization - as currently the native components don't use localized error reporting. +* Add ability to corelate tracing with the runtime - probably some kind of activity ID +* Handling of native assets - currently returning just probing paths. Would be cleaner to return full resolved paths. But we would have to keep some probing paths. In the case of missing `.deps.json` the native library should be looked for in the component directory - thus requires probing - we can't figure out which of the files in the folder are native libraries in the hosts. +* Handling of satellite assemblies (resource assets) - currently returning just probing paths which exclude the culture. So from a resolved asset `./foo/en-us/resource.dll` we only take `./foo` as the probing path. Consider using full paths instead - probably would require more parsing as we would have to be able to figure out the culture ID somewhere to build the true map AssemblyName->path in the managed class. Just like for native assets, if there's no `.deps.json` the only possible solution is to use probing, so the probing semantics would have to be supported anyway. \ No newline at end of file diff --git a/src/installer/corehost/cli/args.cpp b/src/installer/corehost/cli/args.cpp index 66365be..699cebc 100644 --- a/src/installer/corehost/cli/args.cpp +++ b/src/installer/corehost/cli/args.cpp @@ -28,42 +28,40 @@ arguments_t::arguments_t() : * Windows: C:\Program Files (x86) or * Unix: directory of dotnet on the path.\\ */ -void setup_shared_store_paths(const hostpolicy_init_t& init, const pal::string_t& own_dir, arguments_t* args) +void setup_shared_store_paths(const pal::string_t& tfm, host_mode_t host_mode,const pal::string_t& own_dir, arguments_t* args) { - if (init.tfm.empty()) + if (tfm.empty()) { // Old (MNA < 1.1.*) "runtimeconfig.json" files do not contain TFM property. return; } // Environment variable DOTNET_SHARED_STORE - (void) get_env_shared_store_dirs(&args->env_shared_store, get_arch(), init.tfm); + (void) get_env_shared_store_dirs(&args->env_shared_store, get_arch(), tfm); // "dotnet.exe" relative shared store folder - if (init.host_mode == host_mode_t::muxer) + if (host_mode == host_mode_t::muxer) { args->dotnet_shared_store = own_dir; append_path(&args->dotnet_shared_store, RUNTIME_STORE_DIRECTORY_NAME); append_path(&args->dotnet_shared_store, get_arch()); - append_path(&args->dotnet_shared_store, init.tfm.c_str()); + append_path(&args->dotnet_shared_store, tfm.c_str()); } // Global shared store dir bool multilevel_lookup = multilevel_lookup_enabled(); if (multilevel_lookup) { - get_global_shared_store_dirs(&args->global_shared_stores, get_arch(), init.tfm); + get_global_shared_store_dirs(&args->global_shared_stores, get_arch(), tfm); } } bool parse_arguments( const hostpolicy_init_t& init, - const int argc, const pal::char_t* argv[], arguments_t* arg_out) + const int argc, const pal::char_t* argv[], + arguments_t& args) { - arguments_t& args = *arg_out; - - args.host_path = init.host_info.host_path; - + pal::string_t managed_application_path; if (init.host_mode != host_mode_t::apphost) { // First argument is managed app @@ -71,60 +69,72 @@ bool parse_arguments( { return false; } - args.managed_application = pal::string_t(argv[1]); - if (!pal::realpath(&args.managed_application)) - { - trace::error(_X("Failed to locate managed application [%s]"), args.managed_application.c_str()); - return false; - } - args.app_root = get_directory(args.managed_application); + + managed_application_path = pal::string_t(argv[1]); + args.app_argc = argc - 2; args.app_argv = &argv[2]; } else { // Find the managed app in the same directory - args.managed_application = init.host_info.app_path; - if (!pal::realpath(&args.managed_application)) - { - trace::error(_X("Failed to locate managed application [%s]"), args.managed_application.c_str()); - return false; - } - args.app_root = get_directory(init.host_info.app_path); + managed_application_path = init.host_info.app_path; + args.app_argv = &argv[1]; args.app_argc = argc - 1; } - if (!init.deps_file.empty()) + return init_arguments( + managed_application_path, + init.host_info, + init.tfm, + init.host_mode, + init.additional_deps_serialized, + init.deps_file, + init.probe_paths, + args); +} + +bool init_arguments( + const pal::string_t& managed_application_path, + const host_startup_info_t& host_info, + const pal::string_t& tfm, + host_mode_t host_mode, + const pal::string_t& additional_deps_serialized, + const pal::string_t& deps_file, + const std::vector& probe_paths, + arguments_t& args) +{ + args.host_path = host_info.host_path; + args.additional_deps_serialized = additional_deps_serialized; + + args.managed_application = managed_application_path; + if (!pal::realpath(&args.managed_application)) + { + trace::error(_X("Failed to locate managed application [%s]"), args.managed_application.c_str()); + return false; + } + args.app_root = get_directory(args.managed_application); + + if (!deps_file.empty()) { - args.deps_path = init.deps_file; + args.deps_path = deps_file; args.app_root = get_directory(args.deps_path); } - for (const auto& probe : init.probe_paths) + for (const auto& probe : probe_paths) { args.probe_paths.push_back(probe); } - + if (args.deps_path.empty()) { - const auto& app_base = args.app_root; - auto app_name = get_filename(args.managed_application); - - args.deps_path.reserve(app_base.length() + 1 + app_name.length() + 5); - args.deps_path.append(app_base); - - if (!app_base.empty() && app_base.back() != DIR_SEPARATOR) - { - args.deps_path.push_back(DIR_SEPARATOR); - } - args.deps_path.append(app_name, 0, app_name.find_last_of(_X("."))); - args.deps_path.append(_X(".deps.json")); + args.deps_path = get_deps_from_app_binary(args.app_root, args.managed_application); } pal::get_default_servicing_directory(&args.core_servicing); - setup_shared_store_paths(init, get_directory(args.host_path), &args); + setup_shared_store_paths(tfm, host_mode, get_directory(args.host_path), &args); return true; } diff --git a/src/installer/corehost/cli/args.h b/src/installer/corehost/cli/args.h index e9208d1..f05058a 100644 --- a/src/installer/corehost/cli/args.h +++ b/src/installer/corehost/cli/args.h @@ -99,12 +99,14 @@ struct arguments_t std::vector global_shared_stores; pal::string_t dotnet_shared_store; std::vector env_shared_store; + pal::string_t additional_deps_serialized; + int app_argc; const pal::char_t** app_argv; arguments_t(); - inline void print() + inline void trace() { if (trace::is_enabled()) { @@ -129,6 +131,16 @@ struct arguments_t bool parse_arguments( const hostpolicy_init_t& init, - const int argc, const pal::char_t* argv[], arguments_t* arg_out); + const int argc, const pal::char_t* argv[], + arguments_t& arg); +bool init_arguments( + const pal::string_t& managed_application_path, + const host_startup_info_t& host_info, + const pal::string_t& tfm, + host_mode_t host_mode, + const pal::string_t& additional_deps_serialized, + const pal::string_t& deps_file, + const std::vector& probe_paths, + arguments_t& args); #endif // ARGS_H diff --git a/src/installer/corehost/cli/deps_resolver.cpp b/src/installer/corehost/cli/deps_resolver.cpp index e83736c..1ed4a15 100644 --- a/src/installer/corehost/cli/deps_resolver.cpp +++ b/src/installer/corehost/cli/deps_resolver.cpp @@ -23,6 +23,13 @@ const pal::string_t ManifestListMessage = _X( " This assembly was expected to be in the local runtime store as the application was published using the following target manifest files:\n" " %s"); +const pal::string_t DuplicateAssemblyWithDifferentExtensionMessage = _X( + "Error:\n" + " An assembly specified in the application dependencies manifest (%s) has already been found but with a different file extension:\n" + " package: '%s', version: '%s'\n" + " path: '%s'\n" + " previously found assembly: '%s'"); + namespace { // ----------------------------------------------------------------------------- @@ -171,7 +178,6 @@ void deps_resolver_t::get_dir_assemblies( } void deps_resolver_t::setup_shared_store_probes( - const hostpolicy_init_t& init, const arguments_t& args) { for (const auto& shared : args.env_shared_store) @@ -215,7 +221,6 @@ pal::string_t deps_resolver_t::get_lookup_probe_directories() } void deps_resolver_t::setup_probe_config( - const hostpolicy_init_t& init, const arguments_t& args) { if (pal::directory_exists(args.core_servicing)) @@ -239,15 +244,15 @@ void deps_resolver_t::setup_probe_config( m_probes.push_back(probe_config_t::published_deps_dir()); // The framework locations, starting with highest level framework. - for (int i = 1; i < init.fx_definitions.size(); ++i) + for (int i = 1; i < m_fx_definitions.size(); ++i) { - if (pal::directory_exists(init.fx_definitions[i]->get_dir())) + if (pal::directory_exists(m_fx_definitions[i]->get_dir())) { - m_probes.push_back(probe_config_t::fx(init.fx_definitions[i]->get_dir(), &init.fx_definitions[i]->get_deps(), i)); + m_probes.push_back(probe_config_t::fx(m_fx_definitions[i]->get_dir(), &m_fx_definitions[i]->get_deps(), i)); } } - setup_shared_store_probes(init, args); + setup_shared_store_probes(args); for (const auto& probe : m_additional_probes) { @@ -446,12 +451,8 @@ bool deps_resolver_t::resolve_tpa_list( // Verify the extension is the same as the previous verified entry if (get_deps_filename(entry.asset.relative_path) != get_filename(existing->second.resolved_path)) { - trace::error(_X( - "Error:\n" - " An assembly specified in the application dependencies manifest (%s) has already been found but with a different file extension:\n" - " package: '%s', version: '%s'\n" - " path: '%s'\n" - " previously found assembly: '%s'"), + trace::error( + DuplicateAssemblyWithDifferentExtensionMessage.c_str(), entry.deps_file.c_str(), entry.library_name.c_str(), entry.library_version.c_str(), @@ -584,7 +585,7 @@ void deps_resolver_t::init_known_entry_path(const deps_entry_t& entry, const pal } } -void deps_resolver_t::resolve_additional_deps(const hostpolicy_init_t& init) +void deps_resolver_t::resolve_additional_deps(const arguments_t& args, const deps_json_t::rid_fallback_graph_t& rid_fallback_graph) { if (!m_is_framework_dependent) { @@ -597,7 +598,7 @@ void deps_resolver_t::resolve_additional_deps(const hostpolicy_init_t& init) return; } - pal::string_t additional_deps_serialized = init.additional_deps_serialized; + pal::string_t additional_deps_serialized = args.additional_deps_serialized; if (additional_deps_serialized.empty()) { @@ -684,11 +685,10 @@ void deps_resolver_t::resolve_additional_deps(const hostpolicy_init_t& init) } } - auto rids = get_root_framework(m_fx_definitions).get_deps().get_rid_fallback_graph(); for (pal::string_t json_file : m_additional_deps_files) { m_additional_deps.push_back(std::unique_ptr( - new deps_json_t(true, json_file, rids))); + new deps_json_t(true, json_file, rid_fallback_graph))); } } diff --git a/src/installer/corehost/cli/deps_resolver.h b/src/installer/corehost/cli/deps_resolver.h index a65f3cc..030287a 100644 --- a/src/installer/corehost/cli/deps_resolver.h +++ b/src/installer/corehost/cli/deps_resolver.h @@ -39,16 +39,28 @@ typedef std::unordered_map name_to_resolve class deps_resolver_t { public: - deps_resolver_t(hostpolicy_init_t& init, const arguments_t& args) - : m_fx_definitions(init.fx_definitions) + // if root_framework_rid_fallback_graph is specified it is assumed that the fx_definitions + // doesn't contain the root framework at all. + deps_resolver_t( + const arguments_t& args, + fx_definition_vector_t& fx_definitions, + const deps_json_t::rid_fallback_graph_t* root_framework_rid_fallback_graph, + bool is_framework_dependent) + : m_fx_definitions(fx_definitions) , m_app_dir(args.app_root) , m_managed_app(args.managed_application) - , m_is_framework_dependent(init.is_framework_dependent) + , m_is_framework_dependent(is_framework_dependent) , m_core_servicing(args.core_servicing) { - int root_framework = m_fx_definitions.size() - 1; + int lowest_framework = m_fx_definitions.size() - 1; + int root_framework = -1; + if (root_framework_rid_fallback_graph == nullptr) + { + root_framework = lowest_framework; + root_framework_rid_fallback_graph = &m_fx_definitions[root_framework]->get_deps().get_rid_fallback_graph(); + } - for (int i = root_framework; i >= 0; --i) + for (int i = lowest_framework; i >= 0; --i) { if (i == 0) { @@ -69,14 +81,14 @@ public: else { // The rid graph is obtained from the root framework - m_fx_definitions[i]->parse_deps(m_fx_definitions[root_framework]->get_deps().get_rid_fallback_graph()); + m_fx_definitions[i]->parse_deps(*root_framework_rid_fallback_graph); } } - resolve_additional_deps(init); + resolve_additional_deps(args, *root_framework_rid_fallback_graph); setup_additional_probes(args.probe_paths); - setup_probe_config(init, args); + setup_probe_config(args); } bool valid(pal::string_t* errors) @@ -114,13 +126,11 @@ public: } void setup_shared_store_probes( - const hostpolicy_init_t& init, const arguments_t& args); pal::string_t get_lookup_probe_directories(); void setup_probe_config( - const hostpolicy_init_t& init, const arguments_t& args); void setup_additional_probes( @@ -135,7 +145,8 @@ public: const pal::string_t& path); void resolve_additional_deps( - const hostpolicy_init_t& init); + const arguments_t& args, + const deps_json_t::rid_fallback_graph_t& rid_fallback_graph); const deps_json_t& get_deps() const { diff --git a/src/installer/corehost/cli/fxr/fx_muxer.cpp b/src/installer/corehost/cli/fxr/fx_muxer.cpp index 51a3de8..44df04f 100644 --- a/src/installer/corehost/cli/fxr/fx_muxer.cpp +++ b/src/installer/corehost/cli/fxr/fx_muxer.cpp @@ -188,25 +188,6 @@ bool hostpolicy_exists_in_svc(const pal::string_t& version, pal::string_t* resol } /** -* Given path to app binary, say app.dll or app.exe, retrieve the app.deps.json. -*/ -pal::string_t get_deps_from_app_binary(const pal::string_t& app) -{ - assert(app.find(DIR_SEPARATOR) != pal::string_t::npos); - assert(ends_with(app, _X(".dll"), false) || ends_with(app, _X(".exe"), false)); - - // First append directory. - pal::string_t deps_file; - deps_file.assign(get_directory(app)); - - // Then the app name and the file extension - pal::string_t app_name = get_filename(app); - deps_file.append(app_name, 0, app_name.find_last_of(_X("."))); - deps_file.append(_X(".deps.json")); - return deps_file; -} - -/** * Given a version and probing paths, find if package layout * directory containing hostpolicy exists. */ @@ -261,7 +242,7 @@ pal::string_t get_deps_file( else { // Self-contained app's hostpolicy is from specified deps or from app deps. - return !specified_deps_file.empty() ? specified_deps_file : get_deps_from_app_binary(app_candidate); + return !specified_deps_file.empty() ? specified_deps_file : get_deps_from_app_binary(get_directory(app_candidate), app_candidate); } } diff --git a/src/installer/corehost/cli/hostpolicy.cpp b/src/installer/corehost/cli/hostpolicy.cpp index a9b8db1..7c576d0 100644 --- a/src/installer/corehost/cli/hostpolicy.cpp +++ b/src/installer/corehost/cli/hostpolicy.cpp @@ -19,7 +19,11 @@ hostpolicy_init_t g_init; int run(const arguments_t& args, pal::string_t* out_host_command_result = nullptr) { // Load the deps resolver - deps_resolver_t resolver(g_init, args); + deps_resolver_t resolver( + args, + g_init.fx_definitions, + /* root_framework_rid_fallback_graph */ nullptr, // This means that the fx_definitions contains the root framework + g_init.is_framework_dependent); pal::string_t resolver_errors; if (!resolver.valid(&resolver_errors)) @@ -332,6 +336,17 @@ int run(const arguments_t& args, pal::string_t* out_host_command_result = nullpt // The breadcrumb destructor will join to the background thread to finish writing } +void trace_hostpolicy_entrypoint_invocation(const pal::string_t& entryPointName) +{ + trace::info(_X("--- Invoked hostpolicy [commit hash: %s] [%s,%s,%s][%s] %s = {"), + _STRINGIFY(REPO_COMMIT_HASH), + _STRINGIFY(HOST_POLICY_PKG_NAME), + _STRINGIFY(HOST_POLICY_PKG_VER), + _STRINGIFY(HOST_POLICY_PKG_REL_DIR), + get_arch(), + entryPointName.c_str()); +} + SHARED_API int corehost_load(host_interface_t* init) { trace::setup(); @@ -347,17 +362,11 @@ SHARED_API int corehost_load(host_interface_t* init) return 0; } -int corehost_main_init(const int argc, const pal::char_t* argv[], const pal::string_t location, arguments_t& args) +int corehost_main_init(const int argc, const pal::char_t* argv[], const pal::string_t& location, arguments_t& args) { if (trace::is_enabled()) { - trace::info(_X("--- Invoked hostpolicy %s[commit hash: %s] [%s,%s,%s][%s] main = {"), - location.c_str(), - _STRINGIFY(REPO_COMMIT_HASH), - _STRINGIFY(HOST_POLICY_PKG_NAME), - _STRINGIFY(HOST_POLICY_PKG_VER), - _STRINGIFY(HOST_POLICY_PKG_REL_DIR), - get_arch()); + trace_hostpolicy_entrypoint_invocation(location); for (int i = 0; i < argc; ++i) { @@ -379,22 +388,19 @@ int corehost_main_init(const int argc, const pal::char_t* argv[], const pal::str g_init.host_info.parse(argc, argv); } - if (!parse_arguments(g_init, argc, argv, &args)) + if (!parse_arguments(g_init, argc, argv, args)) { return StatusCode::LibHostInvalidArgs; } - if (trace::is_enabled()) - { - args.print(); - } + args.trace(); return 0; } SHARED_API int corehost_main(const int argc, const pal::char_t* argv[]) { arguments_t args; - int rc = corehost_main_init(argc, argv, _X(""), args); + int rc = corehost_main_init(argc, argv, _X("corehost_main"), args); if (!rc) { rc = run(args); @@ -448,3 +454,129 @@ SHARED_API int corehost_unload() { return 0; } + +typedef void(*corehost_resolve_component_dependencies_result_fn)( + const pal::char_t* assembly_paths, + const pal::char_t* native_search_paths, + const pal::char_t* resource_search_paths); + +SHARED_API int corehost_resolve_component_dependencies( + const pal::char_t *component_main_assembly_path, + corehost_resolve_component_dependencies_result_fn result) +{ + if (trace::is_enabled()) + { + trace_hostpolicy_entrypoint_invocation(_X("corehost_resolve_component_dependencies")); + + trace::info(_X(" Component main assembly path: %s"), component_main_assembly_path); + trace::info(_X("}")); + + for (const auto& probe : g_init.probe_paths) + { + trace::info(_X("Additional probe dir: %s"), probe.c_str()); + } + } + + // TODO: Need to redirect error writing (trace::error even with tracing disabled) + // to some local buffer and return the buffer to the caller as detailed error message. + // Like this the error is written to the stderr of the process which is pretty bad. + // It makes sense for startup code path as there's no other way to report it to the user. + // But with API call from managed code, the error should be invisible outside of exception. + // Tracing should still contain the error just like now. + + // IMPORTANT: g_init is static/global and thus potentially accessed from multiple threads + // We must only use it as read-only here (unlike the run scenarios which own it). + // For example the frameworks in g_init.fx_definitions can't be used "as-is" by the resolver + // right now as it would try to re-parse the .deps.json and thus modify the objects. + + // The assumption is that component dependency resolution will only be called + // when the coreclr is hosted through this hostpolicy and thus it will + // have already called corehost_main_init. + if (!g_init.host_info.is_valid()) + { + trace::error(_X("Hostpolicy must be initialized and corehost_main must have been called before calling corehost_resolve_component_dependencies.")); + return StatusCode::CoreHostLibLoadFailure; + } + + // Initialize arguments (basically the structure describing the input app/component to resolve) + arguments_t args; + if (!init_arguments( + component_main_assembly_path, + g_init.host_info, + g_init.tfm, + g_init.host_mode, + /* additional_deps_serialized */ pal::string_t(), // Additiona deps - don't use those from the app, they're already in the app + /* deps_file */ pal::string_t(), // Avoid using any other deps file than the one next to the component + g_init.probe_paths, + args)) + { + return StatusCode::LibHostInvalidArgs; + } + + args.trace(); + + // Initialize the "app" framework definition. + auto app = new fx_definition_t(); + + // For now intentionally don't process .runtimeconfig.json since we don't perform framework resolution. + + // Call parse_runtime_config since it initializes the defaults for various settings + // but we don't have any .runtimeconfig.json for the component, so pass in empty paths. + // Empty paths is a valid case and the method will simply skip parsing anything. + app->parse_runtime_config(pal::string_t(), pal::string_t(), fx_reference_t(), fx_reference_t()); + if (!app->get_runtime_config().is_valid()) + { + // This should really never happen, but fail gracefully if it does anyway. + assert(false); + trace::error(_X("Failed to initialize empty runtime config for the component.")); + return StatusCode::InvalidConfigFile; + } + + // For components we don't want to resolve anything from the frameworks, since those will be supplied by the app. + // So only use the component as the "app" framework. + fx_definition_vector_t component_fx_definitions; + component_fx_definitions.push_back(std::unique_ptr(app)); + + // TODO Review: Since we're only passing the one component framework, the resolver will not consider + // frameworks from the app for probing paths. So potential references to paths inside frameworks will not resolve. + + // The RID graph still has to come from the actuall root framework, so take that from the g_init.fx_definitions + // which are the frameworks for the app. + deps_resolver_t resolver( + args, + component_fx_definitions, + &get_root_framework(g_init.fx_definitions).get_deps().get_rid_fallback_graph(), + true); + + pal::string_t resolver_errors; + if (!resolver.valid(&resolver_errors)) + { + trace::error(_X("Error initializing the dependency resolver: %s"), resolver_errors.c_str()); + return StatusCode::ResolverInitFailure; + } + + // Don't write breadcrumbs since we're not executing the app, just resolving dependencies + // doesn't guarantee that they will actually execute. + + probe_paths_t probe_paths; + if (!resolver.resolve_probe_paths(&probe_paths, nullptr)) + { + return StatusCode::ResolverResolveFailure; + } + + if (trace::is_enabled()) + { + trace::info(_X("corehost_resolve_component_dependencies results: {")); + trace::info(_X(" assembly_paths: '%s'"), probe_paths.tpa.data()); + trace::info(_X(" native_search_paths: '%s'"), probe_paths.native.data()); + trace::info(_X(" resource_search_paths: '%s'"), probe_paths.resources.data()); + trace::info(_X("}")); + } + + result( + probe_paths.tpa.data(), + probe_paths.native.data(), + probe_paths.resources.data()); + + return 0; +} diff --git a/src/installer/corehost/common/utils.cpp b/src/installer/corehost/common/utils.cpp index 797bd07..a220739 100644 --- a/src/installer/corehost/common/utils.cpp +++ b/src/installer/corehost/common/utils.cpp @@ -382,3 +382,22 @@ pal::string_t get_dotnet_root_env_var_name() return pal::string_t(_X("DOTNET_ROOT")); } + +/** +* Given path to app binary, say app.dll or app.exe, retrieve the app.deps.json. +*/ +pal::string_t get_deps_from_app_binary(const pal::string_t& app_base, const pal::string_t& app) +{ + pal::string_t deps_file; + auto app_name = get_filename(app); + deps_file.reserve(app_base.length() + 1 + app_name.length() + 5); + deps_file.append(app_base); + + if (!app_base.empty() && app_base.back() != DIR_SEPARATOR) + { + deps_file.push_back(DIR_SEPARATOR); + } + deps_file.append(app_name, 0, app_name.find_last_of(_X("."))); + deps_file.append(_X(".deps.json")); + return deps_file; +} diff --git a/src/installer/corehost/common/utils.h b/src/installer/corehost/common/utils.h index e04681f..371e493 100644 --- a/src/installer/corehost/common/utils.h +++ b/src/installer/corehost/common/utils.h @@ -54,4 +54,5 @@ bool get_file_path_from_env(const pal::char_t* env_key, pal::string_t* recv); size_t index_of_non_numeric(const pal::string_t& str, unsigned i); bool try_stou(const pal::string_t& str, unsigned* num); pal::string_t get_dotnet_root_env_var_name(); +pal::string_t get_deps_from_app_binary(const pal::string_t& app_base, const pal::string_t& app); #endif diff --git a/src/installer/test/Assets/TestProjects/ComponentWithDependencies/Component.cs b/src/installer/test/Assets/TestProjects/ComponentWithDependencies/Component.cs new file mode 100644 index 0000000..e6b49c6 --- /dev/null +++ b/src/installer/test/Assets/TestProjects/ComponentWithDependencies/Component.cs @@ -0,0 +1,6 @@ +namespace Component +{ + public class Component + { + } +} \ No newline at end of file diff --git a/src/installer/test/Assets/TestProjects/ComponentWithDependencies/ComponentDependency/ComponentDependency.csproj b/src/installer/test/Assets/TestProjects/ComponentWithDependencies/ComponentDependency/ComponentDependency.csproj new file mode 100644 index 0000000..07612ef --- /dev/null +++ b/src/installer/test/Assets/TestProjects/ComponentWithDependencies/ComponentDependency/ComponentDependency.csproj @@ -0,0 +1,8 @@ + + + + $(NETCoreAppFramework) + $(MNAVersion) + + + diff --git a/src/installer/test/Assets/TestProjects/ComponentWithDependencies/ComponentDependency/Dependency.cs b/src/installer/test/Assets/TestProjects/ComponentWithDependencies/ComponentDependency/Dependency.cs new file mode 100644 index 0000000..a2a8e47 --- /dev/null +++ b/src/installer/test/Assets/TestProjects/ComponentWithDependencies/ComponentDependency/Dependency.cs @@ -0,0 +1,6 @@ +namespace ComponentDependency +{ + public class Dependency + { + } +} \ No newline at end of file diff --git a/src/installer/test/Assets/TestProjects/ComponentWithDependencies/ComponentWithDependencies.csproj b/src/installer/test/Assets/TestProjects/ComponentWithDependencies/ComponentWithDependencies.csproj new file mode 100644 index 0000000..35d3b20 --- /dev/null +++ b/src/installer/test/Assets/TestProjects/ComponentWithDependencies/ComponentWithDependencies.csproj @@ -0,0 +1,14 @@ + + + + $(NETCoreAppFramework) + $(MNAVersion) + + + + + + + + + diff --git a/src/installer/test/Assets/TestProjects/ComponentWithNoDependencies/Component.cs b/src/installer/test/Assets/TestProjects/ComponentWithNoDependencies/Component.cs new file mode 100644 index 0000000..e6b49c6 --- /dev/null +++ b/src/installer/test/Assets/TestProjects/ComponentWithNoDependencies/Component.cs @@ -0,0 +1,6 @@ +namespace Component +{ + public class Component + { + } +} \ No newline at end of file diff --git a/src/installer/test/Assets/TestProjects/ComponentWithNoDependencies/ComponentWithNoDependencies.csproj b/src/installer/test/Assets/TestProjects/ComponentWithNoDependencies/ComponentWithNoDependencies.csproj new file mode 100644 index 0000000..07612ef --- /dev/null +++ b/src/installer/test/Assets/TestProjects/ComponentWithNoDependencies/ComponentWithNoDependencies.csproj @@ -0,0 +1,8 @@ + + + + $(NETCoreAppFramework) + $(MNAVersion) + + + diff --git a/src/installer/test/Assets/TestProjects/ComponentWithResources/Component.cs b/src/installer/test/Assets/TestProjects/ComponentWithResources/Component.cs new file mode 100644 index 0000000..e6b49c6 --- /dev/null +++ b/src/installer/test/Assets/TestProjects/ComponentWithResources/Component.cs @@ -0,0 +1,6 @@ +namespace Component +{ + public class Component + { + } +} \ No newline at end of file diff --git a/src/installer/test/Assets/TestProjects/ComponentWithResources/ComponentWithResources.csproj b/src/installer/test/Assets/TestProjects/ComponentWithResources/ComponentWithResources.csproj new file mode 100644 index 0000000..07612ef --- /dev/null +++ b/src/installer/test/Assets/TestProjects/ComponentWithResources/ComponentWithResources.csproj @@ -0,0 +1,8 @@ + + + + $(NETCoreAppFramework) + $(MNAVersion) + + + diff --git a/src/installer/test/Assets/TestProjects/ComponentWithResources/Resource.en.resx b/src/installer/test/Assets/TestProjects/ComponentWithResources/Resource.en.resx new file mode 100644 index 0000000..cd32d60 --- /dev/null +++ b/src/installer/test/Assets/TestProjects/ComponentWithResources/Resource.en.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + English + + \ No newline at end of file diff --git a/src/installer/test/Assets/TestProjects/ComponentWithResources/Resource.resx b/src/installer/test/Assets/TestProjects/ComponentWithResources/Resource.resx new file mode 100644 index 0000000..af54d18 --- /dev/null +++ b/src/installer/test/Assets/TestProjects/ComponentWithResources/Resource.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Neutral + + \ No newline at end of file diff --git a/src/installer/test/Assets/TestProjects/HostApiInvokerApp/HostPolicy.cs b/src/installer/test/Assets/TestProjects/HostApiInvokerApp/HostPolicy.cs new file mode 100644 index 0000000..187ddfd --- /dev/null +++ b/src/installer/test/Assets/TestProjects/HostApiInvokerApp/HostPolicy.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace HostApiInvokerApp +{ + public static class HostPolicy + { + internal static class hostpolicy + { + [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = Utils.OSCharSet)] + internal delegate void corehost_resolve_component_dependencies_result_fn( + string assembly_paths, + string native_search_paths, + string resource_search_paths); + + [DllImport(nameof(hostpolicy), CharSet = Utils.OSCharSet, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + internal static extern int corehost_resolve_component_dependencies( + string component_main_assembly_path, + corehost_resolve_component_dependencies_result_fn result); + } + + static void Test_corehost_resolve_component_dependencies(string[] args) + { + if (args.Length != 2) + { + throw new ArgumentException("Invalid number of arguments passed"); + } + + string assemblies = null; + string nativeSearchPaths = null; + string resourceSearcPaths = null; + int rc = hostpolicy.corehost_resolve_component_dependencies( + args[1], + (assembly_paths, native_search_paths, resource_search_paths) => + { + assemblies = assembly_paths; + nativeSearchPaths = native_search_paths; + resourceSearcPaths = resource_search_paths; + }); + + if (assemblies != null) + { + // Sort the assemblies since in the native code we store it in a hash table + // which gives random order. The native code always adds the separator at the end + // so mimic that behavior as well. + assemblies = string.Join(System.IO.Path.PathSeparator, assemblies.Split(System.IO.Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries).OrderBy(a => a)) + System.IO.Path.PathSeparator; + } + + if (rc == 0) + { + Console.WriteLine("corehost_resolve_component_dependencies:Success"); + Console.WriteLine($"corehost_resolve_component_dependencies assemblies:[{assemblies}]"); + Console.WriteLine($"corehost_resolve_component_dependencies native_search_paths:[{nativeSearchPaths}]"); + Console.WriteLine($"corehost_resolve_component_dependencies resource_search_paths:[{resourceSearcPaths}]"); + } + else + { + Console.WriteLine($"corehost_resolve_component_dependencies:Fail[0x{rc.ToString("X8")}]"); + } + } + + public static bool RunTest(string apiToTest, string[] args) + { + switch (apiToTest) + { + case nameof(hostpolicy.corehost_resolve_component_dependencies): + Test_corehost_resolve_component_dependencies(args); + break; + default: + return false; + } + + Utils.LogModulePath("hostpolicy"); + + return true; + } + } +} \ No newline at end of file diff --git a/src/installer/test/Assets/TestProjects/HostApiInvokerApp/Program.cs b/src/installer/test/Assets/TestProjects/HostApiInvokerApp/Program.cs index df20970..d3d6189 100644 --- a/src/installer/test/Assets/TestProjects/HostApiInvokerApp/Program.cs +++ b/src/installer/test/Assets/TestProjects/HostApiInvokerApp/Program.cs @@ -62,6 +62,10 @@ namespace HostApiInvokerApp { return; } + else if (HostPolicy.RunTest(apiToTest, args)) + { + return; + } else { throw new ArgumentException($"Invalid API to test passed as args[0]): {apiToTest}"); diff --git a/src/installer/test/HostActivationTests/GivenThatICareAboutComponentDependencyResolution.cs b/src/installer/test/HostActivationTests/GivenThatICareAboutComponentDependencyResolution.cs new file mode 100644 index 0000000..7d33133 --- /dev/null +++ b/src/installer/test/HostActivationTests/GivenThatICareAboutComponentDependencyResolution.cs @@ -0,0 +1,432 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.NativeHostApis +{ + public class GivenThatICareAboutComponentDependencyResolution : IClassFixture + { + private SharedTestState sharedTestState; + private readonly ITestOutputHelper output; + + public GivenThatICareAboutComponentDependencyResolution(SharedTestState fixture, ITestOutputHelper output) + { + sharedTestState = fixture; + this.output = output; + } + + private const string corehost_resolve_component_dependencies = "corehost_resolve_component_dependencies"; + + [Fact] + public void InvalidMainComponentAssemblyPathFails() + { + var fixture = sharedTestState.PreviouslyPublishedAndRestoredPortableApiTestProjectFixture.Copy(); + + string[] args = + { + corehost_resolve_component_dependencies, + fixture.TestProject.AppDll + "_invalid" + }; + fixture.BuiltDotnet.Exec(fixture.TestProject.AppDll, args) + .CaptureStdOut().CaptureStdErr().EnvironmentVariable("COREHOST_TRACE", "1") + .Execute() + .StdErrAfter("corehost_resolve_component_dependencies = {") + .Should().Pass() + .And.HaveStdOutContaining("corehost_resolve_component_dependencies:Fail[0x80008092]") + .And.HaveStdErrContaining("Failed to locate managed application"); + } + + [Fact] + public void ComponentWithNoDependenciesAndNoDeps() + { + var fixture = sharedTestState.PreviouslyPublishedAndRestoredPortableApiTestProjectFixture.Copy(); + var componentFixture = sharedTestState.PreviouslyPublishedAndRestoredComponentWithNoDependenciesFixture.Copy(); + + // Remove .deps.json + File.Delete(componentFixture.TestProject.DepsJson); + + string[] args = + { + corehost_resolve_component_dependencies, + componentFixture.TestProject.AppDll + }; + fixture.BuiltDotnet.Exec(fixture.TestProject.AppDll, args) + .CaptureStdOut().CaptureStdErr().EnvironmentVariable("COREHOST_TRACE", "1") + .Execute() + .StdErrAfter("corehost_resolve_component_dependencies = {") + .Should().Pass() + .And.HaveStdOutContaining("corehost_resolve_component_dependencies:Success") + .And.HaveStdOutContaining($"corehost_resolve_component_dependencies assemblies:[{componentFixture.TestProject.AppDll}{Path.PathSeparator}]") + .And.HaveStdErrContaining($"app_root='{componentFixture.TestProject.OutputDirectory}{Path.DirectorySeparatorChar}'") + .And.HaveStdErrContaining($"deps='{componentFixture.TestProject.DepsJson}'") + .And.HaveStdErrContaining($"mgd_app='{componentFixture.TestProject.AppDll}'") + .And.HaveStdErrContaining($"-- arguments_t: dotnet shared store: '{Path.Combine(fixture.BuiltDotnet.BinPath, "store", sharedTestState.RepoDirectories.BuildArchitecture, fixture.Framework)}'"); + } + + [Fact] + public void ComponentWithNoDependencies() + { + var fixture = sharedTestState.PreviouslyPublishedAndRestoredPortableApiTestProjectFixture.Copy(); + var componentFixture = sharedTestState.PreviouslyPublishedAndRestoredComponentWithNoDependenciesFixture.Copy(); + + string[] args = + { + corehost_resolve_component_dependencies, + componentFixture.TestProject.AppDll + }; + fixture.BuiltDotnet.Exec(fixture.TestProject.AppDll, args) + .CaptureStdOut().CaptureStdErr().EnvironmentVariable("COREHOST_TRACE", "1") + .Execute() + .StdErrAfter("corehost_resolve_component_dependencies = {") + .Should().Pass() + .And.HaveStdOutContaining("corehost_resolve_component_dependencies:Success") + .And.HaveStdOutContaining($"corehost_resolve_component_dependencies assemblies:[{componentFixture.TestProject.AppDll}{Path.PathSeparator}]"); + } + + private static readonly string[] SupportedOsList = new string[] + { + "ubuntu", + "debian", + "fedora", + "opensuse", + "osx", + "rhel", + "win" + }; + + private string GetExpectedLibuvRid(TestProjectFixture fixture) + { + // Simplified version of the RID fallback for libuv + // Note that we have to take the architecture from the fixture (since this test may run on x64 but the fixture on x86) + // but we can't use the OS part from the fixture RID as that may be too generic (like linux-x64). + string currentRid = PlatformAbstractions.RuntimeEnvironment.GetRuntimeIdentifier(); + string fixtureRid = fixture.CurrentRid; + string osName = currentRid.Split('-')[0]; + string architecture = fixtureRid.Split('-')[1]; + + string supportedOsName = SupportedOsList.FirstOrDefault(a => osName.StartsWith(a)); + if (supportedOsName == null) + { + return null; + } + + osName = supportedOsName; + if (osName == "ubuntu") { osName = "debian"; } + if (osName == "win") { osName = "win7"; } + if (osName == "osx") { return osName; } + + return osName + "-" + architecture; + } + + [Fact] + public void ComponentWithDependencies() + { + var fixture = sharedTestState.PreviouslyPublishedAndRestoredPortableApiTestProjectFixture.Copy(); + var componentFixture = sharedTestState.PreviouslyPublishedAndRestoredComponentWithDependenciesFixture.Copy(); + + string libuvRid = GetExpectedLibuvRid(fixture); + if (libuvRid == null) + { + output.WriteLine($"RID {PlatformAbstractions.RuntimeEnvironment.GetRuntimeIdentifier()} is not supported by libuv and thus we can't run this test on it."); + return; + } + + string[] args = + { + corehost_resolve_component_dependencies, + componentFixture.TestProject.AppDll + }; + fixture.BuiltDotnet.Exec(fixture.TestProject.AppDll, args) + .CaptureStdOut().CaptureStdErr().EnvironmentVariable("COREHOST_TRACE", "1") + .Execute() + .StdErrAfter("corehost_resolve_component_dependencies = {") + .Should().Pass() + .And.HaveStdOutContaining("corehost_resolve_component_dependencies:Success") + .And.HaveStdOutContaining( + $"corehost_resolve_component_dependencies assemblies:[" + + $"{Path.Combine(componentFixture.TestProject.OutputDirectory, "ComponentDependency.dll")}{Path.PathSeparator}" + + $"{componentFixture.TestProject.AppDll}{Path.PathSeparator}" + + $"{Path.Combine(componentFixture.TestProject.OutputDirectory, "Newtonsoft.Json.dll")}{Path.PathSeparator}]") + .And.HaveStdOutContaining( + $"corehost_resolve_component_dependencies native_search_paths:[" + + $"{ExpectedProbingPaths(Path.Combine(componentFixture.TestProject.OutputDirectory, "runtimes", libuvRid, "native"))}]"); + } + + [Fact] + public void ComponentWithDependenciesAndDependencyRemoved() + { + var fixture = sharedTestState.PreviouslyPublishedAndRestoredPortableApiTestProjectFixture.Copy(); + var componentFixture = sharedTestState.PreviouslyPublishedAndRestoredComponentWithDependenciesFixture.Copy(); + + // Remove a dependency + // This will cause the resolution to fail + File.Delete(Path.Combine(componentFixture.TestProject.OutputDirectory, "ComponentDependency.dll")); + + string[] args = + { + corehost_resolve_component_dependencies, + componentFixture.TestProject.AppDll + }; + fixture.BuiltDotnet.Exec(fixture.TestProject.AppDll, args) + .CaptureStdOut().CaptureStdErr().EnvironmentVariable("COREHOST_TRACE", "1") + .Execute() + .StdErrAfter("corehost_resolve_component_dependencies = {") + .Should().Pass() + .And.HaveStdOutContaining("corehost_resolve_component_dependencies:Fail[0x8000808C]") + .And.HaveStdErrContaining("An assembly specified in the application dependencies manifest (ComponentWithDependencies.deps.json) was not found:"); + } + + [Fact] + public void ComponentWithDependenciesAndNoDeps() + { + var fixture = sharedTestState.PreviouslyPublishedAndRestoredPortableApiTestProjectFixture.Copy(); + var componentFixture = sharedTestState.PreviouslyPublishedAndRestoredComponentWithDependenciesFixture.Copy(); + + // Remove .deps.json + File.Delete(componentFixture.TestProject.DepsJson); + + string[] args = + { + corehost_resolve_component_dependencies, + componentFixture.TestProject.AppDll + }; + fixture.BuiltDotnet.Exec(fixture.TestProject.AppDll, args) + .CaptureStdOut().CaptureStdErr().EnvironmentVariable("COREHOST_TRACE", "1") + .Execute() + .StdErrAfter("corehost_resolve_component_dependencies = {") + .Should().Pass() + .And.HaveStdOutContaining("corehost_resolve_component_dependencies:Success") + .And.HaveStdOutContaining( + $"corehost_resolve_component_dependencies assemblies:[" + + $"{Path.Combine(componentFixture.TestProject.OutputDirectory, "ComponentDependency.dll")}{Path.PathSeparator}" + + $"{componentFixture.TestProject.AppDll}{Path.PathSeparator}" + + $"{Path.Combine(componentFixture.TestProject.OutputDirectory, "Newtonsoft.Json.dll")}{Path.PathSeparator}]"); + } + + [Fact] + public void ComponentWithDependenciesAndNoDepsAndDependencyRemoved() + { + var fixture = sharedTestState.PreviouslyPublishedAndRestoredPortableApiTestProjectFixture.Copy(); + var componentFixture = sharedTestState.PreviouslyPublishedAndRestoredComponentWithDependenciesFixture.Copy(); + + // Remove .deps.json + File.Delete(componentFixture.TestProject.DepsJson); + + // Remove a dependency + // Since there's no .deps.json - there's no way for the system to know about this dependency and thus should not be reported. + File.Delete(Path.Combine(componentFixture.TestProject.OutputDirectory, "ComponentDependency.dll")); + + string[] args = + { + corehost_resolve_component_dependencies, + componentFixture.TestProject.AppDll + }; + fixture.BuiltDotnet.Exec(fixture.TestProject.AppDll, args) + .CaptureStdOut().CaptureStdErr().EnvironmentVariable("COREHOST_TRACE", "1") + .Execute() + .StdErrAfter("corehost_resolve_component_dependencies = {") + .Should().Pass() + .And.HaveStdOutContaining("corehost_resolve_component_dependencies:Success") + .And.HaveStdOutContaining( + $"corehost_resolve_component_dependencies assemblies:[" + + $"{componentFixture.TestProject.AppDll}{Path.PathSeparator}" + + $"{Path.Combine(componentFixture.TestProject.OutputDirectory, "Newtonsoft.Json.dll")}{Path.PathSeparator}]"); + } + + [Fact] + public void ComponentWithSameDependencyWithDifferentExtensionShouldFail() + { + var fixture = sharedTestState.PreviouslyPublishedAndRestoredPortableApiTestProjectFixture.Copy(); + var componentFixture = sharedTestState.PreviouslyPublishedAndRestoredComponentWithDependenciesFixture.Copy(); + + // Add a reference to another package which has asset with the same name as the existing ComponentDependency + // but with a different extension. This causes a failure. + SharedFramework.AddReferenceToDepsJson( + componentFixture.TestProject.DepsJson, + "ComponentWithDependencies/1.0.0", + "ComponentDependency_Dupe", + "1.0.0", + testAssembly: "ComponentDependency.notdll"); + + // Make sure the file exists so that we avoid failing due to missing file. + File.Copy( + Path.Combine(componentFixture.TestProject.OutputDirectory, "ComponentDependency.dll"), + Path.Combine(componentFixture.TestProject.OutputDirectory, "ComponentDependency.notdll")); + + string[] args = + { + corehost_resolve_component_dependencies, + componentFixture.TestProject.AppDll + }; + fixture.BuiltDotnet.Exec(fixture.TestProject.AppDll, args) + .CaptureStdOut().CaptureStdErr().EnvironmentVariable("COREHOST_TRACE", "1") + .Execute() + .StdErrAfter("corehost_resolve_component_dependencies = {") + .Should().Pass() + .And.HaveStdOutContaining("corehost_resolve_component_dependencies:Fail[0x8000808C]") + .And.HaveStdErrContaining("An assembly specified in the application dependencies manifest (ComponentWithDependencies.deps.json) has already been found but with a different file extension") + .And.HaveStdErrContaining("package: 'ComponentDependency_Dupe', version: '1.0.0'") + .And.HaveStdErrContaining("path: 'ComponentDependency.notdll'") + .And.HaveStdErrContaining($"previously found assembly: '{Path.Combine(componentFixture.TestProject.OutputDirectory, "ComponentDependency.dll")}'"); + } + + [Fact] + public void ComponentWithCorruptedDepsJsonShouldFail() + { + var fixture = sharedTestState.PreviouslyPublishedAndRestoredPortableApiTestProjectFixture.Copy(); + var componentFixture = sharedTestState.PreviouslyPublishedAndRestoredComponentWithDependenciesFixture.Copy(); + + // Corrupt the .deps.json by appending } to it (malformed json) + File.WriteAllText( + componentFixture.TestProject.DepsJson, + File.ReadAllLines(componentFixture.TestProject.DepsJson) + "}"); + + string[] args = + { + corehost_resolve_component_dependencies, + componentFixture.TestProject.AppDll + }; + fixture.BuiltDotnet.Exec(fixture.TestProject.AppDll, args) + .CaptureStdOut().CaptureStdErr().EnvironmentVariable("COREHOST_TRACE", "1") + .Execute() + .StdErrAfter("corehost_resolve_component_dependencies = {") + .Should().Pass() + .And.HaveStdOutContaining("corehost_resolve_component_dependencies:Fail[0x8000808B]") + .And.HaveStdErrContaining($"A JSON parsing exception occurred in [{componentFixture.TestProject.DepsJson}]: * Line 1, Column 2 Syntax error: Malformed token") + .And.HaveStdErrContaining($"Error initializing the dependency resolver: An error occurred while parsing: {componentFixture.TestProject.DepsJson}"); + } + + [Fact] + public void ComponentWithResourcesShouldReportResourceSearchPaths() + { + var fixture = sharedTestState.PreviouslyPublishedAndRestoredPortableApiTestProjectFixture.Copy(); + var componentFixture = sharedTestState.PreviouslyPublishedAndRestoredComponentWithResourcesFixture.Copy(); + + string[] args = + { + corehost_resolve_component_dependencies, + componentFixture.TestProject.AppDll + }; + fixture.BuiltDotnet.Exec(fixture.TestProject.AppDll, args) + .CaptureStdOut().CaptureStdErr().EnvironmentVariable("COREHOST_TRACE", "1") + .Execute() + .StdErrAfter("corehost_resolve_component_dependencies = {") + .Should().Pass() + .And.HaveStdOutContaining("corehost_resolve_component_dependencies:Success") + .And.HaveStdOutContaining($"corehost_resolve_component_dependencies resource_search_paths:[" + + $"{ExpectedProbingPaths(componentFixture.TestProject.OutputDirectory)}]"); + } + + private string ExpectedProbingPaths(params string[] paths) + { + string result = string.Empty; + foreach (string path in paths) + { + string expectedPath = path; + if (expectedPath.EndsWith(Path.DirectorySeparatorChar)) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // On non-windows the paths are normalized to not end with a / + expectedPath = expectedPath.Substring(0, expectedPath.Length - 1); + } + } + else + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // On windows all paths are normalized to end with a \ + expectedPath = expectedPath + Path.DirectorySeparatorChar; + } + } + + result += expectedPath + Path.PathSeparator; + } + + return result; + } + + [Fact] + public void AdditionalDepsDontAffectComponentDependencyResolution() + { + var fixture = sharedTestState.PreviouslyPublishedAndRestoredPortableApiTestProjectFixture.Copy(); + var componentFixture = sharedTestState.PreviouslyPublishedAndRestoredComponentWithNoDependenciesFixture.Copy(); + + string additionalDepsPath = Path.Combine(Path.GetDirectoryName(fixture.TestProject.DepsJson), "__duplicate.deps.json"); + File.Copy(fixture.TestProject.DepsJson, additionalDepsPath); + + string[] args = + { + corehost_resolve_component_dependencies, + componentFixture.TestProject.AppDll + }; + fixture.BuiltDotnet.Exec(fixture.TestProject.AppDll, args) + .CaptureStdOut().CaptureStdErr().EnvironmentVariable("COREHOST_TRACE", "1").EnvironmentVariable("DOTNET_ADDITIONAL_DEPS", additionalDepsPath) + .Execute() + .StdErrAfter("corehost_resolve_component_dependencies = {") + .Should().Pass() + .And.HaveStdOutContaining("corehost_resolve_component_dependencies:Success") + .And.HaveStdOutContaining($"corehost_resolve_component_dependencies assemblies:[{componentFixture.TestProject.AppDll}{Path.PathSeparator}]"); + } + + + public class SharedTestState : IDisposable + { + public TestProjectFixture PreviouslyPublishedAndRestoredPortableApiTestProjectFixture { get; set; } + public TestProjectFixture PreviouslyPublishedAndRestoredComponentWithNoDependenciesFixture { get; set; } + public TestProjectFixture PreviouslyPublishedAndRestoredComponentWithDependenciesFixture { get; set; } + public TestProjectFixture PreviouslyPublishedAndRestoredComponentWithResourcesFixture { get; set; } + public RepoDirectoriesProvider RepoDirectories { get; set; } + + public string BreadcrumbLocation { get; set; } + + public SharedTestState() + { + RepoDirectories = new RepoDirectoriesProvider(); + + PreviouslyPublishedAndRestoredPortableApiTestProjectFixture = new TestProjectFixture("HostApiInvokerApp", RepoDirectories) + .EnsureRestored(RepoDirectories.CorehostPackages) + .BuildProject(); + + PreviouslyPublishedAndRestoredComponentWithNoDependenciesFixture = new TestProjectFixture("ComponentWithNoDependencies", RepoDirectories) + .EnsureRestored(RepoDirectories.CorehostPackages) + .PublishProject(); + + PreviouslyPublishedAndRestoredComponentWithDependenciesFixture = new TestProjectFixture("ComponentWithDependencies", RepoDirectories) + .EnsureRestored(RepoDirectories.CorehostPackages) + .PublishProject(); + + PreviouslyPublishedAndRestoredComponentWithResourcesFixture = new TestProjectFixture("ComponentWithResources", RepoDirectories) + .EnsureRestored(RepoDirectories.CorehostPackages) + .PublishProject(); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // On non-Windows, we can't just P/Invoke to already loaded hostpolicy, so copy it next to the app dll. + var fixture = PreviouslyPublishedAndRestoredPortableApiTestProjectFixture; + var hostpolicy = Path.Combine( + fixture.BuiltDotnet.GreatestVersionSharedFxPath, + $"{fixture.SharedLibraryPrefix}hostpolicy{fixture.SharedLibraryExtension}"); + + File.Copy( + hostpolicy, + Path.GetDirectoryName(fixture.TestProject.AppDll)); + } + } + + public void Dispose() + { + PreviouslyPublishedAndRestoredPortableApiTestProjectFixture.Dispose(); + PreviouslyPublishedAndRestoredComponentWithNoDependenciesFixture.Dispose(); + PreviouslyPublishedAndRestoredComponentWithDependenciesFixture.Dispose(); + PreviouslyPublishedAndRestoredComponentWithResourcesFixture.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/src/installer/test/HostActivationTests/HostActivationTests.csproj b/src/installer/test/HostActivationTests/HostActivationTests.csproj index b798dba..4eaba0a 100644 --- a/src/installer/test/HostActivationTests/HostActivationTests.csproj +++ b/src/installer/test/HostActivationTests/HostActivationTests.csproj @@ -17,6 +17,7 @@ + diff --git a/src/installer/test/HostActivationTests/SharedFramework.cs b/src/installer/test/HostActivationTests/SharedFramework.cs index 539efef..cdf320c 100644 --- a/src/installer/test/HostActivationTests/SharedFramework.cs +++ b/src/installer/test/HostActivationTests/SharedFramework.cs @@ -272,18 +272,24 @@ namespace Microsoft.DotNet.CoreSetup.Test return depsjson; } - public static void AddReferenceToDepsJson(string jsonFile, string fxNamewWithVersion, string testPackage, string testPackageVersion, JObject testAssemblyVersionInfo = null) + public static void AddReferenceToDepsJson( + string jsonFile, + string fxNameWithVersion, + string testPackage, + string testPackageVersion, + JObject testAssemblyVersionInfo = null, + string testAssembly = null) { JObject depsjson = JObject.Parse(File.ReadAllText(jsonFile)); string testPackageWithVersion = testPackage + "/" + testPackageVersion; - string testAssembly = testPackage + ".dll"; + testAssembly = testAssembly ?? (testPackage + ".dll"); JProperty targetsProperty = (JProperty)depsjson["targets"].First; JObject targetsValue = (JObject)targetsProperty.Value; var assembly = new JProperty(testPackage, testPackageVersion); - JObject packageDependencies = (JObject)targetsValue[fxNamewWithVersion]["dependencies"]; + JObject packageDependencies = (JObject)targetsValue[fxNameWithVersion]["dependencies"]; packageDependencies.Add(assembly); if (testAssemblyVersionInfo == null) diff --git a/src/installer/test/TestUtils/Assertions/CommandResultExtensions.cs b/src/installer/test/TestUtils/Assertions/CommandResultExtensions.cs index fc5ce05..6c7c158 100644 --- a/src/installer/test/TestUtils/Assertions/CommandResultExtensions.cs +++ b/src/installer/test/TestUtils/Assertions/CommandResultExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using FluentAssertions; using Microsoft.DotNet.Cli.Build.Framework; namespace Microsoft.DotNet.CoreSetup.Test @@ -11,5 +12,14 @@ namespace Microsoft.DotNet.CoreSetup.Test { return new CommandResultAssertions(commandResult); } + + public static CommandResult StdErrAfter(this CommandResult commandResult, string pattern) + { + int i = commandResult.StdErr.IndexOf(pattern); + i.Should().BeGreaterOrEqualTo(0, "Trying to filter StdErr after '{0}', but such string can't be found in the StdErr.", pattern); + string filteredStdErr = commandResult.StdErr.Substring(i); + + return new CommandResult(commandResult.StartInfo, commandResult.ExitCode, commandResult.StdOut, filteredStdErr); + } } } -- 2.7.4