Преглед на файлове

PkgC: Add cmake_pkg_config(EXTRACT) command

* Wraps the llpkgc parser with cmPkgConfigParser
* Adds various resolution and mangling code under cmPkgConfigResolver
* Documents new command cmake_pkg_config(EXTRACT). Documentation is written with
  the assumption additional subcommands will be added soon.
* Adds various tests for the above
Vito Gamberini преди 1 година
родител
ревизия
8555c33d92
променени са 54 файла, в които са добавени 2932 реда и са изтрити 0 реда
  1. 261 0
      Help/command/cmake_pkg_config.rst
  2. 1 0
      Help/manual/cmake-commands.7.rst
  3. 6 0
      Source/CMakeLists.txt
  4. 722 0
      Source/cmCMakePkgConfigCommand.cxx
  5. 13 0
      Source/cmCMakePkgConfigCommand.h
  6. 2 0
      Source/cmCommands.cxx
  7. 151 0
      Source/cmPkgConfigParser.cxx
  8. 93 0
      Source/cmPkgConfigParser.h
  9. 870 0
      Source/cmPkgConfigResolver.cxx
  10. 172 0
      Source/cmPkgConfigResolver.h
  11. 1 0
      Tests/RunCMake/CMakeLists.txt
  12. 5 0
      Tests/RunCMake/cmake_pkg_config/CMakeLists.txt
  13. 3 0
      Tests/RunCMake/cmake_pkg_config/PackageRoot/VersionPackages/a.pc
  14. 3 0
      Tests/RunCMake/cmake_pkg_config/PackageRoot/VersionPackages/one.pc
  15. 3 0
      Tests/RunCMake/cmake_pkg_config/PackageRoot/VersionPackages/onedot.pc
  16. 3 0
      Tests/RunCMake/cmake_pkg_config/PackageRoot/VersionPackages/pseudo-empty.pc
  17. 3 0
      Tests/RunCMake/cmake_pkg_config/PackageRoot/VersionPackages/tilde.pc
  18. 3 0
      Tests/RunCMake/cmake_pkg_config/PackageRoot/VersionPackages/zeroone.pc
  19. 15 0
      Tests/RunCMake/cmake_pkg_config/PackageRoot/all-extract-fields.pc
  20. 3 0
      Tests/RunCMake/cmake_pkg_config/PackageRoot/bar.pc
  21. 3 0
      Tests/RunCMake/cmake_pkg_config/PackageRoot/baz.pc
  22. 6 0
      Tests/RunCMake/cmake_pkg_config/PackageRoot/cflags-bothcase-f.pc
  23. 5 0
      Tests/RunCMake/cmake_pkg_config/PackageRoot/cflags-lowercase-f.pc
  24. 5 0
      Tests/RunCMake/cmake_pkg_config/PackageRoot/cflags-uppercase-f.pc
  25. 3 0
      Tests/RunCMake/cmake_pkg_config/PackageRoot/empty-key.pc
  26. 3 0
      Tests/RunCMake/cmake_pkg_config/PackageRoot/foo.pc
  27. 4 0
      Tests/RunCMake/cmake_pkg_config/PackageRoot/invalid.pc
  28. 2 0
      Tests/RunCMake/cmake_pkg_config/PackageRoot/no-description.pc
  29. 2 0
      Tests/RunCMake/cmake_pkg_config/PackageRoot/no-name.pc
  30. 2 0
      Tests/RunCMake/cmake_pkg_config/PackageRoot/no-version.pc
  31. 5 0
      Tests/RunCMake/cmake_pkg_config/PackageRoot/qux-uninstalled.pc
  32. 5 0
      Tests/RunCMake/cmake_pkg_config/PackageRoot/qux.pc
  33. 6 0
      Tests/RunCMake/cmake_pkg_config/PackageRoot/relocate.pc
  34. 18 0
      Tests/RunCMake/cmake_pkg_config/RunCMakeTest.cmake
  35. 15 0
      Tests/RunCMake/cmake_pkg_config/TestEnv-stderr.txt
  36. 75 0
      Tests/RunCMake/cmake_pkg_config/TestEnv.cmake
  37. 21 0
      Tests/RunCMake/cmake_pkg_config/TestExtract-stderr.txt
  38. 29 0
      Tests/RunCMake/cmake_pkg_config/TestExtract.cmake
  39. 8 0
      Tests/RunCMake/cmake_pkg_config/TestMangle-stderr.txt
  40. 22 0
      Tests/RunCMake/cmake_pkg_config/TestMangle.cmake
  41. 29 0
      Tests/RunCMake/cmake_pkg_config/TestQuiet.cmake
  42. 1 0
      Tests/RunCMake/cmake_pkg_config/TestRequired-result.txt
  43. 4 0
      Tests/RunCMake/cmake_pkg_config/TestRequired-stderr.txt
  44. 9 0
      Tests/RunCMake/cmake_pkg_config/TestRequired.cmake
  45. 4 0
      Tests/RunCMake/cmake_pkg_config/TestReroot-stderr.txt
  46. 10 0
      Tests/RunCMake/cmake_pkg_config/TestReroot.cmake
  47. 3 0
      Tests/RunCMake/cmake_pkg_config/TestStrictness-BEST_EFFORT-stderr.txt
  48. 31 0
      Tests/RunCMake/cmake_pkg_config/TestStrictness-PERMISSIVE-stderr.txt
  49. 38 0
      Tests/RunCMake/cmake_pkg_config/TestStrictness-STRICT-stderr.txt
  50. 51 0
      Tests/RunCMake/cmake_pkg_config/TestStrictness.cmake
  51. 2 0
      Tests/RunCMake/cmake_pkg_config/TestUninstalled-stderr.txt
  52. 10 0
      Tests/RunCMake/cmake_pkg_config/TestUninstalled.cmake
  53. 103 0
      Tests/RunCMake/cmake_pkg_config/TestVersion-stderr.txt
  54. 65 0
      Tests/RunCMake/cmake_pkg_config/TestVersion.cmake

+ 261 - 0
Help/command/cmake_pkg_config.rst

@@ -0,0 +1,261 @@
+cmake_pkg_config
+----------------
+
+.. only:: html
+
+   .. contents::
+
+Process pkg-config format package files.
+
+Synopsis
+^^^^^^^^
+
+.. parsed-literal::
+
+  cmake_pkg_config(EXTRACT <package> [<version>] [...])
+
+Introduction
+^^^^^^^^^^^^
+
+This command generates CMake variables and targets from pkg-config format
+package files natively, without needing to invoke or even require the presence
+of a pkg-config implementation. A ``<package>`` is either an absolute path to a
+package file, or a package name to be searched for using the typical pkg-config
+search patterns. The optional ``<version>`` string has the same format and
+semantics as a pkg-config style version specifier, with the exception that if
+no comparison operator is specified ``=`` is assumed.
+
+.. _`common options`:
+
+There are multiple signatures for this command, and some of the options are
+common between them. They are:
+
+``EXACT`` / ``QUIET`` / ``REQUIRED``
+  The ``EXACT`` option requests that the version string be matched exactly
+  (including empty string, if no version is provided), overriding the typical
+  pkg-config version comparison algorithm. This will ignore any comparison
+  operator attached to the version string.
+
+  The ``QUIET`` option disables informational messages, including those
+  indicating that the package cannot be found if it is not ``REQUIRED``. The
+  ``REQUIRED`` option stops processing with an error message if the package
+  cannot be found.
+
+``STRICTNESS <mode>``
+  Specify how strictly the contents of the package files will be verified during
+  parsing and resolution. An invalid file, under the provided strictness mode,
+  will cause the command to fail. Possible modes are:
+
+  * ``STRICT``: Closely mirrors the behavior of the original FDO pkg-config.
+    Variables and keywords must be unique. Variables must be defined before
+    they are used. The Name, Description, and Version keywords must be present.
+    The overall structure of the file must be valid and parsable.
+
+  * ``PERMISSIVE``: Closely mirrors the behavior of the pkgconf implementation.
+    Duplicate variables are overridden. Duplicate keywords are appended.
+    Undefined variables resolve to empty strings. The Name, Description, and
+    Version keywords must be present. The overall structure of the file must be
+    valid and parsable.
+
+  * ``BEST_EFFORT``: Same behavior as ``PERMISSIVE`` with regards to duplicate
+    or uninitialized variables and keywords, but will not fail under any
+    conditions. Package files which require BEST_EFFORT will fail validation
+    under all other major implementations and should be fixed.
+
+  The default strictness is ``PERMISSIVE``.
+
+``ENV_MODE``
+  Specifies which environment variables will be queried when running a given
+  command. Possible modes are:
+
+  * ``FDO``: Queries only the original set of ``PKG_CONFIG_*`` environment
+    variables used by the freedesktop.org ``pkg-config`` implementation.
+
+  * ``PKGCONF``: Queries the more extensive set of environment variables used
+    by the ``pkgconf`` implementation.
+
+  * ``IGNORE``: Ignores the presence, absence, and value of environment
+    variables entirely. In all cases an environment variable would be queried
+    its treated as defined, but with a value of empty string for the purpose
+    of the operation. This does not modify the current environment. For boolean
+    environment variables, such as ``PKG_CONFIG_ALLOW_*``, this means they are
+    evaluated as truthy.
+
+    ``PKG_CONFIG_SYSROOT_PATH`` is a minor exception. When ``ENV_MODE IGNORE``
+    is used, no root path prepending will occur by default and ``pc_sysrootdir``
+    remains defaulted to ``/``.
+
+  Target-generating subcommands always ignore flag-filtering environment
+  variables. The default environment mode is ``PKGCONF``.
+
+``PC_LIBDIR <path>...``
+  Overrides the default search location for package files; also used to derive
+  the ``pc_path`` package variable.
+
+  When this option is not provided, the default library directory is the first
+  available of the following values:
+
+  #. ``CMAKE_PKG_CONFIG_PC_LIB_DIRS``
+  #. The ``PKG_CONFIG_LIBDIR`` environment variable
+  #. The output of ``pkg-config --variable pc_path pkg-config``
+  #. A platform-dependent default value
+
+``PC_PATH <path>...``
+  Overrides the supplemental package file directories which will be prepended
+  to the search path; also used to derive the ``pc_path`` package variable.
+
+  When this option is not provided, the default paths are the first available of
+  the following values:
+
+  #. ``CMAKE_PKG_CONFIG_PC_PATH``
+  #. The ``PKG_CONFIG_PATH`` environment variable
+  #. Empty list
+
+``DISABLE_UNINSTALLED <bool>``
+  Overrides the search behavior for "uninstalled" package files. These are
+  package files with an "-uninstalled" suffix which describe packages integrated
+  directly from a build tree.
+
+  Normally such package files have higher priority than "installed" packages.
+  When ``DISABLE_UNINSTALLED`` is true, searching for "uninstalled" packages
+  is disabled.
+
+  When this option is not provided, the default search behavior is determined
+  by the first available of the following values:
+
+  #. ``CMAKE_PKG_CONFIG_DISABLE_UNINSTALLED``
+  #. If the ``PKG_CONFIG_DISABLE_UNINSTALLED`` environment variable is defined
+     the search is disabled, otherwise it is enabled.
+
+``PC_SYSROOT_DIR <path>``
+  Overrides the root path which will be prepended to paths specified by ``-I``
+  compile flags and ``-L`` library search locations; also used to derive the
+  ``pc_sysrootdir`` package variable.
+
+  When this option is not provided, the default root path is provided by the
+  first available of the following values:
+
+  #. ``CMAKE_PKG_CONFIG_SYSROOT_DIR``
+  #. The ``PKG_CONFIG_SYSROOT_DIR`` environment variable
+  #. If no root path is available, nothing will be prepended to include or
+     library directory paths and ``pc_sysrootdir`` will be set to ``/``
+
+``TOP_BUILD_DIR <path>``
+  Overrides the top build directory path used to derived the ``pc_top_builddir``
+  package variable.
+
+  When this option is not provided, the default top build directory path is
+  the first available of the following values:
+
+  #. ``CMAKE_PKG_CONFIG_TOP_BUILD_DIR``
+  #. The ``PKG_CONFIG_TOP_BUILD_DIR`` environment variable
+  #. If no top build directory path is available, the ``pc_top_builddir``
+     package variable is not set
+
+Signatures
+^^^^^^^^^^
+
+.. signature::
+  cmake_pkg_config(EXTRACT <package> [<version>] [...])
+
+  Extract the contents of the package into variables.
+
+  .. code-block:: cmake
+
+    cmake_pkg_config(EXTRACT <package> [<version>]
+                    [REQUIRED] [EXACT] [QUIET]
+                    [STRICTNESS <mode>]
+                    [ENV_MODE <mode>]
+                    [PC_LIBDIR <path>...]
+                    [PC_PATH <path>...]
+                    [DISABLE_UNINSTALLED <bool>]
+                    [PC_SYSROOT_DIR <path>]
+                    [TOP_BUILD_DIR <path>]
+                    [SYSTEM_INCLUDE_DIRS <path>...]
+                    [SYSTEM_LIBRARY_DIRS <path>...]
+                    [ALLOW_SYSTEM_INCLUDES <bool>]
+                    [ALLOW_SYSTEM_LIBS <bool>])
+
+The following variables will be populated from the contents of package file:
+
+==================================== ====== ========================================================================================
+              Variable                Type                       Definition
+==================================== ====== ========================================================================================
+``CMAKE_PKG_CONFIG_NAME``            String Value of the ``Name`` keyword
+``CMAKE_PKG_CONFIG_DESCRIPTION``     String Value of the ``Description`` keyword
+``CMAKE_PKG_CONFIG_VERSION``         String Value of the ``Version`` keyword
+``CMAKE_PKG_CONFIG_PROVIDES``        List   Value of the ``Provides`` keyword
+``CMAKE_PKG_CONFIG_REQUIRES``        List   Value of the ``Requires`` keyword
+``CMAKE_PKG_CONFIG_CONFLICTS``       List   Value of the ``Conflicts`` keyword
+``CMAKE_PKG_CONFIG_CFLAGS``          String Value of the ``CFlags`` / ``Cflags`` keyword
+``CMAKE_PKG_CONFIG_INCLUDES``        List   All ``-I`` prefixed flags from ``CMAKE_PKG_CONFIG_CFLAGS``
+``CMAKE_PKG_CONFIG_COMPILE_OPTIONS`` List   All flags not prefixed with ``-I`` from ``CMAKE_PKG_CONFIG_CFLAGS``
+``CMAKE_PKG_CONFIG_LIBS``            String Value of the ``Libs`` keyword
+``CMAKE_PKG_CONFIG_LIBDIRS``         List   All ``-L`` prefixed flags from ``CMAKE_PKG_CONFIG_LIBS``
+``CMAKE_PKG_CONFIG_LIBNAMES``        List   All ``-l`` prefixed flags from ``CMAKE_PKG_CONFIG_LIBS``
+``CMAKE_PKG_CONFIG_LINK_OPTIONS``    List   All flags not prefixed with ``-L`` or ``-l`` from ``CMAKE_PKG_CONFIG_LIBS``
+``CMAKE_PKG_CONFIG_*_PRIVATE``       \*     ``CFLAGS`` / ``LIBS`` / ``REQUIRES`` and derived, but in their ``.private`` suffix forms
+==================================== ====== ========================================================================================
+
+``SYSTEM_INCLUDE_DIRS``
+  Overrides the "system" directories for the purpose of flag mangling include
+  directories in ``CMAKE_PKG_CONFIG_CFLAGS`` and derived variables.
+
+  When this option is not provided, the default directories are provided by the
+  first available of the following values:
+
+  #. ``CMAKE_PKG_CONFIG_SYS_INCLUDE_DIRS``
+  #. The ``PKG_CONFIG_SYSTEM_INCLUDE_PATH`` environment variable
+  #. The output of ``pkgconf --variable pc_system_includedirs pkg-config``
+  #. A platform-dependent default value
+
+  Additionally, when the ``ENV_MODE`` is ``PKGCONF`` the
+  ``CMAKE_PKG_CONFIG_PKGCONF_INCLUDES`` variable will be concatenated to the
+  list if available. If it is not available, the following environment variables
+  will be queried and concatenated:
+
+  * ``CPATH``
+  * ``C_INCLUDE_PATH``
+  * ``CPLUS_INCLUDE_PATH``
+  * ``OBJC_INCLUDE_PATH``
+  * ``INCLUDE`` (Windows Only)
+
+``SYSTEM_LIBRARY_DIRS``
+  Overrides the "system" directories for the purpose of flag mangling library
+  directories in ``CMAKE_PKG_CONFIG_LIBS`` and derived variables.
+
+  When this option is not provided, the default directories are provided by the
+  first available of the following values:
+
+  #. ``CMAKE_PKG_CONFIG_SYS_LIB_DIRS``
+  #. The ``PKG_CONFIG_SYSTEM_LIBRARY_PATH`` environment variable
+  #. The output of ``pkgconf --variable pc_system_libdirs pkg-config``
+  #. A platform-dependent default value
+
+  Additionally, when the ``ENV_MODE`` is ``PKGCONF`` the
+  ``CMAKE_PKG_CONFIG_PKGCONF_LIB_DIRS`` variable will be concatenated to the
+  list if available. If it is not available, the ``LIBRARY_PATH`` environment
+  variable will be queried and concatenated.
+
+``ALLOW_SYSTEM_INCLUDES``
+  Preserves "system" directories during flag mangling of include directories
+  in ``CMAKE_PKG_CONFIG_CFLAGS`` and derived variables.
+
+  When this option is not provided, the default value is determined by the first
+  available of the following values:
+
+  #. ``CMAKE_PKG_CONFIG_ALLOW_SYS_INCLUDES``
+  #. If the ``PKG_CONFIG_ALLOW_SYSTEM_CFLAGS`` environment variable is defined
+     the flags are preserved, otherwise they are filtered during flag mangling.
+
+
+``ALLOW_SYSTEM_LIBS``
+  Preserves "system" directories during flag mangling of library directories
+  in ``CMAKE_PKG_CONFIG_LIBS`` and derived variables.
+
+  When this option is not provided, the default value is determined by the first
+  available of the following values:
+
+  #. ``CMAKE_PKG_CONFIG_ALLOW_SYS_LIBS``
+  #. If the ``PKG_CONFIG_ALLOW_SYSTEM_LIBS`` environment variable is defined
+     the flags are preserved, otherwise they are filtered during flag mangling.

+ 1 - 0
Help/manual/cmake-commands.7.rst

@@ -22,6 +22,7 @@ These commands are always available.
    /command/cmake_minimum_required
    /command/cmake_parse_arguments
    /command/cmake_path
+   /command/cmake_pkg_config
    /command/cmake_policy
    /command/configure_file
    /command/continue

+ 6 - 0
Source/CMakeLists.txt

@@ -373,6 +373,10 @@ add_library(
   cmLocalCommonGenerator.h
   cmLocalGenerator.cxx
   cmLocalGenerator.h
+  cmPkgConfigParser.cxx
+  cmPkgConfigParser.h
+  cmPkgConfigResolver.cxx
+  cmPkgConfigResolver.h
   cmPlaceholderExpander.cxx
   cmPlaceholderExpander.h
   cmRulePlaceholderExpander.cxx
@@ -537,6 +541,8 @@ add_library(
   cmCMakeMinimumRequired.h
   cmCMakePathCommand.h
   cmCMakePathCommand.cxx
+  cmCMakePkgConfigCommand.h
+  cmCMakePkgConfigCommand.cxx
   cmCMakePolicyCommand.cxx
   cmCMakePolicyCommand.h
   cmConditionEvaluator.cxx

+ 722 - 0
Source/cmCMakePkgConfigCommand.cxx

@@ -0,0 +1,722 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#include "cmCMakePkgConfigCommand.h"
+
+#include <cstdio>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <cm/filesystem>
+#include <cm/optional>
+#include <cm/string_view>
+#include <cmext/string_view>
+
+#include "cmsys/FStream.hxx"
+
+#include "cmArgumentParser.h"
+#include "cmArgumentParserTypes.h"
+#include "cmExecutionStatus.h"
+#include "cmList.h"
+#include "cmMakefile.h"
+#include "cmMessageType.h"
+#include "cmPkgConfigParser.h"
+#include "cmPkgConfigResolver.h"
+#include "cmStateTypes.h"
+#include "cmStringAlgorithms.h"
+#include "cmSubcommandTable.h"
+#include "cmSystemTools.h"
+#include "cmValue.h"
+#include <cmllpkgc/llpkgc.h>
+
+// IWYU wants this
+namespace {
+struct ExtractArguments;
+}
+
+namespace {
+
+cm::optional<std::string> GetPkgConfigBin(cmMakefile& mf)
+{
+  cm::optional<std::string> result;
+
+  auto pkgcfg = mf.GetDefinition("CMAKE_PKG_CONFIG_BIN");
+  if (pkgcfg.IsNOTFOUND()) {
+    return result;
+  }
+
+  if (pkgcfg) {
+    result = *pkgcfg;
+    return result;
+  }
+
+  std::string path = cmSystemTools::FindProgram("pkgconf");
+  if (path.empty()) {
+    path = cmSystemTools::FindProgram("pkg-config");
+    if (path.empty()) {
+      mf.AddCacheDefinition("CMAKE_PKG_CONFIG_BIN", "pkg-config-NOTFOUND",
+                            "Location of pkg-config or pkgconf binary",
+                            cmStateEnums::FILEPATH);
+      return result;
+    }
+  }
+
+  mf.AddCacheDefinition("CMAKE_PKG_CONFIG_BIN", path,
+                        "Location of pkg-config or pkgconf binary",
+                        cmStateEnums::FILEPATH);
+
+  result = std::move(path);
+  return result;
+}
+
+std::vector<std::string> GetLocations(cmMakefile& mf, const char* cachevar,
+                                      const char* envvar, const char* desc,
+                                      const char* pcvar, bool need_pkgconf,
+                                      std::vector<std::string> default_locs)
+{
+  auto def = mf.GetDefinition(cachevar);
+  if (def) {
+    return cmList(def);
+  }
+
+  std::string paths;
+  if (cmSystemTools::GetEnv(envvar, paths)) {
+    cmPkgConfigResolver::ReplaceSep(paths);
+    mf.AddCacheDefinition(cachevar, paths, desc, cmStateEnums::STRING);
+    return cmList(paths);
+  }
+
+  auto pkgcfg = GetPkgConfigBin(mf);
+  if (!pkgcfg || (need_pkgconf && (pkgcfg->find("pkgconf") == pkgcfg->npos))) {
+    mf.AddCacheDefinition(cachevar, cmList::to_string(default_locs), desc,
+                          cmStateEnums::STRING);
+    return default_locs;
+  }
+
+  std::string out;
+  cmSystemTools::RunSingleCommand({ *pkgcfg, pcvar, "pkg-config" }, &out,
+                                  nullptr, nullptr, nullptr,
+                                  cmSystemTools::OUTPUT_NONE);
+
+  cmPkgConfigResolver::ReplaceSep(out);
+  out = cmTrimWhitespace(out);
+  mf.AddCacheDefinition(cachevar, out, desc, cmStateEnums::STRING);
+  return cmList(out);
+}
+
+std::vector<std::string> GetPcLibDirs(cmMakefile& mf)
+{
+  std::vector<std::string> default_locs = {
+#ifndef _WIN32
+    "/usr/lib/pkgconfig", "/usr/share/pkgconfig"
+#endif
+  };
+  return GetLocations(mf, "CMAKE_PKG_CONFIG_PC_LIB_DIRS", "PKG_CONFIG_LIBDIR",
+                      "Default search locations for package files",
+                      "--variable=pc_path", false, std::move(default_locs));
+}
+
+std::vector<std::string> GetSysLibDirs(cmMakefile& mf)
+{
+  std::vector<std::string> default_locs = {
+#ifndef _WIN32
+    "/lib", "/usr/lib"
+#endif
+  };
+  return GetLocations(
+    mf, "CMAKE_PKG_CONFIG_SYS_LIB_DIRS", "PKG_CONFIG_SYSTEM_LIBRARY_PATH",
+    "System library directories filtered by flag mangling",
+    "--variable=pc_system_libdirs", true, std::move(default_locs));
+}
+
+std::vector<std::string> GetSysCflags(cmMakefile& mf)
+{
+  std::vector<std::string> default_locs = {
+#ifndef _WIN32
+    "/usr/include"
+#endif
+  };
+  return GetLocations(
+    mf, "CMAKE_PKG_CONFIG_SYS_INCLUDE_DIRS", "PKG_CONFIG_SYSTEM_INCLUDE_PATH",
+    "System include directories filtered by flag mangling",
+    "--variable=pc_system_includedirs", true, std::move(default_locs));
+}
+
+std::vector<std::string> GetPkgConfSysLibs(cmMakefile& mf)
+{
+  auto def = mf.GetDefinition("CMAKE_PKG_CONFIG_PKGCONF_LIB_DIRS");
+  if (def) {
+    return cmList(def);
+  }
+
+  std::string paths;
+  if (!cmSystemTools::GetEnv("LIBRARY_PATH", paths)) {
+    return {};
+  }
+
+  cmPkgConfigResolver::ReplaceSep(paths);
+  mf.AddCacheDefinition("CMAKE_PKG_CONFIG_PKGCONF_LIB_DIRS", paths,
+                        "Additional system library directories filtered by "
+                        "flag mangling in PKGCONF mode",
+                        cmStateEnums::STRING);
+  return cmList(paths);
+}
+
+std::vector<std::string> GetPkgConfSysCflags(cmMakefile& mf)
+{
+  auto def = mf.GetDefinition("CMAKE_PKG_CONFIG_PKGCONF_INCLUDES");
+  if (def) {
+    return cmList(def);
+  }
+
+  std::string paths;
+  auto get_and_append = [&](const char* var) {
+    if (paths.empty()) {
+      cmSystemTools::GetEnv(var, paths);
+    } else {
+      std::string tmp;
+      cmSystemTools::GetEnv(var, tmp);
+      if (!tmp.empty()) {
+        paths += ";" + tmp;
+      }
+    }
+  };
+
+  get_and_append("CPATH");
+  get_and_append("C_INCLUDE_PATH");
+  get_and_append("CPLUS_INCLUDE_PATH");
+  get_and_append("OBJC_INCLUDE_PATH");
+
+#ifdef _WIN32
+  get_and_append("INCLUDE");
+#endif
+
+  cmPkgConfigResolver::ReplaceSep(paths);
+  mf.AddCacheDefinition("CMAKE_PKG_CONFIG_PKGCONF_INCLUDES", paths,
+                        "Additional system include directories filtered by "
+                        "flag mangling in PKGCONF mode",
+                        cmStateEnums::STRING);
+  return cmList(paths);
+}
+
+std::vector<std::string> GetPcPath(cmMakefile& mf)
+{
+  auto def = mf.GetDefinition("CMAKE_PKG_CONFIG_PC_PATH");
+  if (def) {
+    return cmList(def);
+  }
+
+  std::string pcpath;
+  if (cmSystemTools::GetEnv("PKG_CONFIG_PATH", pcpath)) {
+    auto result = cmSystemTools::SplitString(pcpath, cmPkgConfigResolver::Sep);
+    mf.AddCacheDefinition(
+      "CMAKE_PKG_CONFIG_PC_PATH", cmList::to_string(result),
+      "Additional search locations for package files", cmStateEnums::STRING);
+    return result;
+  }
+
+  mf.AddCacheDefinition("CMAKE_PKG_CONFIG_PC_PATH", "",
+                        "Additional search locations for package files",
+                        cmStateEnums::STRING);
+  return {};
+}
+
+cm::optional<std::string> GetPath(cmMakefile& mf, const char* cachevar,
+                                  const char* envvar, const char* desc)
+{
+  cm::optional<std::string> result;
+
+  auto def = mf.GetDefinition(cachevar);
+  if (def) {
+    result = *def;
+    return result;
+  }
+
+  std::string path;
+  if (cmSystemTools::GetEnv(envvar, path)) {
+    mf.AddCacheDefinition(cachevar, path, desc, cmStateEnums::FILEPATH);
+    result = std::move(path);
+    return result;
+  }
+
+  return result;
+}
+
+cm::optional<std::string> GetSysrootDir(cmMakefile& mf)
+{
+  return GetPath(mf, "CMAKE_PKG_CONFIG_SYSROOT_DIR", "PKG_CONFIG_SYSROOT_DIR",
+                 "System root used for re-rooting package includes and "
+                 "library directories");
+}
+
+cm::optional<std::string> GetTopBuildDir(cmMakefile& mf)
+{
+  return GetPath(mf, "CMAKE_PKG_CONFIG_TOP_BUILD_DIR",
+                 "PKG_CONFIG_TOP_BUILD_DIR",
+                 "Package file top_build_dir variable default value");
+}
+
+bool GetBool(cmMakefile& mf, const char* cachevar, const char* envvar,
+             const char* desc)
+{
+  auto def = mf.GetDefinition(cachevar);
+  if (def) {
+    return def.IsOn();
+  }
+
+  if (cmSystemTools::HasEnv(envvar)) {
+    mf.AddCacheDefinition(cachevar, "ON", desc, cmStateEnums::BOOL);
+    return true;
+  }
+
+  return false;
+}
+
+bool GetDisableUninstalled(cmMakefile& mf)
+{
+  return GetBool(mf, "CMAKE_PKG_CONFIG_DISABLE_UNINSTALLED",
+                 "PKG_CONFIG_DISABLE_UNINSTALLED",
+                 "Disable search for `-uninstalled` (build tree) packages");
+}
+
+bool GetAllowSysLibs(cmMakefile& mf)
+{
+  return GetBool(mf, "CMAKE_PKG_CONFIG_ALLOW_SYS_LIBS",
+                 "PKG_CONFIG_ALLOW_SYSTEM_LIBS",
+                 "Allow system library directories during flag mangling");
+}
+
+bool GetAllowSysInclude(cmMakefile& mf)
+{
+  return GetBool(mf, "CMAKE_PKG_CONFIG_ALLOW_SYS_INCLUDES",
+                 "PKG_CONFIG_ALLOW_SYSTEM_CFLAGS",
+                 "Allow system include paths during flag manglging");
+}
+
+struct CommonArguments : ArgumentParser::ParseResult
+{
+  bool Required = false;
+  bool Exact = false;
+  bool Quiet = false;
+
+  enum StrictnessType
+  {
+    STRICTNESS_STRICT,
+    STRICTNESS_PERMISSIVE,
+    STRICTNESS_BEST_EFFORT,
+  };
+
+  StrictnessType Strictness = STRICTNESS_PERMISSIVE;
+  std::string StrictnessError;
+
+  ArgumentParser::Continue SetStrictness(cm::string_view strictness)
+  {
+    if (strictness == "STRICT"_s) {
+      Strictness = STRICTNESS_STRICT;
+    } else if (strictness == "PERMISSIVE"_s) {
+      Strictness = STRICTNESS_PERMISSIVE;
+    } else if (strictness == "BEST_EFFORT"_s) {
+      Strictness = STRICTNESS_BEST_EFFORT;
+    } else {
+      StrictnessError =
+        cmStrCat("Invalid 'STRICTNESS' '", strictness,
+                 "'; must be one of 'STRICT', 'PERMISSIVE', or 'BEST_EFFORT'");
+    }
+    return ArgumentParser::Continue::Yes;
+  }
+
+  enum EnvModeType
+  {
+    ENVMODE_FDO,
+    ENVMODE_PKGCONF,
+    ENVMODE_IGNORE,
+  };
+
+  EnvModeType EnvMode = ENVMODE_PKGCONF;
+  std::string EnvModeError;
+
+  ArgumentParser::Continue SetEnvMode(cm::string_view envMode)
+  {
+    if (envMode == "FDO"_s) {
+      EnvMode = ENVMODE_FDO;
+    } else if (envMode == "PKGCONF"_s) {
+      EnvMode = ENVMODE_PKGCONF;
+    } else if (envMode == "IGNORE"_s) {
+      EnvMode = ENVMODE_IGNORE;
+    } else {
+      EnvModeError =
+        cmStrCat("Invalid 'ENV_MODE' '", envMode,
+                 "'; must be one of 'FDO', 'PKGCONF', or 'IGNORE'");
+    }
+    return ArgumentParser::Continue::Yes;
+  }
+
+  cm::optional<std::string> Package;
+  cm::optional<std::string> Version;
+  cm::optional<std::string> SysrootDir;
+  cm::optional<std::string> TopBuildDir;
+
+  cm::optional<bool> DisableUninstalled;
+
+  cm::optional<ArgumentParser::MaybeEmpty<std::vector<std::string>>> PcPath;
+  cm::optional<ArgumentParser::MaybeEmpty<std::vector<std::string>>> PcLibdir;
+
+  bool CheckArgs(cmExecutionStatus& status) const
+  {
+
+    if (!Package) {
+      status.SetError("A package name or absolute path must be specified");
+      return false;
+    }
+
+    if (!StrictnessError.empty()) {
+      status.SetError(StrictnessError);
+      return false;
+    }
+
+    if (!EnvModeError.empty()) {
+      status.SetError(EnvModeError);
+      return false;
+    }
+
+    return true;
+  }
+};
+
+#define BIND_COMMON(argtype)                                                  \
+  (cmArgumentParser<argtype>{})                                               \
+    .Bind(1, &argtype::Package)                                               \
+    .Bind(2, &argtype::Version)                                               \
+    .Bind("REQUIRED"_s, &argtype::Required)                                   \
+    .Bind("EXACT"_s, &argtype::Exact)                                         \
+    .Bind("QUIET"_s, &argtype::Quiet)                                         \
+    .Bind("STRICTNESS"_s, &argtype::SetStrictness)                            \
+    .Bind("ENV_MODE"_s, &argtype::SetEnvMode)                                 \
+    .Bind("PC_SYSROOT_DIR"_s, &argtype::SysrootDir)                           \
+    .Bind("TOP_BUILD_DIR"_s, &argtype::TopBuildDir)                           \
+    .Bind("DISABLE_UNINSTALLED"_s, &argtype::DisableUninstalled)              \
+    .Bind("PC_LIBDIR"_s, &argtype::PcLibdir)                                  \
+    .Bind("PC_PATH"_s, &argtype::PcPath)
+
+void CollectEnv(cmMakefile& mf, cmPkgConfigEnv& env,
+                CommonArguments::EnvModeType mode)
+{
+  if (mode == CommonArguments::EnvModeType::ENVMODE_IGNORE) {
+    return;
+  }
+
+  if (!env.Path) {
+    env.Path = GetPcPath(mf);
+  }
+
+  if (!env.LibDirs) {
+    env.LibDirs = GetPcLibDirs(mf);
+  }
+
+  if (!env.DisableUninstalled) {
+    env.DisableUninstalled = GetDisableUninstalled(mf);
+  }
+
+  if (!env.SysrootDir) {
+    env.SysrootDir = GetSysrootDir(mf);
+  }
+
+  if (!env.TopBuildDir) {
+    env.TopBuildDir = GetTopBuildDir(mf);
+  }
+
+  env.AllowSysCflags = GetAllowSysInclude(mf);
+  env.SysCflags = GetSysCflags(mf);
+
+  env.AllowSysLibs = GetAllowSysLibs(mf);
+  env.SysLibs = GetSysLibDirs(mf);
+
+  if (mode == CommonArguments::EnvModeType::ENVMODE_FDO) {
+    return;
+  }
+
+  *env.SysCflags += GetPkgConfSysCflags(mf);
+  *env.SysLibs += GetPkgConfSysLibs(mf);
+}
+
+cm::optional<cmPkgConfigResult> HandleCommon(CommonArguments& args,
+                                             cmExecutionStatus& status)
+{
+
+  auto& mf = status.GetMakefile();
+
+  if (!args.CheckArgs(status)) {
+    return {};
+  }
+
+  auto warn_or_error = [&](const std::string& err) {
+    if (args.Required) {
+      status.SetError(err);
+      cmSystemTools::SetFatalErrorOccurred();
+    } else if (!args.Quiet) {
+      mf.IssueMessage(MessageType::WARNING, err);
+    }
+  };
+
+  cm::filesystem::path path{ *args.Package };
+
+  cmPkgConfigEnv env;
+
+  if (args.PcLibdir) {
+    env.LibDirs = std::move(*args.PcLibdir);
+  }
+
+  if (args.PcPath) {
+    env.Path = std::move(*args.PcPath);
+  }
+
+  if (args.DisableUninstalled) {
+    env.DisableUninstalled = args.DisableUninstalled;
+  }
+
+  if (args.SysrootDir) {
+    env.SysrootDir = std::move(*args.SysrootDir);
+  }
+
+  if (args.TopBuildDir) {
+    env.TopBuildDir = std::move(*args.TopBuildDir);
+  }
+
+  CollectEnv(mf, env, args.EnvMode);
+
+  if (path.extension() == ".pc") {
+    if (!cmSystemTools::FileExists(path.string())) {
+      warn_or_error(cmStrCat("Could not find '", *args.Package, "'"));
+      return {};
+    }
+  } else {
+
+    std::vector<std::string> search;
+    if (env.Path) {
+      search = *env.Path;
+      if (env.LibDirs) {
+        search += *env.LibDirs;
+      }
+    } else if (env.LibDirs) {
+      search = *env.LibDirs;
+    }
+
+    if (env.DisableUninstalled && !*env.DisableUninstalled) {
+      auto uninstalled = path;
+      uninstalled.concat("-uninstalled.pc");
+      uninstalled =
+        cmSystemTools::FindFile(uninstalled.string(), search, true);
+      if (uninstalled.empty()) {
+        path =
+          cmSystemTools::FindFile(path.concat(".pc").string(), search, true);
+        if (path.empty()) {
+          warn_or_error(cmStrCat("Could not find '", *args.Package, "'"));
+          return {};
+        }
+      } else {
+        path = uninstalled;
+      }
+    } else {
+      path =
+        cmSystemTools::FindFile(path.concat(".pc").string(), search, true);
+      if (path.empty()) {
+        warn_or_error(cmStrCat("Could not find '", *args.Package, "'"));
+        return {};
+      }
+    }
+  }
+
+  auto len = cmSystemTools::FileLength(path.string());
+
+  // Windows requires this weird string -> c_str dance
+  cmsys::ifstream ifs(path.string().c_str(), std::ios::binary);
+
+  if (!ifs) {
+    warn_or_error(cmStrCat("Could not open file '", path.string(), "'"));
+    return {};
+  }
+
+  std::unique_ptr<char[]> buf(new char[len]);
+  ifs.read(buf.get(), len);
+
+  // Shouldn't have hit eof on previous read, should hit eof now
+  if (ifs.fail() || ifs.eof() || ifs.get() != EOF) {
+    warn_or_error(cmStrCat("Error while reading file '", path.string(), "'"));
+    return {};
+  }
+
+  using StrictnessType = CommonArguments::StrictnessType;
+
+  cmPkgConfigParser parser;
+  auto err = parser.Finish(buf.get(), len);
+
+  if (args.Strictness != StrictnessType::STRICTNESS_BEST_EFFORT &&
+      err != PCE_OK) {
+    warn_or_error(cmStrCat("Parsing failed for file '", path.string(), "'"));
+    return {};
+  }
+
+  cm::optional<cmPkgConfigResult> result;
+  if (args.Strictness == StrictnessType::STRICTNESS_STRICT) {
+    result = cmPkgConfigResolver::ResolveStrict(parser.Data(), std::move(env));
+  } else if (args.Strictness == StrictnessType::STRICTNESS_PERMISSIVE) {
+    result =
+      cmPkgConfigResolver::ResolvePermissive(parser.Data(), std::move(env));
+  } else {
+    result =
+      cmPkgConfigResolver::ResolveBestEffort(parser.Data(), std::move(env));
+  }
+
+  if (!result) {
+    warn_or_error(
+      cmStrCat("Resolution failed for file '", path.string(), "'"));
+  } else if (args.Exact) {
+    std::string ver;
+
+    if (args.Version) {
+      ver = cmPkgConfigResolver::ParseVersion(*args.Version).Version;
+    }
+
+    if (ver != result->Version()) {
+      warn_or_error(
+        cmStrCat("Package '", *args.Package, "' version '", result->Version(),
+                 "' does not meet exact version requirement '", ver, "'"));
+      return {};
+    }
+
+  } else if (args.Version) {
+    auto rv = cmPkgConfigResolver::ParseVersion(*args.Version);
+    if (!cmPkgConfigResolver::CheckVersion(rv, result->Version())) {
+      warn_or_error(
+        cmStrCat("Package '", *args.Package, "' version '", result->Version(),
+                 "' does not meet version requirement '", *args.Version, "'"));
+      return {};
+    }
+  }
+
+  return result;
+}
+
+struct ExtractArguments : CommonArguments
+{
+  cm::optional<bool> AllowSystemIncludes;
+  cm::optional<bool> AllowSystemLibs;
+
+  cm::optional<ArgumentParser::MaybeEmpty<std::vector<std::string>>>
+    SystemIncludeDirs;
+  cm::optional<ArgumentParser::MaybeEmpty<std::vector<std::string>>>
+    SystemLibraryDirs;
+};
+
+const auto ExtractParser =
+  BIND_COMMON(ExtractArguments)
+    .Bind("ALLOW_SYSTEM_INCLUDES"_s, &ExtractArguments::AllowSystemIncludes)
+    .Bind("ALLOW_SYSTEM_LIBS"_s, &ExtractArguments::AllowSystemLibs)
+    .Bind("SYSTEM_INCLUDE_DIRS"_s, &ExtractArguments::SystemIncludeDirs)
+    .Bind("SYSTEM_LIBRARY_DIRS"_s, &ExtractArguments::SystemLibraryDirs);
+
+bool HandleExtractCommand(std::vector<std::string> const& args,
+                          cmExecutionStatus& status)
+{
+
+  std::vector<std::string> unparsed;
+  auto parsedArgs = ExtractParser.Parse(args, &unparsed);
+  auto maybeResolved = HandleCommon(parsedArgs, status);
+
+  if (!maybeResolved) {
+    return !parsedArgs.Required;
+  }
+
+  auto& resolved = *maybeResolved;
+  auto version = resolved.Version();
+
+  if (parsedArgs.AllowSystemIncludes) {
+    resolved.env.AllowSysCflags = *parsedArgs.AllowSystemIncludes;
+  }
+
+  if (parsedArgs.AllowSystemLibs) {
+    resolved.env.AllowSysLibs = *parsedArgs.AllowSystemLibs;
+  }
+
+  if (parsedArgs.SystemIncludeDirs) {
+    resolved.env.SysCflags = *parsedArgs.SystemIncludeDirs;
+  }
+
+  if (parsedArgs.SystemLibraryDirs) {
+    resolved.env.SysLibs = *parsedArgs.SystemLibraryDirs;
+  }
+
+  auto& mf = status.GetMakefile();
+  mf.AddDefinition("CMAKE_PKG_CONFIG_NAME", resolved.Name());
+  mf.AddDefinition("CMAKE_PKG_CONFIG_DESCRIPTION", resolved.Description());
+  mf.AddDefinition("CMAKE_PKG_CONFIG_VERSION", version);
+
+  auto make_list = [&](const char* def,
+                       const std::vector<cmPkgConfigDependency>& deps) {
+    std::vector<cm::string_view> vec;
+    vec.reserve(deps.size());
+
+    for (const auto& dep : deps) {
+      vec.emplace_back(dep.Name);
+    }
+
+    mf.AddDefinition(def, cmList::to_string(vec));
+  };
+
+  make_list("CMAKE_PKG_CONFIG_CONFLICTS", resolved.Conflicts());
+  make_list("CMAKE_PKG_CONFIG_PROVIDES", resolved.Provides());
+  make_list("CMAKE_PKG_CONFIG_REQUIRES", resolved.Requires());
+  make_list("CMAKE_PKG_CONFIG_REQUIRES_PRIVATE", resolved.Requires(true));
+
+  auto cflags = resolved.Cflags();
+  mf.AddDefinition("CMAKE_PKG_CONFIG_CFLAGS", cflags.Flagline);
+  mf.AddDefinition("CMAKE_PKG_CONFIG_INCLUDES",
+                   cmList::to_string(cflags.Includes));
+  mf.AddDefinition("CMAKE_PKG_CONFIG_COMPILE_OPTIONS",
+                   cmList::to_string(cflags.CompileOptions));
+
+  cflags = resolved.Cflags(true);
+  mf.AddDefinition("CMAKE_PKG_CONFIG_CFLAGS_PRIVATE", cflags.Flagline);
+  mf.AddDefinition("CMAKE_PKG_CONFIG_INCLUDES_PRIVATE",
+                   cmList::to_string(cflags.Includes));
+  mf.AddDefinition("CMAKE_PKG_CONFIG_COMPILE_OPTIONS_PRIVATE",
+                   cmList::to_string(cflags.CompileOptions));
+
+  auto libs = resolved.Libs();
+  mf.AddDefinition("CMAKE_PKG_CONFIG_LIBS", libs.Flagline);
+  mf.AddDefinition("CMAKE_PKG_CONFIG_LIBDIRS",
+                   cmList::to_string(libs.LibDirs));
+  mf.AddDefinition("CMAKE_PKG_CONFIG_LIBNAMES",
+                   cmList::to_string(libs.LibNames));
+  mf.AddDefinition("CMAKE_PKG_CONFIG_LINK_OPTIONS",
+                   cmList::to_string(libs.LinkOptions));
+
+  libs = resolved.Libs(true);
+  mf.AddDefinition("CMAKE_PKG_CONFIG_LIBS_PRIVATE", libs.Flagline);
+  mf.AddDefinition("CMAKE_PKG_CONFIG_LIBDIRS_PRIVATE",
+                   cmList::to_string(libs.LibDirs));
+  mf.AddDefinition("CMAKE_PKG_CONFIG_LIBNAMES_PRIVATE",
+                   cmList::to_string(libs.LibNames));
+  mf.AddDefinition("CMAKE_PKG_CONFIG_LINK_OPTIONS_PRIVATE",
+                   cmList::to_string(libs.LinkOptions));
+
+  return true;
+}
+} // namespace
+
+bool cmCMakePkgConfigCommand(std::vector<std::string> const& args,
+                             cmExecutionStatus& status)
+{
+  if (args.size() < 2) {
+    status.SetError("must be called with at least two arguments.");
+    return false;
+  }
+
+  static cmSubcommandTable const subcommand{
+    { "EXTRACT"_s, HandleExtractCommand },
+  };
+
+  return subcommand(args[0], args, status);
+}

+ 13 - 0
Source/cmCMakePkgConfigCommand.h

@@ -0,0 +1,13 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <string>
+#include <vector>
+
+class cmExecutionStatus;
+
+bool cmCMakePkgConfigCommand(std::vector<std::string> const& args,
+                             cmExecutionStatus& status);

+ 2 - 0
Source/cmCommands.cxx

@@ -94,6 +94,7 @@
 #  include "cmAuxSourceDirectoryCommand.h"
 #  include "cmBuildNameCommand.h"
 #  include "cmCMakeHostSystemInformationCommand.h"
+#  include "cmCMakePkgConfigCommand.h"
 #  include "cmExportCommand.h"
 #  include "cmExportLibraryDependenciesCommand.h"
 #  include "cmFLTKWrapUICommand.h"
@@ -208,6 +209,7 @@ void GetScriptingCommands(cmState* state)
 #if !defined(CMAKE_BOOTSTRAP)
   state->AddBuiltinCommand("cmake_host_system_information",
                            cmCMakeHostSystemInformationCommand);
+  state->AddBuiltinCommand("cmake_pkg_config", cmCMakePkgConfigCommand);
   state->AddBuiltinCommand("load_cache", cmLoadCacheCommand);
   state->AddBuiltinCommand("remove", cmRemoveCommand);
   state->AddBuiltinCommand("variable_watch", cmVariableWatchCommand);

+ 151 - 0
Source/cmPkgConfigParser.cxx

@@ -0,0 +1,151 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#include "cmPkgConfigParser.h"
+
+#include <cstddef>
+#include <vector>
+
+#include <cm/string_view>
+
+#include <cmllpkgc/llpkgc.h>
+
+cmPkgConfigValueElement::cmPkgConfigValueElement(bool isVariable,
+                                                 cm::string_view data)
+  : IsVariable{ isVariable }
+  , Data{ data }
+{
+}
+
+cmPkgConfigEntry::cmPkgConfigEntry(bool isVariable, cm::string_view key)
+  : IsVariable{ isVariable }
+  , Key{ key }
+{
+}
+
+cmPkgConfigParser::cmPkgConfigParser()
+{
+  llpkgc_init(static_cast<llpkgc_t*>(this), &Settings_);
+}
+
+llpkgc_errno_t cmPkgConfigParser::Parse(char* buf, std::size_t len)
+{
+  return llpkgc_execute(static_cast<llpkgc_t*>(this), buf, len);
+}
+
+llpkgc_errno_t cmPkgConfigParser::Finish()
+{
+  return llpkgc_finish(static_cast<llpkgc_t*>(this));
+}
+
+llpkgc_errno_t cmPkgConfigParser::Finish(char* buf, std::size_t len)
+{
+  Parse(buf, len);
+  return llpkgc_finish(static_cast<llpkgc_t*>(this));
+}
+
+int cmPkgConfigParser::OnSpanNext(const char*, std::size_t len)
+{
+  Len_ += len;
+  return 0;
+}
+
+int cmPkgConfigParser::OnSpanNextTr(llpkgc_t* parser, const char* at,
+                                    std::size_t len)
+{
+  return static_cast<cmPkgConfigParser*>(parser)->OnSpanNext(at, len);
+}
+
+int cmPkgConfigParser::OnKey(const char* at, std::size_t len)
+{
+  Ptr_ = at;
+  Len_ = len;
+  Settings_.on_key = OnSpanNextTr;
+  return 0;
+}
+
+int cmPkgConfigParser::OnKeyTr(llpkgc_t* parser, const char* at,
+                               std::size_t len)
+{
+  return static_cast<cmPkgConfigParser*>(parser)->OnKey(at, len);
+}
+
+int cmPkgConfigParser::OnKeywordComplete()
+{
+  Data_.emplace_back(false, cm::string_view{ Ptr_, Len_ });
+  Settings_.on_key = OnKeyTr;
+  return 0;
+}
+
+int cmPkgConfigParser::OnKeywordCompleteTr(llpkgc_t* parser)
+{
+  return static_cast<cmPkgConfigParser*>(parser)->OnKeywordComplete();
+}
+
+int cmPkgConfigParser::OnVariableComplete()
+{
+  Data_.emplace_back(true, cm::string_view{ Ptr_, Len_ });
+  Settings_.on_key = OnKeyTr;
+  return 0;
+}
+
+int cmPkgConfigParser::OnVariableCompleteTr(llpkgc_t* parser)
+{
+  return static_cast<cmPkgConfigParser*>(parser)->OnVariableComplete();
+}
+
+int cmPkgConfigParser::OnValueLiteral(const char* at, std::size_t len)
+{
+  Ptr_ = at;
+  Len_ = len;
+  Settings_.on_value_literal = OnSpanNextTr;
+  return 0;
+}
+
+int cmPkgConfigParser::OnValueLiteralTr(llpkgc_t* parser, const char* at,
+                                        std::size_t len)
+{
+  return static_cast<cmPkgConfigParser*>(parser)->OnValueLiteral(at, len);
+}
+
+int cmPkgConfigParser::OnValueLiteralComplete()
+{
+  Settings_.on_value_literal = OnValueLiteralTr;
+
+  if (Len_) {
+    Data_.back().Val.emplace_back(false, cm::string_view{ Ptr_, Len_ });
+  }
+
+  return 0;
+}
+
+int cmPkgConfigParser::OnValueLiteralCompleteTr(llpkgc_t* parser)
+{
+  return static_cast<cmPkgConfigParser*>(parser)->OnValueLiteralComplete();
+}
+
+int cmPkgConfigParser::OnValueVariable(const char* at, std::size_t len)
+{
+  Ptr_ = at;
+  Len_ = len;
+  Settings_.on_value_variable = OnSpanNextTr;
+  return 0;
+}
+
+int cmPkgConfigParser::OnValueVariableTr(llpkgc_t* parser, const char* at,
+                                         std::size_t len)
+{
+  return static_cast<cmPkgConfigParser*>(parser)->OnValueVariable(at, len);
+}
+
+int cmPkgConfigParser::OnValueVariableComplete()
+{
+  Settings_.on_value_variable = OnValueVariableTr;
+  Data_.back().Val.emplace_back(true, cm::string_view{ Ptr_, Len_ });
+  return 0;
+}
+
+int cmPkgConfigParser::OnValueVariableCompleteTr(llpkgc_t* parser)
+{
+  return static_cast<cmPkgConfigParser*>(parser)->OnValueVariableComplete();
+}

+ 93 - 0
Source/cmPkgConfigParser.h

@@ -0,0 +1,93 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <cstddef>
+#include <vector>
+
+#include <cm/string_view>
+
+#include <cmllpkgc/llpkgc.h>
+
+struct cmPkgConfigValueElement
+{
+
+  cmPkgConfigValueElement() = default;
+
+  cmPkgConfigValueElement(bool isVariable, cm::string_view data);
+
+  bool IsVariable;
+  cm::string_view Data;
+};
+
+struct cmPkgConfigEntry
+{
+
+  cmPkgConfigEntry() = default;
+
+  cmPkgConfigEntry(bool isVariable, cm::string_view key);
+
+  bool IsVariable;
+  cm::string_view Key;
+  std::vector<cmPkgConfigValueElement> Val;
+};
+
+class cmPkgConfigParser : llpkgc_t
+{
+public:
+  cmPkgConfigParser();
+
+  llpkgc_errno_t Parse(char* buf, std::size_t len);
+
+  llpkgc_errno_t Finish();
+  llpkgc_errno_t Finish(char* buf, std::size_t len);
+
+  std::vector<cmPkgConfigEntry>& Data() { return Data_; }
+
+private:
+  int OnSpanNext(const char*, std::size_t len);
+  static int OnSpanNextTr(llpkgc_t* parser, const char* at, std::size_t len);
+
+  int OnKey(const char* at, std::size_t len);
+  static int OnKeyTr(llpkgc_t* parser, const char* at, std::size_t len);
+
+  int OnKeywordComplete();
+  static int OnKeywordCompleteTr(llpkgc_t* parser);
+
+  int OnVariableComplete();
+  static int OnVariableCompleteTr(llpkgc_t* parser);
+
+  int OnValueLiteral(const char* at, std::size_t len);
+  static int OnValueLiteralTr(llpkgc_t* parser, const char* at,
+                              std::size_t len);
+
+  int OnValueLiteralComplete();
+  static int OnValueLiteralCompleteTr(llpkgc_t* parser);
+
+  int OnValueVariable(const char* at, std::size_t len);
+  static int OnValueVariableTr(llpkgc_t* parser, const char* at,
+                               std::size_t len);
+
+  int OnValueVariableComplete();
+  static int OnValueVariableCompleteTr(llpkgc_t* parser);
+
+  llpkgc_settings_t Settings_{
+    OnKeyTr,
+    OnValueLiteralTr,
+    OnValueVariableTr,
+    nullptr, // on_line_begin
+    OnKeywordCompleteTr,
+    OnVariableCompleteTr,
+    OnValueLiteralCompleteTr,
+    OnValueVariableCompleteTr,
+    nullptr, // on_value_complete
+    nullptr, // on_pkgc_complete
+  };
+
+  const char* Ptr_;
+  std::size_t Len_;
+  std::vector<cmPkgConfigEntry> Data_;
+};

+ 870 - 0
Source/cmPkgConfigResolver.cxx

@@ -0,0 +1,870 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#include "cmPkgConfigResolver.h"
+
+#include <algorithm>
+#include <cctype>
+#include <cstring>
+#include <iterator>
+#include <string>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include <cm/optional>
+#include <cm/string_view>
+
+#include "cmPkgConfigParser.h"
+
+namespace {
+
+void TrimBack(std::string& str)
+{
+  if (!str.empty()) {
+    auto it = str.end() - 1;
+    for (; std::isspace(*it); --it) {
+      if (it == str.begin()) {
+        str.clear();
+        return;
+      }
+    }
+    str.erase(++it, str.end());
+  }
+}
+
+std::string AppendAndTrim(std::string& str, cm::string_view sv)
+{
+  auto size = str.length();
+  str += sv;
+  if (str.empty()) {
+    return {};
+  }
+
+  auto begin = str.begin() + size;
+  auto cur = str.end() - 1;
+
+  while (cur != begin && std::isspace(*cur)) {
+    --cur;
+  }
+
+  if (std::isspace(*cur)) {
+    return {};
+  }
+
+  return { &*begin, static_cast<std::size_t>(cur - begin) + 1 };
+}
+
+} // namespace
+
+std::string cmPkgConfigResult::StrOrDefault(const std::string& key,
+                                            cm::string_view def)
+{
+  auto it = Keywords.find(key);
+  return it == Keywords.end() ? std::string{ def } : it->second;
+};
+
+std::string cmPkgConfigResult::Name()
+{
+  return StrOrDefault("Name");
+}
+
+std::string cmPkgConfigResult::Description()
+{
+  return StrOrDefault("Description");
+}
+
+std::string cmPkgConfigResult::Version()
+{
+  return StrOrDefault("Version");
+}
+
+std::vector<cmPkgConfigDependency> cmPkgConfigResult::Conflicts()
+{
+  auto it = Keywords.find("Conflicts");
+  if (it == Keywords.end()) {
+    return {};
+  }
+
+  return cmPkgConfigResolver::ParseDependencies(it->second);
+}
+
+std::vector<cmPkgConfigDependency> cmPkgConfigResult::Provides()
+{
+  auto it = Keywords.find("Provides");
+  if (it == Keywords.end()) {
+    return {};
+  }
+
+  return cmPkgConfigResolver::ParseDependencies(it->second);
+}
+
+std::vector<cmPkgConfigDependency> cmPkgConfigResult::Requires(bool priv)
+{
+  auto it = Keywords.find(priv ? "Requires.private" : "Requires");
+  if (it == Keywords.end()) {
+    return {};
+  }
+
+  return cmPkgConfigResolver::ParseDependencies(it->second);
+}
+
+cmPkgConfigCflagsResult cmPkgConfigResult::Cflags(bool priv)
+{
+  std::string cflags;
+  auto it = Keywords.find(priv ? "Cflags.private" : "Cflags");
+  if (it != Keywords.end()) {
+    cflags += it->second;
+  }
+
+  it = Keywords.find(priv ? "CFlags.private" : "CFlags");
+  if (it != Keywords.end()) {
+    if (!cflags.empty()) {
+      cflags += " ";
+    }
+    cflags += it->second;
+  }
+
+  auto tokens = cmPkgConfigResolver::TokenizeFlags(cflags);
+
+  if (env.AllowSysCflags) {
+    if (env.SysrootDir) {
+      return cmPkgConfigResolver::MangleCflags(tokens, *env.SysrootDir);
+    }
+    return cmPkgConfigResolver::MangleCflags(tokens);
+  }
+
+  if (env.SysCflags) {
+    if (env.SysrootDir) {
+      return cmPkgConfigResolver::MangleCflags(tokens, *env.SysrootDir,
+                                               *env.SysCflags);
+    }
+    return cmPkgConfigResolver::MangleCflags(tokens, *env.SysCflags);
+  }
+
+  if (env.SysrootDir) {
+    return cmPkgConfigResolver::MangleCflags(
+      tokens, *env.SysrootDir, std::vector<std::string>{ "/usr/include" });
+  }
+
+  return cmPkgConfigResolver::MangleCflags(
+    tokens, std::vector<std::string>{ "/usr/include" });
+}
+
+cmPkgConfigLibsResult cmPkgConfigResult::Libs(bool priv)
+{
+  auto it = Keywords.find(priv ? "Libs.private" : "Libs");
+  if (it == Keywords.end()) {
+    return cmPkgConfigLibsResult();
+  }
+
+  auto tokens = cmPkgConfigResolver::TokenizeFlags(it->second);
+
+  if (env.AllowSysLibs) {
+    if (env.SysrootDir) {
+      return cmPkgConfigResolver::MangleLibs(tokens, *env.SysrootDir);
+    }
+    return cmPkgConfigResolver::MangleLibs(tokens);
+  }
+
+  if (env.SysLibs) {
+    if (env.SysrootDir) {
+      return cmPkgConfigResolver::MangleLibs(tokens, *env.SysrootDir,
+                                             *env.SysLibs);
+    }
+    return cmPkgConfigResolver::MangleLibs(tokens, *env.SysLibs);
+  }
+
+  if (env.SysrootDir) {
+    return cmPkgConfigResolver::MangleLibs(
+      tokens, *env.SysrootDir, std::vector<std::string>{ "/usr/lib" });
+  }
+
+  return cmPkgConfigResolver::MangleLibs(
+    tokens, std::vector<std::string>{ "/usr/lib" });
+}
+
+void cmPkgConfigResolver::ReplaceSep(std::string& list)
+{
+#ifndef _WIN32
+  std::replace(list.begin(), list.end(), ':', ';');
+#else
+  static_cast<void>(list); // Unused parameter
+#endif
+}
+
+cm::optional<cmPkgConfigResult> cmPkgConfigResolver::ResolveStrict(
+  const std::vector<cmPkgConfigEntry>& entries, cmPkgConfigEnv env)
+{
+  cm::optional<cmPkgConfigResult> result;
+  cmPkgConfigResult config;
+  auto& keys = config.Keywords;
+
+  if (env.SysrootDir) {
+    config.Variables["pc_sysrootdir"] = *env.SysrootDir;
+  } else {
+    config.Variables["pc_sysrootdir"] = "/";
+  }
+
+  if (env.TopBuildDir) {
+    config.Variables["pc_top_builddir"] = *env.TopBuildDir;
+  }
+
+  config.env = std::move(env);
+
+  for (const auto& entry : entries) {
+    std::string key(entry.Key);
+    if (entry.IsVariable) {
+      if (config.Variables.find(key) != config.Variables.end()) {
+        return result;
+      }
+      auto var = HandleVariableStrict(entry, config.Variables);
+      if (!var) {
+        return result;
+      }
+      config.Variables[key] = *var;
+    } else {
+      if (key == "Cflags" && keys.find("CFlags") != keys.end()) {
+        return result;
+      }
+      if (key == "CFlags" && keys.find("Cflags") != keys.end()) {
+        return result;
+      }
+      if (key == "Cflags.private" &&
+          keys.find("CFlags.private") != keys.end()) {
+        return result;
+      }
+      if (key == "CFlags.private" &&
+          keys.find("Cflags.private") != keys.end()) {
+        return result;
+      }
+      if (keys.find(key) != keys.end()) {
+        return result;
+      }
+      keys[key] = HandleKeyword(entry, config.Variables);
+    }
+  }
+
+  if (keys.find("Name") == keys.end() ||
+      keys.find("Description") == keys.end() ||
+      keys.find("Version") == keys.end()) {
+    return result;
+  }
+
+  result = std::move(config);
+  return result;
+}
+
+cm::optional<cmPkgConfigResult> cmPkgConfigResolver::ResolvePermissive(
+  const std::vector<cmPkgConfigEntry>& entries, cmPkgConfigEnv env)
+{
+  cm::optional<cmPkgConfigResult> result;
+
+  cmPkgConfigResult config = ResolveBestEffort(entries, std::move(env));
+  const auto& keys = config.Keywords;
+
+  if (keys.find("Name") == keys.end() ||
+      keys.find("Description") == keys.end() ||
+      keys.find("Version") == keys.end()) {
+    return result;
+  }
+
+  result = std::move(config);
+  return result;
+}
+
+cmPkgConfigResult cmPkgConfigResolver::ResolveBestEffort(
+  const std::vector<cmPkgConfigEntry>& entries, cmPkgConfigEnv env)
+{
+  cmPkgConfigResult result;
+
+  if (env.SysrootDir) {
+    result.Variables["pc_sysrootdir"] = *env.SysrootDir;
+  } else {
+    result.Variables["pc_sysrootdir"] = "/";
+  }
+
+  if (env.TopBuildDir) {
+    result.Variables["pc_top_builddir"] = *env.TopBuildDir;
+  }
+
+  result.env = std::move(env);
+
+  for (const auto& entry : entries) {
+    std::string key(entry.Key);
+    if (entry.IsVariable) {
+      result.Variables[key] =
+        HandleVariablePermissive(entry, result.Variables);
+    } else {
+      result.Keywords[key] += HandleKeyword(entry, result.Variables);
+    }
+  }
+  return result;
+}
+
+std::string cmPkgConfigResolver::HandleVariablePermissive(
+  const cmPkgConfigEntry& entry,
+  const std::unordered_map<std::string, std::string>& variables)
+{
+  std::string result;
+  for (const auto& segment : entry.Val) {
+    if (!segment.IsVariable) {
+      result += segment.Data;
+    } else if (entry.Key != segment.Data) {
+      auto it = variables.find(std::string{ segment.Data });
+      if (it != variables.end()) {
+        result += it->second;
+      }
+    }
+  }
+
+  TrimBack(result);
+  return result;
+}
+
+cm::optional<std::string> cmPkgConfigResolver::HandleVariableStrict(
+  const cmPkgConfigEntry& entry,
+  const std::unordered_map<std::string, std::string>& variables)
+{
+  cm::optional<std::string> result;
+
+  std::string value;
+  for (const auto& segment : entry.Val) {
+    if (!segment.IsVariable) {
+      value += segment.Data;
+    } else if (entry.Key == segment.Data) {
+      return result;
+    } else {
+      auto it = variables.find(std::string{ segment.Data });
+      if (it != variables.end()) {
+        value += it->second;
+      } else {
+        return result;
+      }
+    }
+  }
+
+  TrimBack(value);
+  result = std::move(value);
+  return result;
+}
+
+std::string cmPkgConfigResolver::HandleKeyword(
+  const cmPkgConfigEntry& entry,
+  const std::unordered_map<std::string, std::string>& variables)
+{
+  std::string result;
+  for (const auto& segment : entry.Val) {
+    if (!segment.IsVariable) {
+      result += segment.Data;
+    } else {
+      auto it = variables.find(std::string{ segment.Data });
+      if (it != variables.end()) {
+        result += it->second;
+      }
+    }
+  }
+
+  TrimBack(result);
+  return result;
+}
+
+std::vector<cm::string_view> cmPkgConfigResolver::TokenizeFlags(
+  const std::string& flagline)
+{
+  std::vector<cm::string_view> result;
+
+  auto it = flagline.begin();
+  while (it != flagline.end() && std::isspace(*it)) {
+    ++it;
+  }
+
+  while (it != flagline.end()) {
+    const char* start = &(*it);
+    std::size_t len = 0;
+
+    for (; it != flagline.end() && !std::isspace(*it); ++it) {
+      ++len;
+    }
+
+    for (; it != flagline.end() && std::isspace(*it); ++it) {
+      ++len;
+    }
+
+    result.emplace_back(start, len);
+  }
+
+  return result;
+}
+
+cmPkgConfigCflagsResult cmPkgConfigResolver::MangleCflags(
+  const std::vector<cm::string_view>& flags)
+{
+  cmPkgConfigCflagsResult result;
+
+  for (auto flag : flags) {
+    if (flag.rfind("-I", 0) == 0) {
+      result.Includes.emplace_back(AppendAndTrim(result.Flagline, flag));
+    } else {
+      result.CompileOptions.emplace_back(AppendAndTrim(result.Flagline, flag));
+    }
+  }
+
+  return result;
+}
+
+cmPkgConfigCflagsResult cmPkgConfigResolver::MangleCflags(
+  const std::vector<cm::string_view>& flags, const std::string& sysroot)
+{
+  cmPkgConfigCflagsResult result;
+
+  for (auto flag : flags) {
+    if (flag.rfind("-I", 0) == 0) {
+      std::string reroot = Reroot(flag, "-I", sysroot);
+      result.Includes.emplace_back(AppendAndTrim(result.Flagline, reroot));
+    } else {
+      result.CompileOptions.emplace_back(AppendAndTrim(result.Flagline, flag));
+    }
+  }
+
+  return result;
+}
+
+cmPkgConfigCflagsResult cmPkgConfigResolver::MangleCflags(
+  const std::vector<cm::string_view>& flags,
+  const std::vector<std::string>& syspaths)
+{
+  cmPkgConfigCflagsResult result;
+
+  for (auto flag : flags) {
+    if (flag.rfind("-I", 0) == 0) {
+      cm::string_view noprefix{ flag.data() + 2, flag.size() - 2 };
+
+      if (std::all_of(syspaths.begin(), syspaths.end(),
+                      [&](const std::string& path) {
+                        return noprefix.rfind(path, 0) == noprefix.npos;
+                      })) {
+        result.Includes.emplace_back(AppendAndTrim(result.Flagline, flag));
+      }
+
+    } else {
+      result.CompileOptions.emplace_back(AppendAndTrim(result.Flagline, flag));
+    }
+  }
+
+  return result;
+}
+
+cmPkgConfigCflagsResult cmPkgConfigResolver::MangleCflags(
+  const std::vector<cm::string_view>& flags, const std::string& sysroot,
+  const std::vector<std::string>& syspaths)
+{
+  cmPkgConfigCflagsResult result;
+
+  for (auto flag : flags) {
+    if (flag.rfind("-I", 0) == 0) {
+      std::string reroot = Reroot(flag, "-I", sysroot);
+      cm::string_view noprefix{ reroot.data() + 2, reroot.size() - 2 };
+
+      if (std::all_of(syspaths.begin(), syspaths.end(),
+                      [&](const std::string& path) {
+                        return noprefix.rfind(path, 0) == noprefix.npos;
+                      })) {
+        result.Includes.emplace_back(AppendAndTrim(result.Flagline, reroot));
+      }
+
+    } else {
+      result.CompileOptions.emplace_back(AppendAndTrim(result.Flagline, flag));
+    }
+  }
+
+  return result;
+}
+
+cmPkgConfigLibsResult cmPkgConfigResolver::MangleLibs(
+  const std::vector<cm::string_view>& flags)
+{
+  cmPkgConfigLibsResult result;
+
+  for (auto flag : flags) {
+    if (flag.rfind("-L", 0) == 0) {
+      result.LibDirs.emplace_back(AppendAndTrim(result.Flagline, flag));
+    } else if (flag.rfind("-l", 0) == 0) {
+      result.LibNames.emplace_back(AppendAndTrim(result.Flagline, flag));
+    } else {
+      result.LinkOptions.emplace_back(AppendAndTrim(result.Flagline, flag));
+    }
+  }
+
+  return result;
+}
+
+cmPkgConfigLibsResult cmPkgConfigResolver::MangleLibs(
+  const std::vector<cm::string_view>& flags, const std::string& sysroot)
+{
+  cmPkgConfigLibsResult result;
+
+  for (auto flag : flags) {
+    if (flag.rfind("-L", 0) == 0) {
+      std::string reroot = Reroot(flag, "-L", sysroot);
+      result.LibDirs.emplace_back(AppendAndTrim(result.Flagline, reroot));
+    } else if (flag.rfind("-l", 0) == 0) {
+      result.LibNames.emplace_back(AppendAndTrim(result.Flagline, flag));
+    } else {
+      result.LinkOptions.emplace_back(AppendAndTrim(result.Flagline, flag));
+    }
+  }
+
+  return result;
+}
+
+cmPkgConfigLibsResult cmPkgConfigResolver::MangleLibs(
+  const std::vector<cm::string_view>& flags,
+  const std::vector<std::string>& syspaths)
+{
+  cmPkgConfigLibsResult result;
+
+  for (auto flag : flags) {
+    if (flag.rfind("-L", 0) == 0) {
+      cm::string_view noprefix{ flag.data() + 2, flag.size() - 2 };
+
+      if (std::all_of(syspaths.begin(), syspaths.end(),
+                      [&](const std::string& path) {
+                        return noprefix.rfind(path, 0) == noprefix.npos;
+                      })) {
+        result.LibDirs.emplace_back(AppendAndTrim(result.Flagline, flag));
+      }
+
+    } else if (flag.rfind("-l", 0) == 0) {
+      result.LibNames.emplace_back(AppendAndTrim(result.Flagline, flag));
+    } else {
+      result.LinkOptions.emplace_back(AppendAndTrim(result.Flagline, flag));
+    }
+  }
+
+  return result;
+}
+
+cmPkgConfigLibsResult cmPkgConfigResolver::MangleLibs(
+  const std::vector<cm::string_view>& flags, const std::string& sysroot,
+  const std::vector<std::string>& syspaths)
+{
+  cmPkgConfigLibsResult result;
+
+  for (auto flag : flags) {
+    if (flag.rfind("-L", 0) == 0) {
+      std::string reroot = Reroot(flag, "-L", sysroot);
+      cm::string_view noprefix{ reroot.data() + 2, reroot.size() - 2 };
+
+      if (std::all_of(syspaths.begin(), syspaths.end(),
+                      [&](const std::string& path) {
+                        return noprefix.rfind(path, 0) == noprefix.npos;
+                      })) {
+        result.LibDirs.emplace_back(AppendAndTrim(result.Flagline, reroot));
+      }
+
+    } else if (flag.rfind("-l", 0) == 0) {
+      result.LibNames.emplace_back(AppendAndTrim(result.Flagline, flag));
+    } else {
+      result.LinkOptions.emplace_back(AppendAndTrim(result.Flagline, flag));
+    }
+  }
+
+  return result;
+}
+
+std::string cmPkgConfigResolver::Reroot(cm::string_view flag,
+                                        cm::string_view prefix,
+                                        const std::string& sysroot)
+{
+  std::string result = std::string{ prefix };
+  result += sysroot;
+  result += cm::string_view{ flag.data() + prefix.length(),
+                             flag.size() - prefix.length() };
+  return result;
+}
+
+cmPkgConfigVersionReq cmPkgConfigResolver::ParseVersion(
+  std::string::const_iterator& cur, std::string::const_iterator end)
+{
+  cmPkgConfigVersionReq result;
+  if (*cur == '=') {
+    result.Operation = result.EQ;
+    ++cur;
+  } else if (*cur == '>') {
+    ++cur;
+
+    if (cur == end) {
+      result.Operation = result.GT;
+      return result;
+    }
+
+    if (*cur == '=') {
+      result.Operation = result.GT_EQ;
+      ++cur;
+    } else {
+      result.Operation = result.GT;
+    }
+
+  } else if (*cur == '<') {
+    ++cur;
+
+    if (cur == end) {
+      result.Operation = result.LT;
+      return result;
+    }
+
+    if (*cur == '=') {
+      result.Operation = result.LT_EQ;
+      ++cur;
+    } else {
+      result.Operation = result.LT;
+    }
+
+  } else if (*cur == '!') {
+    ++cur;
+
+    if (cur == end) {
+      result.Operation = result.ANY;
+      return result;
+    }
+
+    if (*cur == '=') {
+      result.Operation = result.NEQ;
+      ++cur;
+    } else {
+      result.Operation = result.ANY;
+    }
+  }
+
+  for (;; ++cur) {
+    if (cur == end) {
+      return result;
+    }
+
+    if (!std::isspace(*cur)) {
+      break;
+    }
+  }
+
+  for (; cur != end && !std::isspace(*cur) && *cur != ','; ++cur) {
+    result.Version += *cur;
+  }
+
+  return result;
+}
+
+std::vector<cmPkgConfigDependency> cmPkgConfigResolver::ParseDependencies(
+  const std::string& deps)
+{
+
+  std::vector<cmPkgConfigDependency> result;
+
+  auto cur = deps.begin();
+  auto end = deps.end();
+
+  while (cur != end) {
+    while ((std::isspace(*cur) || *cur == ',')) {
+      if (++cur == end) {
+        return result;
+      }
+    }
+
+    result.emplace_back();
+    auto& dep = result.back();
+
+    while (!std::isspace(*cur) && *cur != ',') {
+      dep.Name += *cur;
+      if (++cur == end) {
+        return result;
+      }
+    }
+
+    auto in_operator = [&]() -> bool {
+      for (;; ++cur) {
+        if (cur == end) {
+          return false;
+        }
+
+        if (*cur == '>' || *cur == '=' || *cur == '<' || *cur == '!') {
+          return true;
+        }
+
+        if (!std::isspace(*cur)) {
+          return false;
+        }
+      }
+    };
+
+    if (!in_operator()) {
+      continue;
+    }
+
+    dep.VerReq = ParseVersion(cur, end);
+  }
+
+  return result;
+}
+
+bool cmPkgConfigResolver::CheckVersion(const cmPkgConfigVersionReq& desired,
+                                       const std::string& provided)
+{
+
+  if (desired.Operation == cmPkgConfigVersionReq::ANY) {
+    return true;
+  }
+
+  // https://blog.jasonantman.com/2014/07/how-yum-and-rpm-compare-versions/
+
+  auto check_with_op = [&](int comp) -> bool {
+    switch (desired.Operation) {
+      case cmPkgConfigVersionReq::EQ:
+        return comp == 0;
+      case cmPkgConfigVersionReq::NEQ:
+        return comp != 0;
+      case cmPkgConfigVersionReq::GT:
+        return comp < 0;
+      case cmPkgConfigVersionReq::GT_EQ:
+        return comp <= 0;
+      case cmPkgConfigVersionReq::LT:
+        return comp > 0;
+      case cmPkgConfigVersionReq::LT_EQ:
+        return comp >= 0;
+      default:
+        return true;
+    }
+  };
+
+  if (desired.Version == provided) {
+    return check_with_op(0);
+  }
+
+  auto a_cur = desired.Version.begin();
+  auto a_end = desired.Version.end();
+
+  auto b_cur = provided.begin();
+  auto b_end = provided.end();
+
+  while (a_cur != a_end && b_cur != b_end) {
+    while (a_cur != a_end && !std::isalnum(*a_cur) && *a_cur != '~') {
+      ++a_cur;
+    }
+
+    while (b_cur != b_end && !std::isalnum(*b_cur) && *b_cur != '~') {
+      ++b_cur;
+    }
+
+    if (a_cur == a_end || b_cur == b_end) {
+      break;
+    }
+
+    if (*a_cur == '~' || *b_cur == '~') {
+      if (*a_cur != '~') {
+        return check_with_op(1);
+      }
+
+      if (*b_cur != '~') {
+        return check_with_op(-1);
+      }
+
+      ++a_cur;
+      ++b_cur;
+      continue;
+    }
+
+    auto a_seg = a_cur;
+    auto b_seg = b_cur;
+    bool is_num;
+
+    if (std::isdigit(*a_cur)) {
+      is_num = true;
+      while (a_cur != a_end && std::isdigit(*a_cur)) {
+        ++a_cur;
+      }
+
+      while (b_cur != b_end && std::isdigit(*b_cur)) {
+        ++b_cur;
+      }
+
+    } else {
+      is_num = false;
+      while (a_cur != a_end && std::isalpha(*a_cur)) {
+        ++a_cur;
+      }
+
+      while (b_cur != b_end && std::isalpha(*b_cur)) {
+        ++b_cur;
+      }
+    }
+
+    auto a_len = std::distance(a_seg, a_cur);
+    auto b_len = std::distance(b_seg, b_cur);
+
+    if (!b_len) {
+      return check_with_op(is_num ? 1 : -1);
+    }
+
+    if (is_num) {
+      while (a_seg != a_cur && *a_seg == '0') {
+        ++a_seg;
+      }
+
+      while (b_seg != b_cur && *b_seg == '0') {
+        ++b_seg;
+      }
+
+      a_len = std::distance(a_seg, a_cur);
+      b_len = std::distance(b_seg, b_cur);
+
+      if (a_len != b_len) {
+        return check_with_op(a_len > b_len ? 1 : -1);
+      }
+
+      auto cmp = std::memcmp(&*a_seg, &*b_seg, a_len);
+      if (cmp) {
+        return check_with_op(cmp);
+      }
+    } else {
+      auto cmp = std::memcmp(&*a_seg, &*b_seg, std::min(a_len, b_len));
+      if (cmp) {
+        return check_with_op(cmp);
+      }
+
+      if (a_len != b_len) {
+        return check_with_op(a_len > b_len ? 1 : -1);
+      }
+    }
+  }
+
+  if (a_cur == a_end) {
+    if (b_cur == b_end) {
+      return check_with_op(0);
+    }
+    return check_with_op(-1);
+  }
+
+  return check_with_op(1);
+}
+
+cmPkgConfigVersionReq cmPkgConfigResolver::ParseVersion(
+  const std::string& version)
+{
+  cmPkgConfigVersionReq result;
+
+  auto cur = version.begin();
+  auto end = version.end();
+
+  if (cur == end) {
+    result.Operation = cmPkgConfigVersionReq::EQ;
+    return result;
+  }
+
+  result = ParseVersion(cur, end);
+  cur = version.begin();
+
+  if (*cur != '=' && *cur != '!' && *cur != '<' && *cur != '>') {
+    result.Operation = cmPkgConfigVersionReq::EQ;
+  }
+
+  return result;
+}

+ 172 - 0
Source/cmPkgConfigResolver.h

@@ -0,0 +1,172 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include <cm/optional>
+#include <cm/string_view>
+
+// From cmPkgConfigParser.h, IWYU doesn't like including the header
+struct cmPkgConfigEntry;
+
+struct cmPkgConfigCflagsResult
+{
+  std::string Flagline;
+  std::vector<std::string> Includes;
+  std::vector<std::string> CompileOptions;
+};
+
+struct cmPkgConfigLibsResult
+{
+  std::string Flagline;
+  std::vector<std::string> LibDirs;
+  std::vector<std::string> LibNames;
+  std::vector<std::string> LinkOptions;
+};
+
+struct cmPkgConfigVersionReq
+{
+  enum
+  {
+    ANY = 0,
+    LT,
+    LT_EQ,
+    EQ,
+    NEQ,
+    GT_EQ,
+    GT,
+  } Operation = ANY;
+  std::string Version;
+};
+
+struct cmPkgConfigDependency
+{
+  std::string Name;
+  cmPkgConfigVersionReq VerReq;
+};
+
+struct cmPkgConfigEnv
+{
+  cm::optional<std::vector<std::string>> Path;
+  cm::optional<std::vector<std::string>> LibDirs;
+  cm::optional<std::vector<std::string>> SysCflags;
+  cm::optional<std::vector<std::string>> SysLibs;
+
+  cm::optional<std::string> SysrootDir;
+  cm::optional<std::string> TopBuildDir;
+
+  cm::optional<bool> DisableUninstalled;
+
+  bool AllowSysCflags = true;
+  bool AllowSysLibs = true;
+};
+
+class cmPkgConfigResult
+{
+public:
+  std::unordered_map<std::string, std::string> Keywords;
+  std::unordered_map<std::string, std::string> Variables;
+
+  std::string Name();
+  std::string Description();
+  std::string Version();
+
+  std::vector<cmPkgConfigDependency> Conflicts();
+  std::vector<cmPkgConfigDependency> Provides();
+  std::vector<cmPkgConfigDependency> Requires(bool priv = false);
+
+  cmPkgConfigCflagsResult Cflags(bool priv = false);
+  cmPkgConfigLibsResult Libs(bool priv = false);
+
+  cmPkgConfigEnv env;
+
+private:
+  std::string StrOrDefault(const std::string& key, cm::string_view def = "");
+};
+
+class cmPkgConfigResolver
+{
+  friend class cmPkgConfigResult;
+
+public:
+  static cm::optional<cmPkgConfigResult> ResolveStrict(
+    const std::vector<cmPkgConfigEntry>& entries, cmPkgConfigEnv env);
+
+  static cm::optional<cmPkgConfigResult> ResolvePermissive(
+    const std::vector<cmPkgConfigEntry>& entries, cmPkgConfigEnv env);
+
+  static cmPkgConfigResult ResolveBestEffort(
+    const std::vector<cmPkgConfigEntry>& entries, cmPkgConfigEnv env);
+
+  static cmPkgConfigVersionReq ParseVersion(const std::string& version);
+
+  static bool CheckVersion(const cmPkgConfigVersionReq& desired,
+                           const std::string& provided);
+
+  static void ReplaceSep(std::string& list);
+
+#ifdef _WIN32
+  static const char Sep = ';';
+#else
+  static const char Sep = ':';
+#endif
+
+private:
+  static std::string HandleVariablePermissive(
+    const cmPkgConfigEntry& entry,
+    const std::unordered_map<std::string, std::string>& variables);
+
+  static cm::optional<std::string> HandleVariableStrict(
+    const cmPkgConfigEntry& entry,
+    const std::unordered_map<std::string, std::string>& variables);
+
+  static std::string HandleKeyword(
+    const cmPkgConfigEntry& entry,
+    const std::unordered_map<std::string, std::string>& variables);
+
+  static std::vector<cm::string_view> TokenizeFlags(
+    const std::string& flagline);
+
+  static cmPkgConfigCflagsResult MangleCflags(
+    const std::vector<cm::string_view>& flags);
+
+  static cmPkgConfigCflagsResult MangleCflags(
+    const std::vector<cm::string_view>& flags, const std::string& sysroot);
+
+  static cmPkgConfigCflagsResult MangleCflags(
+    const std::vector<cm::string_view>& flags,
+    const std::vector<std::string>& syspaths);
+
+  static cmPkgConfigCflagsResult MangleCflags(
+    const std::vector<cm::string_view>& flags, const std::string& sysroot,
+    const std::vector<std::string>& syspaths);
+
+  static cmPkgConfigLibsResult MangleLibs(
+    const std::vector<cm::string_view>& flags);
+
+  static cmPkgConfigLibsResult MangleLibs(
+    const std::vector<cm::string_view>& flags, const std::string& sysroot);
+
+  static cmPkgConfigLibsResult MangleLibs(
+    const std::vector<cm::string_view>& flags,
+    const std::vector<std::string>& syspaths);
+
+  static cmPkgConfigLibsResult MangleLibs(
+    const std::vector<cm::string_view>& flags, const std::string& sysroot,
+    const std::vector<std::string>& syspaths);
+
+  static std::string Reroot(cm::string_view flag, cm::string_view prefix,
+                            const std::string& sysroot);
+
+  static cmPkgConfigVersionReq ParseVersion(std::string::const_iterator& cur,
+                                            std::string::const_iterator end);
+
+  static std::vector<cmPkgConfigDependency> ParseDependencies(
+    const std::string& deps);
+};

+ 1 - 0
Tests/RunCMake/CMakeLists.txt

@@ -560,6 +560,7 @@ add_RunCMake_test(cmake_language)
 add_RunCMake_test(cmake_minimum_required)
 add_RunCMake_test(cmake_parse_arguments)
 add_RunCMake_test(cmake_path -DMSYS=${MSYS})
+add_RunCMake_test(cmake_pkg_config)
 add_RunCMake_test(continue)
 add_executable(color_warning color_warning.c)
 add_executable(fake_build_command fake_build_command.c)

+ 5 - 0
Tests/RunCMake/cmake_pkg_config/CMakeLists.txt

@@ -0,0 +1,5 @@
+cmake_minimum_required(VERSION 3.30)
+project(${RunCMake_TEST} NONE)
+
+set(CMAKE_PKG_CONFIG_PC_LIB_DIRS ${CMAKE_CURRENT_LIST_DIR}/PackageRoot)
+include(${RunCMake_TEST}.cmake)

+ 3 - 0
Tests/RunCMake/cmake_pkg_config/PackageRoot/VersionPackages/a.pc

@@ -0,0 +1,3 @@
+Name:
+Version: aa
+Description:

+ 3 - 0
Tests/RunCMake/cmake_pkg_config/PackageRoot/VersionPackages/one.pc

@@ -0,0 +1,3 @@
+Name:
+Version: 11
+Description:

+ 3 - 0
Tests/RunCMake/cmake_pkg_config/PackageRoot/VersionPackages/onedot.pc

@@ -0,0 +1,3 @@
+Name:
+Version: 1.1.1
+Description:

+ 3 - 0
Tests/RunCMake/cmake_pkg_config/PackageRoot/VersionPackages/pseudo-empty.pc

@@ -0,0 +1,3 @@
+Name:
+Version: ~0
+Description:

+ 3 - 0
Tests/RunCMake/cmake_pkg_config/PackageRoot/VersionPackages/tilde.pc

@@ -0,0 +1,3 @@
+Name:
+Version: ~~1
+Description:

+ 3 - 0
Tests/RunCMake/cmake_pkg_config/PackageRoot/VersionPackages/zeroone.pc

@@ -0,0 +1,3 @@
+Name:
+Version: 01
+Description:

+ 15 - 0
Tests/RunCMake/cmake_pkg_config/PackageRoot/all-extract-fields.pc

@@ -0,0 +1,15 @@
+Name: Extract All
+Description: All flags example
+Version: 1.0.0
+
+Conflicts: Alpha Beta
+Provides: Gamma Delta
+
+Requires: Epsilon Zea
+Requires.private: Eta Theta
+
+Cflags: Iota -IKappa Lambda -IMu
+Cflags.private: Nu -IXi Omnicron -IPi
+
+Libs: Rho -LSigma -lTau Upsilon -LPhi -lChi
+Libs.private: Psi -LOmega -lMoe Larry -LCurly -lShemp

+ 3 - 0
Tests/RunCMake/cmake_pkg_config/PackageRoot/bar.pc

@@ -0,0 +1,3 @@
+Name: Bar
+Description: Bar Description
+Version: 1.0.0

+ 3 - 0
Tests/RunCMake/cmake_pkg_config/PackageRoot/baz.pc

@@ -0,0 +1,3 @@
+Name: Baz
+Description: Baz Description
+Version: 1.0.0

+ 6 - 0
Tests/RunCMake/cmake_pkg_config/PackageRoot/cflags-bothcase-f.pc

@@ -0,0 +1,6 @@
+Name: Cflags Bothcase
+Description: The f is lowercase and uppercase
+Version: 1.0.0
+
+Cflags: lowercase
+CFlags: uppercase

+ 5 - 0
Tests/RunCMake/cmake_pkg_config/PackageRoot/cflags-lowercase-f.pc

@@ -0,0 +1,5 @@
+Name: Cflags Lowercase
+Description: The f is lowercase
+Version: 1.0.0
+
+Cflags: lowercase

+ 5 - 0
Tests/RunCMake/cmake_pkg_config/PackageRoot/cflags-uppercase-f.pc

@@ -0,0 +1,5 @@
+Name: CFlags Uppercase
+Description: The f is uppercase
+Version: 1.0.0
+
+CFlags: uppercase

+ 3 - 0
Tests/RunCMake/cmake_pkg_config/PackageRoot/empty-key.pc

@@ -0,0 +1,3 @@
+Name:
+Description:
+Version:

+ 3 - 0
Tests/RunCMake/cmake_pkg_config/PackageRoot/foo.pc

@@ -0,0 +1,3 @@
+Name: Foo
+Description: Foo Description
+Version: 1.0.0

+ 4 - 0
Tests/RunCMake/cmake_pkg_config/PackageRoot/invalid.pc

@@ -0,0 +1,4 @@
+Name: Invalid
+Description: Will cause a parse error
+Version: 1.0.0
+BrokenKey

+ 2 - 0
Tests/RunCMake/cmake_pkg_config/PackageRoot/no-description.pc

@@ -0,0 +1,2 @@
+Name: name
+Version: version

+ 2 - 0
Tests/RunCMake/cmake_pkg_config/PackageRoot/no-name.pc

@@ -0,0 +1,2 @@
+Description: description
+Version: version

+ 2 - 0
Tests/RunCMake/cmake_pkg_config/PackageRoot/no-version.pc

@@ -0,0 +1,2 @@
+Name: name
+Description: description

+ 5 - 0
Tests/RunCMake/cmake_pkg_config/PackageRoot/qux-uninstalled.pc

@@ -0,0 +1,5 @@
+Name: Qux
+Description: Qux Description
+Version: 1.0.0
+
+Cflags: QuxUninstalled

+ 5 - 0
Tests/RunCMake/cmake_pkg_config/PackageRoot/qux.pc

@@ -0,0 +1,5 @@
+Name: Qux
+Description: Qux Description
+Version: 1.0.0
+
+Cflags: QuxInstalled

+ 6 - 0
Tests/RunCMake/cmake_pkg_config/PackageRoot/relocate.pc

@@ -0,0 +1,6 @@
+Name: Relocate
+Description: For testing relocation and flag mangling
+Version: 1.0.0
+
+Cflags: -I/Alpha Beta -I/Gamma
+Libs: -L/Delta Epsilon -L/Zeta

+ 18 - 0
Tests/RunCMake/cmake_pkg_config/RunCMakeTest.cmake

@@ -0,0 +1,18 @@
+include(RunCMake)
+
+set(cmd ${CMAKE_COMMAND} ${CMAKE_CURRENT_LIST_DIR} -G ${RunCMake_GENERATOR})
+
+foreach(strictness IN ITEMS STRICT PERMISSIVE BEST_EFFORT)
+  run_cmake_command(TestStrictness-${strictness} ${cmd}
+    -DRunCMake_TEST=TestStrictness -DSTRICTNESS=${strictness}
+  )
+endforeach()
+
+run_cmake(TestEnv)
+run_cmake(TestExtract)
+run_cmake(TestMangle)
+run_cmake(TestQuiet)
+run_cmake(TestRequired)
+run_cmake(TestReroot)
+run_cmake(TestUninstalled)
+run_cmake(TestVersion)

+ 15 - 0
Tests/RunCMake/cmake_pkg_config/TestEnv-stderr.txt

@@ -0,0 +1,15 @@
+Includes: -I/Alpha;-I/Gamma
+LibDirs: -L/Delta;-L/Zeta
+Cflags: QuxInstalled
+PC_LIB_DIRS: Alpha;Beta
+PC_PATH: [^
+]*/PackageRoot
+DISABLE_UNINSTALLED: ON
+SYSROOT_DIR: Delta
+TOP_BUILD_DIR: Epsilon
+SYSTEM_INCLUDE_DIRS: Zeta;Eta
+SYSTEM_LIB_DIRS: Theta;Iota
+ALLOW_SYSTEM_INCLUDES: ON
+ALLOW_SYSTEM_LIBRARIES: ON
+PKGCONF_INCLUDES: Mu;Nu;Xi;Omnicron;Pi;Rho;Sigma;Tau
+PKGCONF_LIB_DIRS: Upsilon;Phi

+ 75 - 0
Tests/RunCMake/cmake_pkg_config/TestEnv.cmake

@@ -0,0 +1,75 @@
+set(CMAKE_PKG_CONFIG_PC_LIB_DIRS)
+
+set(ENV{PKG_CONFIG_PATH} ${CMAKE_CURRENT_LIST_DIR}/PackageRoot)
+
+if(WIN32)
+  set(sep ";")
+else()
+  set(sep ":")
+endif()
+
+set(ENV{PKG_CONFIG_LIBDIR} "Alpha${sep}Beta")
+set(ENV{PKG_CONFIG_DISABLE_UNINSTALLED} Gamma)
+set(ENV{PKG_CONFIG_SYSROOT_DIR} Delta)
+set(ENV{PKG_CONFIG_TOP_BUILD_DIR} Epsilon)
+set(ENV{PKG_CONFIG_SYSTEM_INCLUDE_PATH} "Zeta${sep}Eta")
+set(ENV{PKG_CONFIG_SYSTEM_LIBRARY_PATH} "Theta${sep}Iota")
+set(ENV{PKG_CONFIG_ALLOW_SYSTEM_CFLAGS} Kappa)
+set(ENV{PKG_CONFIG_ALLOW_SYSTEM_LIBS} Lambda)
+
+set(ENV{CPATH} "Mu${sep}Nu")
+set(ENV{C_INCLUDE_PATH} "Xi${sep}Omnicron")
+set(ENV{CPLUS_INCLUDE_PATH} "Pi${sep}Rho")
+
+if(WIN32)
+  set(ENV{OBJC_INCLUDE_PATH} Sigma)
+  set(ENV{INCLUDE} Tau)
+else()
+  set(ENV{OBJC_INCLUDE_PATH} Sigma:Tau)
+endif()
+
+set(ENV{LIBRARY_PATH} "Upsilon${sep}Phi")
+
+cmake_pkg_config(
+  EXTRACT relocate
+  ENV_MODE IGNORE
+  PC_PATH ${CMAKE_CURRENT_LIST_DIR}/PackageRoot
+  SYSTEM_INCLUDE_DIRS /Alpha
+  SYSTEM_LIBRARY_DIRS /Beta
+)
+
+# Shouldn't mangle, ALLOW_SYSTEM_* should default to on under ENV IGNORE
+message("Includes: ${CMAKE_PKG_CONFIG_INCLUDES}")
+message("LibDirs: ${CMAKE_PKG_CONFIG_LIBDIRS}")
+
+cmake_pkg_config(
+  EXTRACT qux
+  ENV_MODE IGNORE
+  PC_PATH ${CMAKE_CURRENT_LIST_DIR}/PackageRoot
+)
+
+# Shouldn't find uninstalled package
+message("Cflags: ${CMAKE_PKG_CONFIG_CFLAGS}")
+
+cmake_pkg_config(
+  EXTRACT foo
+  ENV_MODE FDO
+)
+
+message("PC_LIB_DIRS: ${CMAKE_PKG_CONFIG_PC_LIB_DIRS}")
+message("PC_PATH: ${CMAKE_PKG_CONFIG_PC_PATH}")
+message("DISABLE_UNINSTALLED: ${CMAKE_PKG_CONFIG_DISABLE_UNINSTALLED}")
+message("SYSROOT_DIR: ${CMAKE_PKG_CONFIG_SYSROOT_DIR}")
+message("TOP_BUILD_DIR: ${CMAKE_PKG_CONFIG_TOP_BUILD_DIR}")
+message("SYSTEM_INCLUDE_DIRS: ${CMAKE_PKG_CONFIG_SYS_INCLUDE_DIRS}")
+message("SYSTEM_LIB_DIRS: ${CMAKE_PKG_CONFIG_SYS_LIB_DIRS}")
+message("ALLOW_SYSTEM_INCLUDES: ${CMAKE_PKG_CONFIG_ALLOW_SYS_INCLUDES}")
+message("ALLOW_SYSTEM_LIBRARIES: ${CMAKE_PKG_CONFIG_ALLOW_SYS_LIBS}")
+
+cmake_pkg_config(
+  EXTRACT foo
+  ENV_MODE PKGCONF
+)
+
+message("PKGCONF_INCLUDES: ${CMAKE_PKG_CONFIG_PKGCONF_INCLUDES}")
+message("PKGCONF_LIB_DIRS: ${CMAKE_PKG_CONFIG_PKGCONF_LIB_DIRS}")

+ 21 - 0
Tests/RunCMake/cmake_pkg_config/TestExtract-stderr.txt

@@ -0,0 +1,21 @@
+Name: Extract All
+Description: All flags example
+Version: 1.0.0
+Conflicts: Alpha;Beta
+Provides: Gamma;Delta
+Requires: Epsilon;Zea
+Requires.private: Eta;Theta
+Cflags: Iota -IKappa Lambda -IMu
+Includes: -IKappa;-IMu
+CompileOptions: Iota;Lambda
+Cflags.private: Nu -IXi Omnicron -IPi
+Includes.private: -IXi;-IPi
+CompileOptions.private: Nu;Omnicron
+Libs: Rho -LSigma -lTau Upsilon -LPhi -lChi
+LibDirs: -LSigma;-LPhi
+LibNames: -lTau;-lChi
+LinkOptions: Rho;Upsilon
+Libs.private: Psi -LOmega -lMoe Larry -LCurly -lShemp
+LibDirs.private: -LOmega;-LCurly
+LibNames.private: -lMoe;-lShemp
+LinkOptions.private: Psi;Larry

+ 29 - 0
Tests/RunCMake/cmake_pkg_config/TestExtract.cmake

@@ -0,0 +1,29 @@
+cmake_pkg_config(EXTRACT all-extract-fields)
+
+message("Name: ${CMAKE_PKG_CONFIG_NAME}")
+message("Description: ${CMAKE_PKG_CONFIG_DESCRIPTION}")
+message("Version: ${CMAKE_PKG_CONFIG_VERSION}")
+
+message("Conflicts: ${CMAKE_PKG_CONFIG_CONFLICTS}")
+message("Provides: ${CMAKE_PKG_CONFIG_PROVIDES}")
+
+message("Requires: ${CMAKE_PKG_CONFIG_REQUIRES}")
+message("Requires.private: ${CMAKE_PKG_CONFIG_REQUIRES_PRIVATE}")
+
+message("Cflags: ${CMAKE_PKG_CONFIG_CFLAGS}")
+message("Includes: ${CMAKE_PKG_CONFIG_INCLUDES}")
+message("CompileOptions: ${CMAKE_PKG_CONFIG_COMPILE_OPTIONS}")
+
+message("Cflags.private: ${CMAKE_PKG_CONFIG_CFLAGS_PRIVATE}")
+message("Includes.private: ${CMAKE_PKG_CONFIG_INCLUDES_PRIVATE}")
+message("CompileOptions.private: ${CMAKE_PKG_CONFIG_COMPILE_OPTIONS_PRIVATE}")
+
+message("Libs: ${CMAKE_PKG_CONFIG_LIBS}")
+message("LibDirs: ${CMAKE_PKG_CONFIG_LIBDIRS}")
+message("LibNames: ${CMAKE_PKG_CONFIG_LIBNAMES}")
+message("LinkOptions: ${CMAKE_PKG_CONFIG_LINK_OPTIONS}")
+
+message("Libs.private: ${CMAKE_PKG_CONFIG_LIBS_PRIVATE}")
+message("LibDirs.private: ${CMAKE_PKG_CONFIG_LIBDIRS_PRIVATE}")
+message("LibNames.private: ${CMAKE_PKG_CONFIG_LIBNAMES_PRIVATE}")
+message("LinkOptions.private: ${CMAKE_PKG_CONFIG_LINK_OPTIONS_PRIVATE}")

+ 8 - 0
Tests/RunCMake/cmake_pkg_config/TestMangle-stderr.txt

@@ -0,0 +1,8 @@
+Cflags: Beta -I/Gamma
+Includes: -I/Gamma
+Libs: Epsilon -L/Zeta
+LibDirs: -L/Zeta
+Cflags: -I/Alpha Beta -I/Gamma
+Includes: -I/Alpha;-I/Gamma
+Libs: -L/Delta Epsilon -L/Zeta
+LibDirs: -L/Delta;-L/Zeta

+ 22 - 0
Tests/RunCMake/cmake_pkg_config/TestMangle.cmake

@@ -0,0 +1,22 @@
+set(CMAKE_PKG_CONFIG_SYS_INCLUDE_DIRS /Alpha)
+set(CMAKE_PKG_CONFIG_SYS_LIB_DIRS /Delta)
+
+cmake_pkg_config(EXTRACT relocate)
+
+message("Cflags: ${CMAKE_PKG_CONFIG_CFLAGS}")
+message("Includes: ${CMAKE_PKG_CONFIG_INCLUDES}")
+
+message("Libs: ${CMAKE_PKG_CONFIG_LIBS}")
+message("LibDirs: ${CMAKE_PKG_CONFIG_LIBDIRS}")
+
+cmake_pkg_config(
+  EXTRACT relocate
+  ALLOW_SYSTEM_INCLUDES ON
+  ALLOW_SYSTEM_LIBS ON
+)
+
+message("Cflags: ${CMAKE_PKG_CONFIG_CFLAGS}")
+message("Includes: ${CMAKE_PKG_CONFIG_INCLUDES}")
+
+message("Libs: ${CMAKE_PKG_CONFIG_LIBS}")
+message("LibDirs: ${CMAKE_PKG_CONFIG_LIBDIRS}")

+ 29 - 0
Tests/RunCMake/cmake_pkg_config/TestQuiet.cmake

@@ -0,0 +1,29 @@
+cmake_pkg_config(
+  EXTRACT foo
+  QUIET
+  STRICTNESS STRICT
+)
+
+cmake_pkg_config(
+  EXTRACT no-name
+  QUIET
+  STRICTNESS STRICT
+)
+
+cmake_pkg_config(
+  EXTRACT empty-key
+  QUIET
+  STRICTNESS STRICT
+)
+
+cmake_pkg_config(
+  EXTRACT cflags-bothcase-f
+  QUIET
+  STRICTNESS STRICT
+)
+
+cmake_pkg_config(
+  EXTRACT does-not-exist
+  QUIET
+  STRICTNESS STRICT
+)

+ 1 - 0
Tests/RunCMake/cmake_pkg_config/TestRequired-result.txt

@@ -0,0 +1 @@
+1

+ 4 - 0
Tests/RunCMake/cmake_pkg_config/TestRequired-stderr.txt

@@ -0,0 +1,4 @@
+CMake Error at TestRequired.cmake:[0-9]+ \(cmake_pkg_config\):
+  cmake_pkg_config Could not find 'does-not-exist'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)

+ 9 - 0
Tests/RunCMake/cmake_pkg_config/TestRequired.cmake

@@ -0,0 +1,9 @@
+cmake_pkg_config(
+  EXTRACT foo
+  REQUIRED
+)
+
+cmake_pkg_config(
+  EXTRACT does-not-exist
+  REQUIRED
+)

+ 4 - 0
Tests/RunCMake/cmake_pkg_config/TestReroot-stderr.txt

@@ -0,0 +1,4 @@
+Cflags: -I/NewRoot/Alpha Beta -I/NewRoot/Gamma
+Includes: -I/NewRoot/Alpha;-I/NewRoot/Gamma
+Libs: -L/NewRoot/Delta Epsilon -L/NewRoot/Zeta
+LibDirs: -L/NewRoot/Delta;-L/NewRoot/Zeta

+ 10 - 0
Tests/RunCMake/cmake_pkg_config/TestReroot.cmake

@@ -0,0 +1,10 @@
+cmake_pkg_config(
+  EXTRACT relocate
+  PC_SYSROOT_DIR /NewRoot
+)
+
+message("Cflags: ${CMAKE_PKG_CONFIG_CFLAGS}")
+message("Includes: ${CMAKE_PKG_CONFIG_INCLUDES}")
+
+message("Libs: ${CMAKE_PKG_CONFIG_LIBS}")
+message("LibDirs: ${CMAKE_PKG_CONFIG_LIBDIRS}")

+ 3 - 0
Tests/RunCMake/cmake_pkg_config/TestStrictness-BEST_EFFORT-stderr.txt

@@ -0,0 +1,3 @@
+Cflags: lowercase
+CFlags: uppercase
+Cflags: lowercase uppercase

+ 31 - 0
Tests/RunCMake/cmake_pkg_config/TestStrictness-PERMISSIVE-stderr.txt

@@ -0,0 +1,31 @@
+CMake Warning at TestStrictness.cmake:[0-9]+ \(cmake_pkg_config\):
+  Resolution failed for file[^
+]*(.)*/PackageRoot/no-name.pc'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestStrictness.cmake:[0-9]+ \(cmake_pkg_config\):
+  Resolution failed for file[^
+]*(.)*/PackageRoot/no-description.pc'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestStrictness.cmake:[0-9]+ \(cmake_pkg_config\):
+  Resolution failed for file[^
+]*(.)*/PackageRoot/no-version.pc'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestStrictness.cmake:[0-9]+ \(cmake_pkg_config\):
+  Parsing failed for file[^
+]*(.)*/PackageRoot/invalid.pc'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+Cflags: lowercase
+CFlags: uppercase
+Cflags: lowercase uppercase

+ 38 - 0
Tests/RunCMake/cmake_pkg_config/TestStrictness-STRICT-stderr.txt

@@ -0,0 +1,38 @@
+CMake Warning at TestStrictness.cmake:[0-9]+ \(cmake_pkg_config\):
+  Resolution failed for file[^
+]*(.)*/PackageRoot/no-name.pc'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestStrictness.cmake:[0-9]+ \(cmake_pkg_config\):
+  Resolution failed for file[^
+]*(.)*/PackageRoot/no-description.pc'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestStrictness.cmake:[0-9]+ \(cmake_pkg_config\):
+  Resolution failed for file[^
+]*(.)*/PackageRoot/no-version.pc'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestStrictness.cmake:[0-9]+ \(cmake_pkg_config\):
+  Parsing failed for file[^
+]*(.)*/PackageRoot/invalid.pc'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+Cflags: lowercase
+CFlags: uppercase
+CMake Warning at TestStrictness.cmake:[0-9]+ \(cmake_pkg_config\):
+  Resolution failed for file[^
+]*(.)*/PackageRoot/cflags-bothcase-f.pc'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+Cflags:

+ 51 - 0
Tests/RunCMake/cmake_pkg_config/TestStrictness.cmake

@@ -0,0 +1,51 @@
+cmake_pkg_config(
+  EXTRACT foo
+  STRICTNESS ${STRICTNESS}
+  REQUIRED
+)
+
+cmake_pkg_config(
+  EXTRACT empty-key
+  STRICTNESS ${STRICTNESS}
+  REQUIRED
+)
+
+cmake_pkg_config(
+  EXTRACT no-name
+  STRICTNESS ${STRICTNESS}
+)
+
+cmake_pkg_config(
+  EXTRACT no-description
+  STRICTNESS ${STRICTNESS}
+)
+
+cmake_pkg_config(
+  EXTRACT no-version
+  STRICTNESS ${STRICTNESS}
+)
+
+cmake_pkg_config(
+  EXTRACT invalid
+  STRICTNESS ${STRICTNESS}
+)
+
+cmake_pkg_config(
+  EXTRACT cflags-lowercase-f
+  STRICTNESS ${STRICTNESS}
+)
+message("Cflags: ${CMAKE_PKG_CONFIG_CFLAGS}")
+
+set(CMAKE_PKG_CONFIG_CFLAGS)
+cmake_pkg_config(
+  EXTRACT cflags-uppercase-f
+  STRICTNESS ${STRICTNESS}
+)
+message("CFlags: ${CMAKE_PKG_CONFIG_CFLAGS}")
+
+set(CMAKE_PKG_CONFIG_CFLAGS)
+cmake_pkg_config(
+  EXTRACT cflags-bothcase-f
+  STRICTNESS ${STRICTNESS}
+)
+message("Cflags: ${CMAKE_PKG_CONFIG_CFLAGS}")

+ 2 - 0
Tests/RunCMake/cmake_pkg_config/TestUninstalled-stderr.txt

@@ -0,0 +1,2 @@
+QuxUninstalled
+QuxInstalled

+ 10 - 0
Tests/RunCMake/cmake_pkg_config/TestUninstalled.cmake

@@ -0,0 +1,10 @@
+cmake_pkg_config(EXTRACT qux)
+
+message(${CMAKE_PKG_CONFIG_CFLAGS})
+
+cmake_pkg_config(
+  EXTRACT qux
+  DISABLE_UNINSTALLED ON
+)
+
+message(${CMAKE_PKG_CONFIG_CFLAGS})

+ 103 - 0
Tests/RunCMake/cmake_pkg_config/TestVersion-stderr.txt

@@ -0,0 +1,103 @@
+CMake Warning at TestVersion.cmake:[0-9]+ \(cmake_pkg_config\):
+  Package 'a' version 'aa' does not meet version requirement '<a'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestVersion.cmake:[0-9]+ \(cmake_pkg_config\):
+  Package 'a' version 'aa' does not meet version requirement '>aaa'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestVersion.cmake:[0-9]+ \(cmake_pkg_config\):
+  Package 'a' version 'aa' does not meet version requirement '>bb'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestVersion.cmake:[0-9]+ \(cmake_pkg_config\):
+  Package 'a' version 'aa' does not meet version requirement '>1'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestVersion.cmake:[0-9]+ \(cmake_pkg_config\):
+  Package 'empty-key' version '' does not meet version requirement '!='
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestVersion.cmake:[0-9]+ \(cmake_pkg_config\):
+  Package 'empty-key' version '' does not meet version requirement '=0'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestVersion.cmake:[0-9]+ \(cmake_pkg_config\):
+  Package 'one' version '11' does not meet version requirement '<1'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestVersion.cmake:[0-9]+ \(cmake_pkg_config\):
+  Package 'one' version '11' does not meet version requirement '>111'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestVersion.cmake:[0-9]+ \(cmake_pkg_config\):
+  Package 'one' version '11' does not meet version requirement '>22'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestVersion.cmake:[0-9]+ \(cmake_pkg_config\):
+  Package 'one' version '11' does not meet version requirement '<a'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestVersion.cmake:[0-9]+ \(cmake_pkg_config\):
+  Package 'onedot' version '1.1.1' does not meet version requirement '>1.2.1'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestVersion.cmake:[0-9]+ \(cmake_pkg_config\):
+  Package 'onedot' version '1.1.1' does not meet version requirement '>
+  1.2.1'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestVersion.cmake:[0-9]+ \(cmake_pkg_config\):
+  Package 'onedot' version '1.1.1' does not meet exact version requirement
+  '01.01.01'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestVersion.cmake:[0-9]+ \(cmake_pkg_config\):
+  Package 'pseudo-empty' version '~0' does not meet version requirement '=~'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestVersion.cmake:[0-9]+ \(cmake_pkg_config\):
+  Package 'pseudo-empty' version '~0' does not meet version requirement
+  '!=~0'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestVersion.cmake:[0-9]+ \(cmake_pkg_config\):
+  Package 'tilde' version '~~1' does not meet version requirement '>~1'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+
+
+CMake Warning at TestVersion.cmake:[0-9]+ \(cmake_pkg_config\):
+  Package 'tilde' version '~~1' does not meet version requirement '<~~~1'
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)

+ 65 - 0
Tests/RunCMake/cmake_pkg_config/TestVersion.cmake

@@ -0,0 +1,65 @@
+set(CMAKE_PKG_CONFIG_PC_PATH ${CMAKE_CURRENT_LIST_DIR}/PackageRoot/VersionPackages)
+
+# Good = Should Succeed
+# Bad  = Should Warn
+
+cmake_pkg_config(EXTRACT a =aa)                   # Good
+cmake_pkg_config(EXTRACT a >a)                    # Good
+cmake_pkg_config(EXTRACT a <a)                    # Bad
+cmake_pkg_config(EXTRACT a >aaa)                  # Bad
+cmake_pkg_config(EXTRACT a <aaa)                  # Good
+
+cmake_pkg_config(EXTRACT a !=bb)                  # Good
+cmake_pkg_config(EXTRACT a >bb)                   # Bad
+cmake_pkg_config(EXTRACT a <bb)                   # Good
+
+cmake_pkg_config(EXTRACT a >1)                    # Bad
+cmake_pkg_config(EXTRACT a <1)                    # Good
+
+cmake_pkg_config(EXTRACT empty-key =)             # Good
+cmake_pkg_config(EXTRACT empty-key !=)            # Bad
+cmake_pkg_config(EXTRACT empty-key =0)            # Bad
+cmake_pkg_config(EXTRACT empty-key !=0)           # Good
+
+cmake_pkg_config(EXTRACT empty-key EXACT)         # Good
+
+cmake_pkg_config(EXTRACT one =11)                 # Good
+cmake_pkg_config(EXTRACT one >1)                  # Good
+cmake_pkg_config(EXTRACT one <1)                  # Bad
+cmake_pkg_config(EXTRACT one >111)                # Bad
+cmake_pkg_config(EXTRACT one <111)                # Good
+
+cmake_pkg_config(EXTRACT one !=22)                # Good
+cmake_pkg_config(EXTRACT one >22)                 # Bad
+cmake_pkg_config(EXTRACT one <22)                 # Good
+
+cmake_pkg_config(EXTRACT one >a)                  # Good
+cmake_pkg_config(EXTRACT one <a)                  # Bad
+
+cmake_pkg_config(EXTRACT onedot 1.1.1)            # Good
+cmake_pkg_config(EXTRACT onedot 01.01.01)         # Good
+cmake_pkg_config(EXTRACT onedot =1.1.1)           # Good
+cmake_pkg_config(EXTRACT onedot =01.01.01)        # Good
+cmake_pkg_config(EXTRACT onedot <1.2.1)           # Good
+cmake_pkg_config(EXTRACT onedot >1.2.1)           # Bad
+
+cmake_pkg_config(EXTRACT onedot "< 1.2.1")        # Good
+cmake_pkg_config(EXTRACT onedot "> 1.2.1")        # Bad
+
+cmake_pkg_config(EXTRACT onedot 1.1.1 EXACT)      # Good
+cmake_pkg_config(EXTRACT onedot =1.1.1 EXACT)     # Good
+cmake_pkg_config(EXTRACT onedot =01.01.01 EXACT)  # Bad
+
+cmake_pkg_config(EXTRACT pseudo-empty =~)         # Bad
+cmake_pkg_config(EXTRACT pseudo-empty !=~)        # Good
+cmake_pkg_config(EXTRACT pseudo-empty =~0)        # Good
+cmake_pkg_config(EXTRACT pseudo-empty !=~0)       # Bad
+
+cmake_pkg_config(EXTRACT tilde =~~1)              # Good
+cmake_pkg_config(EXTRACT tilde <~1)               # Good
+cmake_pkg_config(EXTRACT tilde >~1)               # Bad
+cmake_pkg_config(EXTRACT tilde <~~~1)             # Bad
+cmake_pkg_config(EXTRACT tilde >~~~1)             # Good
+
+cmake_pkg_config(EXTRACT zeroone =1)              # Good
+cmake_pkg_config(EXTRACT zeroone =001)            # Good