Support 'additionalFrameworks' section in app's runtimeconfig.json (dotnet/core-setup...
authorSteve Harter <steveharter@users.noreply.github.com>
Thu, 1 Mar 2018 00:53:11 +0000 (18:53 -0600)
committerGitHub <noreply@github.com>
Thu, 1 Mar 2018 00:53:11 +0000 (18:53 -0600)
Commit migrated from https://github.com/dotnet/core-setup/commit/a94c3251709071805f51fad5fb606d804ab3fad8

src/installer/corehost/cli/args.h
src/installer/corehost/cli/fx_definition.cpp
src/installer/corehost/cli/fx_definition.h
src/installer/corehost/cli/fxr/fx_muxer.cpp
src/installer/corehost/cli/runtime_config.cpp
src/installer/corehost/cli/runtime_config.h
src/installer/test/HostActivationTests/GivenThatICareAboutMultilevelSharedFxLookup.cs

index 49218fb..02316a6 100644 (file)
@@ -14,7 +14,6 @@ struct probe_config_t
 {
     pal::string_t probe_dir;
     bool patch_roll_fwd;
-    bool prerelease_roll_fwd;
     const deps_json_t* probe_deps_json;
     int fx_level;
 
index 1a8274a..481d289 100644 (file)
@@ -26,10 +26,11 @@ fx_definition_t::fx_definition_t(
 void fx_definition_t::parse_runtime_config(
     const pal::string_t& path,
     const pal::string_t& dev_path,
-    const runtime_config_t* defaults
+    const runtime_config_t* higher_layer_config,
+    const runtime_config_t* app_config
 )
 {
-    m_runtime_config.parse(path, dev_path, defaults);
+    m_runtime_config.parse(path, dev_path, higher_layer_config, app_config);
 }
 
 void fx_definition_t::parse_deps()
index 29f7e8d..3819e0f 100644 (file)
@@ -24,7 +24,7 @@ public:
     const pal::string_t& get_found_version() const { return m_found_version; }
     const pal::string_t& get_dir() const { return m_dir; }
     const runtime_config_t& get_runtime_config() const { return m_runtime_config; }
-    void parse_runtime_config(const pal::string_t& path, const pal::string_t& dev_path, const runtime_config_t* defaults);
+    void parse_runtime_config(const pal::string_t& path, const pal::string_t& dev_path, const runtime_config_t* higher_layer_config, const runtime_config_t* app_config);
 
     const pal::string_t& get_deps_file() const { return m_deps_file; }
     void set_deps_file(const pal::string_t value) { m_deps_file = value; }
index 0002d7e..f767abd 100644 (file)
@@ -1098,7 +1098,7 @@ int read_config(
         get_runtime_config_paths_from_arg(runtime_config, &config_file, &dev_config_file);
     }
 
-    app.parse_runtime_config(config_file, dev_config_file, nullptr);
+    app.parse_runtime_config(config_file, dev_config_file, nullptr, nullptr);
     if (!app.get_runtime_config().is_valid())
     {
         trace::error(_X("Invalid runtimeconfig.json [%s] [%s]"), app.get_runtime_config().get_path().c_str(), app.get_runtime_config().get_dev_path().c_str());
@@ -1151,19 +1151,22 @@ int fx_muxer_t::read_config_and_execute(
         return rc;
     }
 
-    auto config = app->get_runtime_config();
+    auto app_config = app->get_runtime_config();
 
     // 'Roll forward on no candidate fx' is set to 1 (roll_fwd_on_no_candidate_fx_option::minor) by default. It can be changed through:
-    // 1. Command line argument (--roll-forward-on-no-candidate-fx). Only defaults the app's config.
-    // 2. Runtimeconfig json file ('rollForwardOnNoCandidateFx' property), which is used as a default for lower level frameworks if they don't specify a value.
-    // 3. DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX env var. Only defaults the app's config.
-    // The conflicts will be resolved by following the priority rank described above (from 1 to 3).
+    // 1. Command line argument (--roll-forward-on-no-candidate-fx).
+    // 2. Runtimeconfig json file ('rollForwardOnNoCandidateFx' property in "framework" section:).
+    // 3. Runtimeconfig json file ('rollForwardOnNoCandidateFx' property), which is used as a default for lower level frameworks if they don't specify a value.
+    // 4. DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX env var. Only defaults the app's config.
+    // The conflicts will be resolved by following the priority rank described above (from 1 to 4).
     // The env var condition is verified in the config file processing
     if (!roll_fwd_on_no_candidate_fx.empty())
     {
-        config.set_roll_fwd_on_no_candidate_fx(static_cast<roll_fwd_on_no_candidate_fx_option>(pal::xtoi(roll_fwd_on_no_candidate_fx.c_str())));
+        app_config.force_roll_fwd_on_no_candidate_fx(static_cast<roll_fwd_on_no_candidate_fx_option>(pal::xtoi(roll_fwd_on_no_candidate_fx.c_str())));
     }
 
+    auto config = app_config;
+
     // Determine additional deps
     pal::string_t additional_deps_serialized;
     bool is_portable = config.get_portable();
@@ -1193,7 +1196,7 @@ int fx_muxer_t::read_config_and_execute(
             pal::string_t config_file;
             pal::string_t dev_config_file;
             get_runtime_config_paths(fx->get_dir(), config.get_fx_name(), &config_file, &dev_config_file);
-            fx->parse_runtime_config(config_file, dev_config_file, &config);
+            fx->parse_runtime_config(config_file, dev_config_file, &config, &app_config);
 
             config = fx->get_runtime_config();
             if (!config.is_valid())
index 6ed5b94..020b4e2 100644 (file)
@@ -8,26 +8,36 @@
 #include "runtime_config.h"
 #include <cassert>
 
+
+// The semantics of applying the runtimeconfig.json values follows, in the following steps from
+// first to last, where last always wins. These steps are also annotated in the code here.
+// 1a) If the app, apply the default values from the environment
+// 1b) If the framework, apply the default values from the higher framework
+// 2) Apply the values in the current "framework" section; use these as defaults for current layer
+// 3) Apply the values in the app's "additionalFrameworks" section for the targeted framework
+// 4) Apply the readonly values which are the settings that can't be changed by lower layers
+
 runtime_config_t::runtime_config_t()
     : m_patch_roll_fwd(true)
-    , m_prerelease_roll_fwd(false)
     , m_roll_fwd_on_no_candidate_fx(roll_fwd_on_no_candidate_fx_option::minor)
     , m_portable(false)
     , m_valid(false)
 {
 }
 
-void runtime_config_t::parse(const pal::string_t& path, const pal::string_t& dev_path, const runtime_config_t* defaults)
+void runtime_config_t::parse(const pal::string_t& path, const pal::string_t& dev_path, const runtime_config_t* higher_layer_config, const runtime_config_t* app_config)
 {
     m_path = path;
     m_dev_path = dev_path;
 
-    // Apply the defaults from previous config.
-    if (defaults)
+    // Step #1: apply the defaults from the environment (for the app) or previous\higher layer (for a framework)
+    if (higher_layer_config != nullptr)
     {
-        m_patch_roll_fwd = defaults->m_patch_roll_fwd;
-        m_prerelease_roll_fwd = defaults->m_prerelease_roll_fwd;
-        m_roll_fwd_on_no_candidate_fx = defaults->m_roll_fwd_on_no_candidate_fx;
+        // Copy the previous defaults so we can default the next framework; these may be changed by the current fx
+        copy_framework_settings_to(higher_layer_config->m_fx_global, m_fx_global);
+
+        // Apply the defaults
+        set_effective_values(m_fx_global);
     }
     else
     {
@@ -37,15 +47,20 @@ void runtime_config_t::parse(const pal::string_t& path, const pal::string_t& dev
         if (pal::getenv(_X("DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX"), &env_no_candidate))
         {
             m_roll_fwd_on_no_candidate_fx = static_cast<roll_fwd_on_no_candidate_fx_option>(pal::xtoi(env_no_candidate.c_str()));
+            m_fx_global.set_roll_fwd_on_no_candidate_fx(m_roll_fwd_on_no_candidate_fx);
         }
     }
 
-    m_valid = ensure_parsed();
+    m_valid = ensure_parsed(app_config);
 
-    // Overwrite values that can't be changed from previous config.
-    if (defaults)
+    if (m_valid)
     {
-        m_tfm = defaults->m_tfm;
+        // Step #4: apply the readonly values
+        if (app_config != nullptr)
+        {
+            m_tfm = app_config->m_tfm;
+            set_effective_values(app_config->m_fx_readonly);
+        }
     }
 
     trace::verbose(_X("Runtime config [%s] is valid=[%d]"), path.c_str(), m_valid);
@@ -95,18 +110,14 @@ bool runtime_config_t::parse_opts(const json_value& opts)
     if (patch_roll_fwd != opts_obj.end())
     {
         m_patch_roll_fwd = patch_roll_fwd->second.as_bool();
-    }
-
-    auto prerelease_roll_fwd = opts_obj.find(_X("preReleaseRollForward"));
-    if (prerelease_roll_fwd != opts_obj.end())
-    {
-        m_prerelease_roll_fwd = prerelease_roll_fwd->second.as_bool();
+        m_fx_global.set_patch_roll_fwd(m_patch_roll_fwd);
     }
 
     auto roll_fwd_on_no_candidate_fx = opts_obj.find(_X("rollForwardOnNoCandidateFx"));
     if (roll_fwd_on_no_candidate_fx != opts_obj.end())
     {
         m_roll_fwd_on_no_candidate_fx = static_cast<roll_fwd_on_no_candidate_fx_option>(roll_fwd_on_no_candidate_fx->second.as_integer());
+        m_fx_global.set_roll_fwd_on_no_candidate_fx(m_roll_fwd_on_no_candidate_fx);
     }
 
     auto tfm = opts_obj.find(_X("tfm"));
@@ -115,6 +126,8 @@ bool runtime_config_t::parse_opts(const json_value& opts)
         m_tfm = tfm->second.as_string();
     }
 
+    // Step #2: apply the "framework" section
+
     auto framework =  opts_obj.find(_X("framework"));
     if (framework == opts_obj.end())
     {
@@ -124,8 +137,73 @@ bool runtime_config_t::parse_opts(const json_value& opts)
     m_portable = true;
 
     const auto& fx_obj = framework->second.as_object();
+
     m_fx_name = fx_obj.at(_X("name")).as_string();
-    m_fx_ver = fx_obj.at(_X("version")).as_string();
+
+    bool rc = parse_framework(fx_obj);
+    if (rc)
+    {
+        set_effective_values(m_fx);
+    }
+
+    return rc;
+}
+
+void runtime_config_t::set_effective_values(const runtime_config_framework_t& overrides)
+{
+    if (overrides.get_fx_ver() != nullptr)
+    {
+        m_fx_ver = *overrides.get_fx_ver();
+    }
+
+    if (overrides.get_roll_fwd_on_no_candidate_fx() != nullptr)
+    {
+        m_roll_fwd_on_no_candidate_fx = *overrides.get_roll_fwd_on_no_candidate_fx();
+    }
+
+    if (overrides.get_patch_roll_fwd() != nullptr)
+    {
+        m_patch_roll_fwd = *overrides.get_patch_roll_fwd();
+    }
+}
+
+/*static*/ void runtime_config_t::copy_framework_settings_to(const runtime_config_framework_t& from, runtime_config_framework_t& to)
+{
+    if (from.get_fx_ver() != nullptr)
+    {
+        to.set_fx_ver(*from.get_fx_ver());
+    }
+
+    if (from.get_roll_fwd_on_no_candidate_fx() != nullptr)
+    {
+        to.set_roll_fwd_on_no_candidate_fx(*from.get_roll_fwd_on_no_candidate_fx());
+    }
+
+    if (from.get_patch_roll_fwd() != nullptr)
+    {
+        to.set_patch_roll_fwd(*from.get_patch_roll_fwd());
+    }
+}
+
+bool runtime_config_t::parse_framework(const json_object& fx_obj)
+{
+    auto fx_ver = fx_obj.find(_X("version"));
+    if (fx_ver != fx_obj.end())
+    {
+        m_fx.set_fx_ver(fx_ver->second.as_string());
+    }
+
+    auto patch_roll_fwd = fx_obj.find(_X("applyPatches"));
+    if (patch_roll_fwd != fx_obj.end())
+    {
+        m_fx.set_patch_roll_fwd(patch_roll_fwd->second.as_bool());
+    }
+
+    auto roll_fwd_on_no_candidate_fx = fx_obj.find(_X("rollForwardOnNoCandidateFx"));
+    if (roll_fwd_on_no_candidate_fx != fx_obj.end())
+    {
+        m_fx.set_roll_fwd_on_no_candidate_fx(static_cast<roll_fwd_on_no_candidate_fx_option>(roll_fwd_on_no_candidate_fx->second.as_integer()));
+    }
 
     return true;
 }
@@ -174,7 +252,7 @@ bool runtime_config_t::ensure_dev_config_parsed()
     return true;
 }
 
-bool runtime_config_t::ensure_parsed()
+bool runtime_config_t::ensure_parsed(const runtime_config_t* app_config)
 {
     trace::verbose(_X("Attempting to read runtime config: %s"), m_path.c_str());
     if (!ensure_dev_config_parsed())
@@ -201,6 +279,7 @@ bool runtime_config_t::ensure_parsed()
         trace::verbose(_X("UTF-8 BOM skipped while reading [%s]"), m_path.c_str());
     }
 
+    bool rc = true;
     try
     {
         const auto root = json_value::parse(file);
@@ -208,7 +287,52 @@ bool runtime_config_t::ensure_parsed()
         const auto iter = json.find(_X("runtimeOptions"));
         if (iter != json.end())
         {
-            parse_opts(iter->second);
+            rc = parse_opts(iter->second);
+
+            if (rc)
+            {
+                if (app_config == nullptr)
+                {
+                    // If there is no app_config yet, then we are the app
+                    // Read the additionalFrameworks section so we can apply later when each framework's runtimeconfig is read
+                    const auto& opts_obj = iter->second.as_object();
+                    const auto iter = opts_obj.find(_X("additionalFrameworks"));
+                    if (iter != opts_obj.end())
+                    {
+                        const auto& additional_frameworks = iter->second.as_array();
+                        for (const auto& fx : additional_frameworks)
+                        {
+                            runtime_config_t fx_overrides;
+                            const auto& fx_obj = fx.as_object();
+                            fx_overrides.m_fx_name = fx_obj.at(_X("name")).as_string();
+                            if (fx_overrides.m_fx_name.length() == 0)
+                            {
+                                trace::verbose(_X("No framework name in additionalFrameworks section."));
+                                rc = false;
+                                break;
+                            }
+
+                            rc = fx_overrides.parse_framework(fx_obj);
+                            if (!rc)
+                            {
+                                break;
+                            }
+
+                            m_additional_frameworks[fx_overrides.m_fx_name] = fx_overrides.m_fx;
+                        }
+                    }
+
+                    // Follow through to step #3 in case the framework is also specified in the additionalFrameworks section
+                    app_config = this;
+                }
+
+                // Step #3: apply the values from "additionalFrameworks"
+                auto overrides = app_config->m_additional_frameworks.find(m_fx_name);
+                if (overrides != app_config->m_additional_frameworks.end())
+                {
+                    set_effective_values(overrides->second);
+                }
+            }
         }
     }
     catch (const std::exception& je)
@@ -218,7 +342,8 @@ bool runtime_config_t::ensure_parsed()
         trace::error(_X("A JSON parsing exception occurred in [%s]: %s"), m_path.c_str(), jes.c_str());
         return false;
     }
-    return true;
+
+    return rc;
 }
 
 const pal::string_t& runtime_config_t::get_tfm() const
@@ -245,22 +370,17 @@ bool runtime_config_t::get_patch_roll_fwd() const
     return m_patch_roll_fwd;
 }
 
-bool runtime_config_t::get_prerelease_roll_fwd() const
-{
-    assert(m_valid);
-    return m_prerelease_roll_fwd;
-}
-
 roll_fwd_on_no_candidate_fx_option runtime_config_t::get_roll_fwd_on_no_candidate_fx() const
 {
     assert(m_valid);
     return m_roll_fwd_on_no_candidate_fx;
 }
 
-void runtime_config_t::set_roll_fwd_on_no_candidate_fx(roll_fwd_on_no_candidate_fx_option value)
+void runtime_config_t::force_roll_fwd_on_no_candidate_fx(roll_fwd_on_no_candidate_fx_option value)
 {
     assert(m_valid);
     m_roll_fwd_on_no_candidate_fx = value;
+    m_fx_readonly.set_roll_fwd_on_no_candidate_fx(value);
 }
 
 bool runtime_config_t::get_portable() const
index 32aa258..3204717 100644 (file)
@@ -10,6 +10,7 @@
 #include "cpprest/json.h"
 
 typedef web::json::value json_value;
+typedef web::json::object json_object;
 
 enum class roll_fwd_on_no_candidate_fx_option
 {
@@ -18,45 +19,109 @@ enum class roll_fwd_on_no_candidate_fx_option
     major_or_minor
 };
 
+class runtime_config_framework_t
+{
+public:
+    // Uses a "nullable<T>" pattern until we add such a type
+
+    runtime_config_framework_t()
+        : has_fx_ver(false)
+        , has_roll_fwd_on_no_candidate_fx(false)
+        , has_patch_roll_fwd(false)
+        , fx_ver(_X(""))
+        , patch_roll_fwd(false)
+        , roll_fwd_on_no_candidate_fx((roll_fwd_on_no_candidate_fx_option)0)
+        { }
+
+    const pal::string_t* get_fx_ver() const
+    {
+        return (has_fx_ver ? &fx_ver : nullptr);
+    }
+    void set_fx_ver(pal::string_t value)
+    {
+        has_fx_ver = true;
+        fx_ver = value;
+    }
+
+    const bool* get_patch_roll_fwd() const
+    {
+        return (has_patch_roll_fwd ? &patch_roll_fwd : nullptr);
+    }
+    void set_patch_roll_fwd(bool value)
+    {
+        has_patch_roll_fwd = true;
+        patch_roll_fwd = value;
+    }
+
+    const roll_fwd_on_no_candidate_fx_option* get_roll_fwd_on_no_candidate_fx() const
+    {
+        return (has_roll_fwd_on_no_candidate_fx ? &roll_fwd_on_no_candidate_fx : nullptr);
+    }
+    void set_roll_fwd_on_no_candidate_fx(roll_fwd_on_no_candidate_fx_option value)
+    {
+        has_roll_fwd_on_no_candidate_fx = true;
+        roll_fwd_on_no_candidate_fx = value;
+    }
+
+private:
+    bool has_fx_ver;
+    bool has_patch_roll_fwd;
+    bool has_roll_fwd_on_no_candidate_fx;
+
+    pal::string_t fx_ver;
+    bool patch_roll_fwd;
+    roll_fwd_on_no_candidate_fx_option roll_fwd_on_no_candidate_fx;
+};
+
 class runtime_config_t
 {
 public:
     runtime_config_t();
-    void parse(const pal::string_t& path, const pal::string_t& dev_path, const runtime_config_t* defaults);
+    void parse(const pal::string_t& path, const pal::string_t& dev_path, const runtime_config_t* higher_layer_config, const runtime_config_t* app_config);
     bool is_valid() const { return m_valid; }
     const pal::string_t& get_path() const { return m_path; }
     const pal::string_t& get_dev_path() const { return m_dev_path; }
     const pal::string_t& get_fx_version() const;
-    void set_fx_version(const pal::string_t& value);
     const pal::string_t& get_fx_name() const;
     const pal::string_t& get_tfm() const;
     const std::list<pal::string_t>& get_probe_paths() const;
     bool get_patch_roll_fwd() const;
-    bool get_prerelease_roll_fwd() const;
     roll_fwd_on_no_candidate_fx_option get_roll_fwd_on_no_candidate_fx() const;
-    void set_roll_fwd_on_no_candidate_fx(roll_fwd_on_no_candidate_fx_option value);
+    void force_roll_fwd_on_no_candidate_fx(roll_fwd_on_no_candidate_fx_option value);
     bool get_portable() const;
     bool parse_opts(const json_value& opts);
     void combine_properties(std::unordered_map<pal::string_t, pal::string_t>& combined_properties) const;
 
 private:
-    bool ensure_parsed();
+    bool ensure_parsed(const runtime_config_t* defaults);
     bool ensure_dev_config_parsed();
 
     std::unordered_map<pal::string_t, pal::string_t> m_properties;
+    std::unordered_map<pal::string_t, runtime_config_framework_t> m_additional_frameworks;
+    runtime_config_framework_t m_fx_global;     // the settings that will be applied to the next lower layer; does not include version (Step #1)
+    runtime_config_framework_t m_fx;            // the settings in the current "framework" section (Step #3)
+    runtime_config_framework_t m_fx_readonly;   // the settings that can't be changed by lower layers (Step #4)
     std::vector<std::string> m_prop_keys;
     std::vector<std::string> m_prop_values;
     std::list<pal::string_t> m_probe_paths;
+
     pal::string_t m_tfm;
     pal::string_t m_fx_name;
+
+    // These are the effective settings
     pal::string_t m_fx_ver;
     bool m_patch_roll_fwd;
-    bool m_prerelease_roll_fwd;
     roll_fwd_on_no_candidate_fx_option m_roll_fwd_on_no_candidate_fx;
 
     pal::string_t m_dev_path;
     pal::string_t m_path;
     bool m_portable;
     bool m_valid;
+
+private:
+    bool parse_framework(const json_object& fx_obj);
+    void set_effective_values(const runtime_config_framework_t& overrides);
+    static void copy_framework_settings_to(const runtime_config_framework_t& from, runtime_config_framework_t& to);
+
 };
 #endif // __RUNTIME_CONFIG_H__
\ No newline at end of file
index 63f254b..5799695 100644 (file)
@@ -716,7 +716,7 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSharedFxLooku
         }
 
         [Fact]
-        public void Multiple_SharedFxLookup_Propagated_RuntimeConfig_Values()
+        public void Multiple_SharedFxLookup_Propagated_Global_RuntimeConfig_Values()
         {
             var fixture = PreviouslyBuiltAndRestoredPortableTestProjectFixture
                 .Copy();
@@ -806,7 +806,7 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSharedFxLooku
         }
 
         [Fact]
-        public void Multiple_SharedFxLookup_UberFx_Deps_Overrides_NetCoreApp()
+        public void Multiple_SharedFxLookup_Propagated_Additional_Framework_RuntimeConfig_Values()
         {
             var fixture = PreviouslyBuiltAndRestoredPortableTestProjectFixture
                 .Copy();
@@ -815,19 +815,21 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSharedFxLooku
             var appDll = fixture.TestProject.AppDll;
 
             string runtimeConfig = Path.Combine(fixture.TestProject.OutputDirectory, "SharedFxLookupPortableApp.runtimeconfig.json");
-            SetRuntimeConfigJson(runtimeConfig, "7777.0.0", null, useUberFramework: true);
+
+            var additionalfxs = new JArray();
+            additionalfxs.Add(GetAdditionalFramework("Microsoft.NETCore.App", "9999.1.0", applyPatches: false, rollForwardOnNoCandidateFx: 0));
+            SetRuntimeConfigJson(runtimeConfig, "7777.0.0", null, useUberFramework: true, additionalFrameworks : additionalfxs);
 
             // Add versions in the exe folders
-            AddAvailableSharedFxVersions(_exeSharedFxBaseDir, "9999.0.0");
-            AddAvailableSharedUberFxVersions(_exeSharedUberFxBaseDir, "9999.0.0", null, "7777.0.0");
+            AddAvailableSharedFxVersions(_exeSharedFxBaseDir, "9999.1.0");
+            AddAvailableSharedUberFxVersions(_exeSharedUberFxBaseDir, "9999.5.5", "UberValue", "7777.0.0");
 
-            // The System.Collections.Immutable.dll is located in the UberFramework and NetCoreApp
-            // The System.Collections.dll is only located in NetCoreApp
-            // Version: NetCoreApp 9999.0.0
+            // Version: NetCoreApp 9999.5.5 (in framework section)
+            //          NetCoreApp 9999.1.0 (in app's additionalFrameworks section)
             //          UberFramework 7777.0.0
-            // Exe: NetCoreApp 9999.0.0
+            // Exe: NetCoreApp 9999.1.0
             //      UberFramework 7777.0.0
-            // Expected: 9999.0.0
+            // Expected: 9999.1.0
             //           7777.0.0
             dotnet.Exec(appDll)
                 .WorkingDirectory(_currentWorkingDir)
@@ -838,13 +840,59 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSharedFxLooku
                 .Should()
                 .Pass()
                 .And
-                .HaveStdErrContaining(Path.Combine("7777.0.0", "System.Collections.Immutable.dll"))
+                .HaveStdErrContaining(Path.Combine(_exeSelectedMessage, "9999.1.0"))
+                .And
+                .HaveStdErrContaining(Path.Combine(_exeFoundUberFxMessage, "7777.0.0"));
+
+            // Change the additionalFrameworks to allow roll forward, overriding Uber's global section and ignoring Uber's framework section
+            additionalfxs.Clear();
+            additionalfxs.Add(GetAdditionalFramework("Microsoft.NETCore.App", "9999.0.0", applyPatches: false, rollForwardOnNoCandidateFx: 1));
+            additionalfxs.Add(GetAdditionalFramework("UberFx", "7777.0.0", applyPatches: false, rollForwardOnNoCandidateFx: 0));
+            SetRuntimeConfigJson(runtimeConfig, "7777.0.0", rollFwdOnNoCandidateFx:0, useUberFramework: true, additionalFrameworks: additionalfxs);
+
+            // Version: NetCoreApp 9999.5.5 (in framework section)
+            //          NetCoreApp 9999.0.0 (in app's additionalFrameworks section)
+            //          UberFramework 7777.0.0
+            //          UberFramework 7777.0.0 (in app's additionalFrameworks section)
+            // 'Roll forward on no candidate fx' disabled through env var
+            // 'Roll forward on no candidate fx' disabled through Uber's global runtimeconfig
+            // 'Roll forward on no candidate fx' enabled for NETCore.App enabled through additionalFrameworks section
+            // Exe: NetCoreApp 9999.1.0
+            //      UberFramework 7777.0.0
+            // Expected: 9999.1.0
+            //           7777.0.0
+            dotnet.Exec(appDll)
+                .WorkingDirectory(_currentWorkingDir)
+                .EnvironmentVariable("COREHOST_TRACE", "1")
+                .EnvironmentVariable("DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX", "0")
+                .CaptureStdOut()
+                .CaptureStdErr()
+                .Execute()
+                .Should()
+                .Pass()
                 .And
-                .HaveStdErrContaining(Path.Combine("9999.0.0", "System.Collections.dll"))
+                .HaveStdErrContaining(Path.Combine(_exeSelectedMessage, "9999.1.0"))
                 .And
-                .NotHaveStdErrContaining(Path.Combine("9999.0.0", "System.Collections.Immutable.dll"));
+                .HaveStdErrContaining(Path.Combine(_exeFoundUberFxMessage, "7777.0.0"));
 
-            DeleteAvailableSharedFxVersions(_exeSharedFxBaseDir, "9999.0.0");
+            // Same as previous except use of '--roll-forward-on-no-candidate-fx'
+            // Expected: Fail since '--roll-forward-on-no-candidate-fx' should apply to all layers
+            dotnet.Exec(
+                    "exec",
+                    "--roll-forward-on-no-candidate-fx", "0",
+                    appDll)
+                .WorkingDirectory(_currentWorkingDir)
+                .EnvironmentVariable("COREHOST_TRACE", "1")
+                .EnvironmentVariable("DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX", "1")
+                .CaptureStdOut()
+                .CaptureStdErr()
+                .Execute()
+                .Should()
+                .Fail()
+                .And
+                .HaveStdErrContaining("It was not possible to find any compatible framework version");
+
+            DeleteAvailableSharedFxVersions(_exeSharedFxBaseDir, "9999.1.0");
             DeleteAvailableSharedFxVersions(_exeSharedUberFxBaseDir, "7777.0.0");
         }
 
@@ -1070,7 +1118,7 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSharedFxLooku
          *   }
          * }
         */
-        private void SetRuntimeConfigJson(string destFile, string version, int? rollFwdOnNoCandidateFx = null, string testConfigPropertyValue = null, bool? useUberFramework = false)
+        private void SetRuntimeConfigJson(string destFile, string version, int? rollFwdOnNoCandidateFx = null, string testConfigPropertyValue = null, bool? useUberFramework = false, JArray additionalFrameworks = null)
         {
             string name = useUberFramework.HasValue && useUberFramework.Value ? "Microsoft.UberFramework" : "Microsoft.NETCore.App";
 
@@ -1099,6 +1147,11 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSharedFxLooku
                 );
             }
 
+            if (additionalFrameworks != null)
+            {
+                runtimeOptions.Add("additionalFrameworks", additionalFrameworks);
+            }
+
             FileInfo file = new FileInfo(destFile);
             if (!file.Directory.Exists)
             {
@@ -1110,6 +1163,28 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.MultilevelSharedFxLooku
             File.WriteAllText(destFile, json.ToString());
         }
 
+        static private JObject GetAdditionalFramework(string fxName, string fxVersion, bool? applyPatches, int? rollForwardOnNoCandidateFx)
+        {
+            var jobject = new JObject(new JProperty("name", fxName));
+
+            if (fxVersion != null)
+            {
+                jobject.Add(new JProperty("version", fxVersion));
+            }
+
+            if (applyPatches.HasValue)
+            {
+                jobject.Add(new JProperty("applyPatches", applyPatches.Value));
+            }
+
+            if (rollForwardOnNoCandidateFx.HasValue)
+            {
+                jobject.Add(new JProperty("rollForwardOnNoCandidateFx", rollForwardOnNoCandidateFx));
+            }
+
+            return jobject;
+        }
+
         static private void CreateUberFrameworkArtifacts(string builtSharedFxDir, string builtSharedUberFxDir, string assemblyVersion = null, string fileVersion = null)
         {
             DirectoryInfo dir = new DirectoryInfo(builtSharedUberFxDir);