diff --git a/scripts/azure-pipelines/end-to-end-tests.ps1 b/scripts/azure-pipelines/end-to-end-tests.ps1
new file mode 100644
index 0000000000..b0642df824
--- /dev/null
+++ b/scripts/azure-pipelines/end-to-end-tests.ps1
@@ -0,0 +1,134 @@
+# Copyright (c) Microsoft Corporation.
+# SPDX-License-Identifier: MIT
+#
+<#
+.SYNOPSIS
+End-to-End tests for the vcpkg executable.
+
+.DESCRIPTION
+These tests cover the command line interface and broad functions of vcpkg, including `install`, `remove` and certain
+binary caching scenarios. They use the vcpkg executable in the current directory.
+
+.PARAMETER Triplet
+The triplet to use for testing purposes.
+
+.PARAMETER WorkingRoot
+The location used as scratch space for testing.
+
+#>
+
+[CmdletBinding()]
+Param(
+    [Parameter(Mandatory = $true)]
+    [ValidateNotNullOrEmpty()]
+    [string]$Triplet,
+    [Parameter(Mandatory = $true)]
+    [ValidateNotNullOrEmpty()]
+    [string]$WorkingRoot
+)
+
+$ErrorActionPreference = "Stop"
+
+$TestingRoot = Join-Path $WorkingRoot 'testing'
+$buildtreesRoot = Join-Path $TestingRoot 'buildtrees'
+$installRoot = Join-Path $TestingRoot 'installed'
+$packagesRoot = Join-Path $TestingRoot 'packages'
+$NuGetRoot = Join-Path $TestingRoot 'nuget'
+$NuGetRoot2 = Join-Path $TestingRoot 'nuget2'
+$ArchiveRoot = Join-Path $TestingRoot 'archives'
+$commonArgs = @(
+    "--triplet",
+    $Triplet,
+    "--x-buildtrees-root=$buildtreesRoot",
+    "--x-install-root=$installRoot",
+    "--x-packages-root=$packagesRoot"
+)
+
+Remove-Item -Recurse -Force $TestingRoot -ErrorAction SilentlyContinue
+mkdir $TestingRoot
+mkdir $NuGetRoot
+
+function Require-FileExists {
+    [CmdletBinding()]
+    Param(
+        [string]$File
+    )
+    if (-Not (Test-Path $File)) {
+        throw "'$CurrentTest' failed to create file '$File'"
+    }
+}
+function Require-FileNotExists {
+    [CmdletBinding()]
+    Param(
+        [string]$File
+    )
+    if (Test-Path $File) {
+        throw "'$CurrentTest' should not have created file '$File'"
+    }
+}
+
+# Test simple installation
+$args = $commonArgs + @("install","rapidjson","--binarycaching","--x-binarysource=clear;files,$ArchiveRoot,write;nuget,$NuGetRoot,upload")
+$CurrentTest = "./vcpkg $($args -join ' ')"
+Write-Host $CurrentTest
+./vcpkg @args
+
+Require-FileExists "$installRoot/$Triplet/include/rapidjson/rapidjson.h"
+
+# Test simple removal
+$args = $commonArgs + @("remove", "rapidjson")
+$CurrentTest = "./vcpkg $($args -join ' ')"
+Write-Host $CurrentTest
+./vcpkg @args
+
+Require-FileNotExists "$installRoot/$Triplet/include/rapidjson/rapidjson.h"
+
+# Test restoring from files archive
+$args = $commonArgs + @("install","rapidjson","--binarycaching","--x-binarysource=clear;files,$ArchiveRoot,read")
+$CurrentTest = "./vcpkg $($args -join ' ')"
+Remove-Item -Recurse -Force $installRoot
+Remove-Item -Recurse -Force $buildtreesRoot
+Write-Host $CurrentTest
+./vcpkg @args
+
+Require-FileExists "$installRoot/$Triplet/include/rapidjson/rapidjson.h"
+Require-FileNotExists "$buildtreesRoot/rapidjson/src"
+
+# Test restoring from nuget
+$args = $commonArgs + @("install","rapidjson","--binarycaching","--x-binarysource=clear;nuget,$NuGetRoot")
+$CurrentTest = "./vcpkg $($args -join ' ')"
+Remove-Item -Recurse -Force $installRoot
+Remove-Item -Recurse -Force $buildtreesRoot
+Write-Host $CurrentTest
+./vcpkg @args
+
+Require-FileExists "$installRoot/$Triplet/include/rapidjson/rapidjson.h"
+Require-FileNotExists "$buildtreesRoot/rapidjson/src"
+
+# Test four-phase flow
+$args = $commonArgs + @("install","rapidjson","--dry-run","--x-write-nuget-packages-config=$TestingRoot/packages.config")
+$CurrentTest = "./vcpkg $($args -join ' ')"
+Remove-Item -Recurse -Force $installRoot -ErrorAction SilentlyContinue
+Write-Host $CurrentTest
+./vcpkg @args
+Require-FileNotExists "$installRoot/$Triplet/include/rapidjson/rapidjson.h"
+Require-FileNotExists "$buildtreesRoot/rapidjson/src"
+Require-FileExists "$TestingRoot/packages.config"
+
+& $(./vcpkg fetch nuget) restore $TestingRoot/packages.config -OutputDirectory "$NuGetRoot2" -Source "$NuGetRoot"
+
+Remove-Item -Recurse -Force $NuGetRoot -ErrorAction SilentlyContinue
+mkdir $NuGetRoot
+
+$args = $commonArgs + @("install","rapidjson","tinyxml","--binarycaching","--x-binarysource=clear;nuget,$NuGetRoot2;nuget,$NuGetRoot,upload")
+$CurrentTest = "./vcpkg $($args -join ' ')"
+Write-Host $CurrentTest
+./vcpkg @args
+Require-FileExists "$installRoot/$Triplet/include/rapidjson/rapidjson.h"
+Require-FileExists "$installRoot/$Triplet/include/tinyxml.h"
+Require-FileNotExists "$buildtreesRoot/rapidjson/src"
+Require-FileExists "$buildtreesRoot/tinyxml/src"
+
+if ((Get-ChildItem $NuGetRoot -Filter '*.nupkg' | Measure-Object).Count -ne 1) {
+    throw "In '$CurrentTest': did not create exactly 1 NuGet package"
+}
diff --git a/scripts/azure-pipelines/windows/azure-pipelines.yml b/scripts/azure-pipelines/windows/azure-pipelines.yml
index 69ea089d6c..a20ee23bb1 100644
--- a/scripts/azure-pipelines/windows/azure-pipelines.yml
+++ b/scripts/azure-pipelines/windows/azure-pipelines.yml
@@ -42,6 +42,7 @@ jobs:
         cmake.exe -G Ninja -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON -DVCPKG_DEVELOPMENT_WARNINGS=ON -DVCPKG_WARNINGS_AS_ERRORS=ON -DVCPKG_BUILD_FUZZING=ON -B build.x86.debug -S toolsrc
         ninja.exe -C build.x86.debug
         build.x86.debug\vcpkg-test.exe
+        powershell.exe -NoProfile -ExecutionPolicy Bypass "scripts\azure-pipelines\end-to-end-tests.ps1 -WorkingRoot \"%cd%\testing\" -triplet x86-windows"
       failOnStderr: true
   - task: PowerShell@2
     displayName: '*** Test Modified Ports and Prepare Test Logs ***'
diff --git a/toolsrc/include/vcpkg/base/files.h b/toolsrc/include/vcpkg/base/files.h
index dce9584fa4..7676b49a03 100644
--- a/toolsrc/include/vcpkg/base/files.h
+++ b/toolsrc/include/vcpkg/base/files.h
@@ -201,4 +201,8 @@ namespace vcpkg::Files
     bool has_invalid_chars_for_filesystem(const std::string& s);
 
     void print_paths(const std::vector<fs::path>& paths);
+
+    /// Performs "lhs / rhs" according to the C++17 Filesystem Library Specification.
+    /// This function exists as a workaround for TS implementations.
+    fs::path combine(const fs::path& lhs, const fs::path& rhs);
 }
diff --git a/toolsrc/include/vcpkg/binarycaching.h b/toolsrc/include/vcpkg/binarycaching.h
index 61af79a3fe..c1db1f1692 100644
--- a/toolsrc/include/vcpkg/binarycaching.h
+++ b/toolsrc/include/vcpkg/binarycaching.h
@@ -39,8 +39,7 @@ namespace vcpkg
         virtual void push_failure(const VcpkgPaths& paths, const std::string& abi_tag, const PackageSpec& spec) = 0;
         /// Requests the result of `try_restore()` without actually downloading the package. Used by CI to determine
         /// missing packages.
-        virtual RestoreResult precheck(const VcpkgPaths& paths,
-                                       const Dependencies::InstallPlanAction& action) = 0;
+        virtual RestoreResult precheck(const VcpkgPaths& paths, const Dependencies::InstallPlanAction& action) = 0;
     };
 
     IBinaryProvider& null_binary_provider();
@@ -50,5 +49,7 @@ namespace vcpkg
     ExpectedS<std::unique_ptr<IBinaryProvider>> create_binary_provider_from_configs_pure(const std::string& env_string,
                                                                                          View<std::string> args);
 
+    std::string generate_nuget_packages_config(const Dependencies::ActionPlan& action);
+
     void help_topic_binary_caching(const VcpkgPaths& paths);
 }
diff --git a/toolsrc/include/vcpkg/binarycaching.private.h b/toolsrc/include/vcpkg/binarycaching.private.h
index f1fd046de3..f20a0db121 100644
--- a/toolsrc/include/vcpkg/binarycaching.private.h
+++ b/toolsrc/include/vcpkg/binarycaching.private.h
@@ -36,9 +36,6 @@ namespace vcpkg
 
     struct XmlSerializer
     {
-        std::string buf;
-        int indent = 0;
-
         XmlSerializer& emit_declaration();
         XmlSerializer& open_tag(StringLiteral sl);
         XmlSerializer& start_complex_open_tag(StringLiteral sl);
@@ -49,6 +46,14 @@ namespace vcpkg
         XmlSerializer& text(StringView sv);
         XmlSerializer& simple_tag(StringLiteral tag, StringView content);
         XmlSerializer& line_break();
+
+        std::string buf;
+
+    private:
+        XmlSerializer& emit_pending_indent();
+
+        int m_indent = 0;
+        bool m_pending_indent = false;
     };
 
 }
\ No newline at end of file
diff --git a/toolsrc/include/vcpkg/commands.h b/toolsrc/include/vcpkg/commands.h
index b73e91fe76..cec9237f57 100644
--- a/toolsrc/include/vcpkg/commands.h
+++ b/toolsrc/include/vcpkg/commands.h
@@ -163,7 +163,8 @@ namespace vcpkg::Commands
                                  const CMakeVars::CMakeVarProvider& cmake_vars,
                                  const std::vector<FullPackageSpec>& specs,
                                  const Build::BuildPackageOptions& install_plan_options,
-                                 DryRun dry_run);
+                                 DryRun dry_run,
+                                 const Optional<fs::path>& pkgsconfig_path);
         void perform_and_exit(const VcpkgCmdArguments& args, const VcpkgPaths& paths, Triplet default_triplet);
     }
 
diff --git a/toolsrc/src/vcpkg-test/binarycaching.cpp b/toolsrc/src/vcpkg-test/binarycaching.cpp
index 817b85e03d..b220b5ccba 100644
--- a/toolsrc/src/vcpkg-test/binarycaching.cpp
+++ b/toolsrc/src/vcpkg-test/binarycaching.cpp
@@ -1,10 +1,11 @@
 #include <catch2/catch.hpp>
 #include <vcpkg/binarycaching.private.h>
+#include <vcpkg/binarycaching.h>
 #include <vcpkg/base/files.h>
+#include <vcpkg/dependencies.h>
 #include <vcpkg/vcpkgcmdarguments.h>
 #include <vcpkg/sourceparagraph.h>
 #include <vcpkg/paragraphs.h>
-#include <vcpkg/dependencies.h>
 #include <string>
 
 using namespace vcpkg;
@@ -96,9 +97,9 @@ Features: a, b
 Dependencies:
 </description>
     <packageTypes><packageType name="vcpkg"/></packageTypes>
-    </metadata>
+  </metadata>
   <files><file src=")" PKGPATH R"(" target=""/></files>
-  </package>
+</package>
 )";
     auto expected_lines = Strings::split(expected, '\n');
     auto nuspec_lines = Strings::split(nuspec, '\n');
@@ -123,8 +124,14 @@ TEST_CASE ("XmlSerializer", "[XmlSerializer]")
 
     xml = XmlSerializer();
     xml.emit_declaration();
-    xml.start_complex_open_tag("a").text_attr("b", "<").text_attr("c", "  ").finish_self_closing_complex_tag();
-    REQUIRE(xml.buf == R"(<?xml version="1.0" encoding="utf-8"?><a b="&lt;" c="  "/>)");
+    xml.start_complex_open_tag("a")
+        .text_attr("b", "<")
+        .text_attr("c", "  ")
+        .finish_self_closing_complex_tag()
+        .line_break();
+    xml.simple_tag("d", "e");
+    REQUIRE(xml.buf == R"(<?xml version="1.0" encoding="utf-8"?><a b="&lt;" c="  "/>)"
+                       "\n<d>e</d>");
 
     xml = XmlSerializer();
     xml.start_complex_open_tag("a").finish_complex_open_tag();
@@ -134,5 +141,72 @@ TEST_CASE ("XmlSerializer", "[XmlSerializer]")
     xml.line_break();
     xml.open_tag("a").line_break().line_break();
     xml.close_tag("a").line_break().line_break();
-    REQUIRE(xml.buf == "\n<a>\n  \n  </a>\n\n");
-}
\ No newline at end of file
+    REQUIRE(xml.buf == "\n<a>\n\n</a>\n\n");
+
+    xml = XmlSerializer();
+    xml.start_complex_open_tag("a")
+        .text_attr("b", "<")
+        .line_break()
+        .text_attr("c", "  ")
+        .finish_complex_open_tag()
+        .line_break();
+    xml.simple_tag("d", "e").line_break();
+    REQUIRE(xml.buf == "<a b=\"&lt;\"\n  c=\"  \">\n  <d>e</d>\n");
+}
+
+TEST_CASE ("generate_nuget_packages_config", "[generate_nuget_packages_config]")
+{
+    Dependencies::ActionPlan plan;
+    auto packageconfig = generate_nuget_packages_config(plan);
+    REQUIRE(packageconfig == R"(<?xml version="1.0" encoding="utf-8"?>
+<packages>
+</packages>
+)");
+
+    auto pghs = Paragraphs::parse_paragraphs(R"(
+Source: zlib
+Version: 1.5
+Description: a spiffy compression library wrapper
+)",
+                                             "<testdata>");
+    REQUIRE(pghs.has_value());
+    auto maybe_scf = SourceControlFile::parse_control_file(fs::path(), std::move(*pghs.get()));
+    REQUIRE(maybe_scf.has_value());
+    SourceControlFileLocation scfl{std::move(*maybe_scf.get()), fs::path()};
+    plan.install_actions.push_back(Dependencies::InstallPlanAction());
+    plan.install_actions[0].spec = PackageSpec("zlib", Triplet::X64_ANDROID);
+    plan.install_actions[0].source_control_file_location = scfl;
+    plan.install_actions[0].abi_info = Build::AbiInfo{};
+    plan.install_actions[0].abi_info.get()->package_abi = "packageabi";
+
+    packageconfig = generate_nuget_packages_config(plan);
+    REQUIRE(packageconfig == R"(<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="zlib_x64-android" version="1.5.0-packageabi"/>
+</packages>
+)");
+
+    auto pghs2 = Paragraphs::parse_paragraphs(R"(
+Source: zlib2
+Version: 1.52
+Description: a spiffy compression library wrapper
+)",
+                                              "<testdata>");
+    REQUIRE(pghs2.has_value());
+    auto maybe_scf2 = SourceControlFile::parse_control_file(fs::path(), std::move(*pghs2.get()));
+    REQUIRE(maybe_scf2.has_value());
+    SourceControlFileLocation scfl2{std::move(*maybe_scf2.get()), fs::path()};
+    plan.install_actions.push_back(Dependencies::InstallPlanAction());
+    plan.install_actions[1].spec = PackageSpec("zlib2", Triplet::X64_ANDROID);
+    plan.install_actions[1].source_control_file_location = scfl2;
+    plan.install_actions[1].abi_info = Build::AbiInfo{};
+    plan.install_actions[1].abi_info.get()->package_abi = "packageabi2";
+
+    packageconfig = generate_nuget_packages_config(plan);
+    REQUIRE(packageconfig == R"(<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="zlib_x64-android" version="1.5.0-packageabi"/>
+  <package id="zlib2_x64-android" version="1.52.0-packageabi2"/>
+</packages>
+)");
+}
diff --git a/toolsrc/src/vcpkg/base/files.cpp b/toolsrc/src/vcpkg/base/files.cpp
index 9d9aecb696..2e39073cea 100644
--- a/toolsrc/src/vcpkg/base/files.cpp
+++ b/toolsrc/src/vcpkg/base/files.cpp
@@ -989,4 +989,20 @@ namespace vcpkg::Files
         message.push_back('\n');
         System::print2(message);
     }
+
+    fs::path combine(const fs::path& lhs, const fs::path& rhs)
+    {
+#if VCPKG_USE_STD_FILESYSTEM
+        return lhs / rhs;
+#else // ^^^ VCPKG_USE_STD_FILESYSTEM // !VCPKG_USE_STD_FILESYSTEM vvv
+        if (rhs.is_absolute())
+        {
+            return rhs;
+        }
+        else
+        {
+            return lhs / rhs;
+        }
+#endif
+    }
 }
diff --git a/toolsrc/src/vcpkg/binarycaching.cpp b/toolsrc/src/vcpkg/binarycaching.cpp
index 5bebc4fb86..532e07032b 100644
--- a/toolsrc/src/vcpkg/binarycaching.cpp
+++ b/toolsrc/src/vcpkg/binarycaching.cpp
@@ -285,7 +285,7 @@ namespace
             for (auto&& action : plan.install_actions)
             {
                 auto& spec = action.spec;
-                fs.remove_all_inside(paths.package_dir(spec), VCPKG_LINE_INFO);
+                fs.remove_all(paths.package_dir(spec), VCPKG_LINE_INFO);
 
                 nuget_refs.emplace_back(spec, NugetReference(action));
             }
@@ -615,42 +615,57 @@ XmlSerializer& XmlSerializer::emit_declaration()
 }
 XmlSerializer& XmlSerializer::open_tag(StringLiteral sl)
 {
+    emit_pending_indent();
     Strings::append(buf, '<', sl, '>');
-    indent += 2;
+    m_indent += 2;
     return *this;
 }
 XmlSerializer& XmlSerializer::start_complex_open_tag(StringLiteral sl)
 {
+    emit_pending_indent();
     Strings::append(buf, '<', sl);
-    indent += 2;
+    m_indent += 2;
     return *this;
 }
 XmlSerializer& XmlSerializer::text_attr(StringLiteral name, StringView content)
 {
-    Strings::append(buf, ' ', name, "=\"");
+    if (m_pending_indent)
+    {
+        m_pending_indent = false;
+        buf.append(m_indent, ' ');
+    }
+    else
+    {
+        buf.push_back(' ');
+    }
+    Strings::append(buf, name, "=\"");
     text(content);
     Strings::append(buf, '"');
     return *this;
 }
 XmlSerializer& XmlSerializer::finish_complex_open_tag()
 {
+    emit_pending_indent();
     Strings::append(buf, '>');
     return *this;
 }
 XmlSerializer& XmlSerializer::finish_self_closing_complex_tag()
 {
+    emit_pending_indent();
     Strings::append(buf, "/>");
-    indent -= 2;
+    m_indent -= 2;
     return *this;
 }
 XmlSerializer& XmlSerializer::close_tag(StringLiteral sl)
 {
+    m_indent -= 2;
+    emit_pending_indent();
     Strings::append(buf, "</", sl, '>');
-    indent -= 2;
     return *this;
 }
 XmlSerializer& XmlSerializer::text(StringView sv)
 {
+    emit_pending_indent();
     for (auto ch : sv)
     {
         if (ch == '&')
@@ -682,12 +697,21 @@ XmlSerializer& XmlSerializer::text(StringView sv)
 }
 XmlSerializer& XmlSerializer::simple_tag(StringLiteral tag, StringView content)
 {
-    return open_tag(tag).text(content).close_tag(tag);
+    return emit_pending_indent().open_tag(tag).text(content).close_tag(tag);
 }
 XmlSerializer& XmlSerializer::line_break()
 {
     buf.push_back('\n');
-    buf.append(indent, ' ');
+    m_pending_indent = true;
+    return *this;
+}
+XmlSerializer& XmlSerializer::emit_pending_indent()
+{
+    if (m_pending_indent)
+    {
+        m_pending_indent = false;
+        buf.append(m_indent, ' ');
+    }
     return *this;
 }
 
@@ -1118,3 +1142,22 @@ void vcpkg::help_topic_binary_caching(const VcpkgPaths&)
 
     System::print2(tbl.m_str);
 }
+
+std::string vcpkg::generate_nuget_packages_config(const Dependencies::ActionPlan& action)
+{
+    auto refs = Util::fmap(action.install_actions,
+                           [&](const Dependencies::InstallPlanAction& ipa) { return NugetReference(ipa); });
+    XmlSerializer xml;
+    xml.emit_declaration().line_break();
+    xml.open_tag("packages").line_break();
+    for (auto&& ref : refs)
+    {
+        xml.start_complex_open_tag("package")
+            .text_attr("id", ref.id)
+            .text_attr("version", ref.version)
+            .finish_self_closing_complex_tag()
+            .line_break();
+    }
+    xml.close_tag("packages").line_break();
+    return std::move(xml.buf);
+}
diff --git a/toolsrc/src/vcpkg/build.cpp b/toolsrc/src/vcpkg/build.cpp
index 5e2f8778b7..7d29761658 100644
--- a/toolsrc/src/vcpkg/build.cpp
+++ b/toolsrc/src/vcpkg/build.cpp
@@ -484,8 +484,9 @@ namespace vcpkg::Build
             env);
         out_file.close();
 
-        Checks::check_exit(
-            VCPKG_LINE_INFO, !compiler_hash.empty(), "Error occured while detecting compiler information");
+        Checks::check_exit(VCPKG_LINE_INFO,
+                           !compiler_hash.empty(),
+                           "Error occured while detecting compiler information. Pass `--debug` for more information.");
 
         Debug::print("Detecting compiler hash for triplet ", triplet, ": ", compiler_hash, "\n");
         return compiler_hash;
@@ -866,6 +867,8 @@ namespace vcpkg::Build
         for (auto it = action_plan.install_actions.begin(); it != action_plan.install_actions.end(); ++it)
         {
             auto& action = *it;
+            if (action.abi_info.has_value()) continue;
+
             std::vector<AbiEntry> dependency_abis;
             if (!Util::Enum::to_bool(action.build_options.only_downloads))
             {
diff --git a/toolsrc/src/vcpkg/commands.setinstalled.cpp b/toolsrc/src/vcpkg/commands.setinstalled.cpp
index f52e4942b7..df191ee3e6 100644
--- a/toolsrc/src/vcpkg/commands.setinstalled.cpp
+++ b/toolsrc/src/vcpkg/commands.setinstalled.cpp
@@ -14,16 +14,22 @@
 namespace vcpkg::Commands::SetInstalled
 {
     static constexpr StringLiteral OPTION_DRY_RUN = "--dry-run";
+    static constexpr StringLiteral OPTION_WRITE_PACKAGES_CONFIG = "--x-write-nuget-packages-config";
 
     static constexpr CommandSwitch INSTALL_SWITCHES[] = {
         {OPTION_DRY_RUN, "Do not actually build or install"},
     };
+    static constexpr CommandSetting INSTALL_SETTINGS[] = {
+        {OPTION_WRITE_PACKAGES_CONFIG,
+         "Writes out a NuGet packages.config-formatted file for use with external binary caching.\n"
+         "See `vcpkg help binarycaching` for more information."},
+    };
 
     const CommandStructure COMMAND_STRUCTURE = {
         create_example_string(R"(x-set-installed <package>...)"),
         0,
         SIZE_MAX,
-        {INSTALL_SWITCHES},
+        {INSTALL_SWITCHES, INSTALL_SETTINGS},
         nullptr,
     };
 
@@ -34,7 +40,8 @@ namespace vcpkg::Commands::SetInstalled
                              const CMakeVars::CMakeVarProvider& cmake_vars,
                              const std::vector<FullPackageSpec>& specs,
                              const Build::BuildPackageOptions& install_plan_options,
-                             DryRun dry_run)
+                             DryRun dry_run,
+                             const Optional<fs::path>& maybe_pkgsconfig)
     {
         // We have a set of user-requested specs.
         // We need to know all the specs which are required to fulfill dependencies for those specs.
@@ -91,6 +98,16 @@ namespace vcpkg::Commands::SetInstalled
 
         Dependencies::print_plan(action_plan, true, paths.ports);
 
+        if (auto p_pkgsconfig = maybe_pkgsconfig.get())
+        {
+            Build::compute_all_abis(paths, action_plan, cmake_vars, status_db);
+            auto& fs = paths.get_filesystem();
+            auto pkgsconfig_path = Files::combine(paths.original_cwd, *p_pkgsconfig);
+            auto pkgsconfig_contents = generate_nuget_packages_config(action_plan);
+            fs.write_contents(pkgsconfig_path, pkgsconfig_contents, VCPKG_LINE_INFO);
+            System::print2("Wrote NuGet packages config information to ", pkgsconfig_path.u8string(), "\n");
+        }
+
         if (dry_run == DryRun::Yes)
         {
             Checks::exit_success(VCPKG_LINE_INFO);
@@ -142,6 +159,12 @@ namespace vcpkg::Commands::SetInstalled
         PortFileProvider::PathsPortFileProvider provider(paths, args.overlay_ports);
         auto cmake_vars = CMakeVars::make_triplet_cmake_var_provider(paths);
 
+        Optional<fs::path> pkgsconfig;
+        auto it_pkgsconfig = options.settings.find(OPTION_WRITE_PACKAGES_CONFIG);
+        if (it_pkgsconfig != options.settings.end())
+        {
+            pkgsconfig = it_pkgsconfig->second;
+        }
         perform_and_exit_ex(args,
                             paths,
                             provider,
@@ -149,6 +172,7 @@ namespace vcpkg::Commands::SetInstalled
                             *cmake_vars,
                             specs,
                             install_plan_options,
-                            dry_run ? DryRun::Yes : DryRun::No);
+                            dry_run ? DryRun::Yes : DryRun::No,
+                            pkgsconfig);
     }
 }
diff --git a/toolsrc/src/vcpkg/install.cpp b/toolsrc/src/vcpkg/install.cpp
index 5750469add..aa79bb1f29 100644
--- a/toolsrc/src/vcpkg/install.cpp
+++ b/toolsrc/src/vcpkg/install.cpp
@@ -500,6 +500,7 @@ namespace vcpkg::Install
     static constexpr StringLiteral OPTION_XUNIT = "--x-xunit";
     static constexpr StringLiteral OPTION_USE_ARIA2 = "--x-use-aria2";
     static constexpr StringLiteral OPTION_CLEAN_AFTER_BUILD = "--clean-after-build";
+    static constexpr StringLiteral OPTION_WRITE_PACKAGES_CONFIG = "--x-write-nuget-packages-config";
 
     static constexpr std::array<CommandSwitch, 8> INSTALL_SWITCHES = {{
         {OPTION_DRY_RUN, "Do not actually build or install"},
@@ -511,8 +512,11 @@ namespace vcpkg::Install
         {OPTION_USE_ARIA2, "Use aria2 to perform download tasks"},
         {OPTION_CLEAN_AFTER_BUILD, "Clean buildtrees, packages and downloads after building each package"},
     }};
-    static constexpr std::array<CommandSetting, 1> INSTALL_SETTINGS = {{
+    static constexpr std::array<CommandSetting, 2> INSTALL_SETTINGS = {{
         {OPTION_XUNIT, "File to output results in XUnit format (Internal use)"},
+        {OPTION_WRITE_PACKAGES_CONFIG,
+         "Writes out a NuGet packages.config-formatted file for use with external binary caching.\nSee `vcpkg help "
+         "binarycaching` for more information."},
     }};
 
     std::vector<std::string> get_all_port_names(const VcpkgPaths& paths)
@@ -654,7 +658,8 @@ namespace vcpkg::Install
     void perform_and_exit(const VcpkgCmdArguments& args, const VcpkgPaths& paths, Triplet default_triplet)
     {
         // input sanitization
-        const ParsedArguments options = args.parse_arguments(paths.manifest_mode_enabled() ? MANIFEST_COMMAND_STRUCTURE : COMMAND_STRUCTURE);
+        const ParsedArguments options =
+            args.parse_arguments(paths.manifest_mode_enabled() ? MANIFEST_COMMAND_STRUCTURE : COMMAND_STRUCTURE);
 
         auto binaryprovider =
             create_binary_provider_from_configs(paths, args.binary_sources).value_or_exit(VCPKG_LINE_INFO);
@@ -697,8 +702,10 @@ namespace vcpkg::Install
 
             if (ec)
             {
-                Checks::exit_with_message(VCPKG_LINE_INFO, "Failed to load manifest file (%s): %s\n",
-                    path_to_manifest.u8string(), ec.message());
+                Checks::exit_with_message(VCPKG_LINE_INFO,
+                                          "Failed to load manifest file (%s): %s\n",
+                                          path_to_manifest.u8string(),
+                                          ec.message());
             }
 
             std::vector<FullPackageSpec> specs;
@@ -716,7 +723,21 @@ namespace vcpkg::Install
                 Checks::exit_fail(VCPKG_LINE_INFO);
             }
 
-            Commands::SetInstalled::perform_and_exit_ex(args, paths, provider, *binaryprovider, var_provider, specs, install_plan_options, dry_run ? Commands::DryRun::Yes : Commands::DryRun::No);
+            Optional<fs::path> pkgsconfig;
+            auto it_pkgsconfig = options.settings.find(OPTION_WRITE_PACKAGES_CONFIG);
+            if (it_pkgsconfig != options.settings.end())
+            {
+                pkgsconfig = fs::u8path(it_pkgsconfig->second);
+            }
+            Commands::SetInstalled::perform_and_exit_ex(args,
+                                                        paths,
+                                                        provider,
+                                                        *binaryprovider,
+                                                        var_provider,
+                                                        specs,
+                                                        install_plan_options,
+                                                        dry_run ? Commands::DryRun::Yes : Commands::DryRun::No,
+                                                        pkgsconfig);
         }
 
         const std::vector<FullPackageSpec> specs = Util::fmap(args.command_arguments, [&](auto&& arg) {
@@ -799,6 +820,17 @@ namespace vcpkg::Install
 
         Dependencies::print_plan(action_plan, is_recursive, paths.ports);
 
+        auto it_pkgsconfig = options.settings.find(OPTION_WRITE_PACKAGES_CONFIG);
+        if (it_pkgsconfig != options.settings.end())
+        {
+            Build::compute_all_abis(paths, action_plan, var_provider, status_db);
+
+            auto pkgsconfig_path = Files::combine(paths.original_cwd, fs::u8path(it_pkgsconfig->second));
+            auto pkgsconfig_contents = generate_nuget_packages_config(action_plan);
+            fs.write_contents(pkgsconfig_path, pkgsconfig_contents, VCPKG_LINE_INFO);
+            System::print2("Wrote NuGet packages config information to ", pkgsconfig_path.u8string(), "\n");
+        }
+
         if (dry_run)
         {
             Checks::exit_success(VCPKG_LINE_INFO);