Przeglądaj źródła

find_package: Actually find .cps files

Add a helper class to read CPS files. Use this to teach find_package how
to consider and accept .cps files in its search. (Note that no version
testing is performed at this time.) Add a simple test that we can find a
package from a .cps file and correctly extract the version information.

Note that this doesn't actually import anything from CPS yet.
Matthew Woehlke 11 miesięcy temu
rodzic
commit
91c31ada23

+ 2 - 0
Source/CMakeLists.txt

@@ -412,6 +412,8 @@ add_library(
   cmNewLineStyle.cxx
   cmOrderDirectories.cxx
   cmOrderDirectories.h
+  cmPackageInfoReader.cxx
+  cmPackageInfoReader.h
   cmPathResolver.cxx
   cmPathResolver.h
   cmPlistParser.cxx

+ 59 - 18
Source/cmFindPackageCommand.cxx

@@ -1503,7 +1503,12 @@ bool cmFindPackageCommand::HandlePackageMode(
     this->StoreVersionFound();
 
     // Parse the configuration file.
-    if (this->ReadListFile(this->FileFound, DoPolicyScope)) {
+    if (this->CpsReader) {
+      // FIXME TODO
+
+      // The package has been found.
+      found = true;
+    } else if (this->ReadListFile(this->FileFound, DoPolicyScope)) {
       // The package has been found.
       found = true;
 
@@ -2487,27 +2492,63 @@ bool cmFindPackageCommand::CheckVersion(std::string const& config_file)
   bool haveResult = false;
   std::string version = "unknown";
 
-  // Get the filename without the .cmake extension.
+  // Get the file extension.
   std::string::size_type pos = config_file.rfind('.');
-  std::string version_file_base = config_file.substr(0, pos);
+  std::string ext = cmSystemTools::LowerCase(config_file.substr(pos));
+
+  if (ext == ".cps"_s) {
+    std::unique_ptr<cmPackageInfoReader> reader =
+      cmPackageInfoReader::Read(config_file);
+    if (reader && reader->GetName() == this->Name) {
+      cm::optional<std::string> cpsVersion = reader->GetVersion();
+      if (cpsVersion) {
+        // TODO: Implement version check for CPS
+        this->VersionFound = (version = std::move(*cpsVersion));
+
+        std::vector<unsigned> const& versionParts = reader->ParseVersion();
+        this->VersionFoundCount = static_cast<unsigned>(versionParts.size());
+        switch (this->VersionFoundCount) {
+          case 4:
+            this->VersionFoundTweak = versionParts[3];
+            CM_FALLTHROUGH;
+          case 3:
+            this->VersionFoundPatch = versionParts[2];
+            CM_FALLTHROUGH;
+          case 2:
+            this->VersionFoundMinor = versionParts[1];
+            CM_FALLTHROUGH;
+          case 1:
+            this->VersionFoundMajor = versionParts[0];
+            CM_FALLTHROUGH;
+          default:
+            break;
+        }
+      }
+      this->CpsReader = std::move(reader);
+      result = true;
+    }
+  } else {
+    // Get the filename without the .cmake extension.
+    std::string version_file_base = config_file.substr(0, pos);
 
-  // Look for foo-config-version.cmake
-  std::string version_file = cmStrCat(version_file_base, "-version.cmake");
-  if (!haveResult && cmSystemTools::FileExists(version_file, true)) {
-    result = this->CheckVersionFile(version_file, version);
-    haveResult = true;
-  }
+    // Look for foo-config-version.cmake
+    std::string version_file = cmStrCat(version_file_base, "-version.cmake");
+    if (!haveResult && cmSystemTools::FileExists(version_file, true)) {
+      result = this->CheckVersionFile(version_file, version);
+      haveResult = true;
+    }
 
-  // Look for fooConfigVersion.cmake
-  version_file = cmStrCat(version_file_base, "Version.cmake");
-  if (!haveResult && cmSystemTools::FileExists(version_file, true)) {
-    result = this->CheckVersionFile(version_file, version);
-    haveResult = true;
-  }
+    // Look for fooConfigVersion.cmake
+    version_file = cmStrCat(version_file_base, "Version.cmake");
+    if (!haveResult && cmSystemTools::FileExists(version_file, true)) {
+      result = this->CheckVersionFile(version_file, version);
+      haveResult = true;
+    }
 
-  // If no version was requested a versionless package is acceptable.
-  if (!haveResult && this->Version.empty()) {
-    result = true;
+    // If no version was requested a versionless package is acceptable.
+    if (!haveResult && this->Version.empty()) {
+      result = true;
+    }
   }
 
   ConfigFileInfo configFileInfo;

+ 4 - 0
Source/cmFindPackageCommand.h

@@ -7,6 +7,7 @@
 #include <cstddef>
 #include <functional>
 #include <map>
+#include <memory>
 #include <set>
 #include <string>
 #include <utility>
@@ -17,6 +18,7 @@
 #include <cm3p/kwiml/int.h>
 
 #include "cmFindCommon.h"
+#include "cmPackageInfoReader.h"
 #include "cmPolicies.h"
 
 // IWYU insists we should forward-declare instead of including <functional>,
@@ -283,6 +285,8 @@ private:
   };
   std::vector<ConfigFileInfo> ConsideredConfigs;
 
+  std::unique_ptr<cmPackageInfoReader> CpsReader;
+
   friend struct std::hash<ConfigFileInfo>;
 };
 

+ 146 - 0
Source/cmPackageInfoReader.cxx

@@ -0,0 +1,146 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "cmPackageInfoReader.h"
+
+#include <limits>
+
+#include <cm/string_view>
+#include <cmext/string_view>
+
+#include <cm3p/json/reader.h>
+#include <cm3p/json/value.h>
+#include <cm3p/json/version.h>
+
+#include "cmsys/FStream.hxx"
+
+#include "cmStringAlgorithms.h"
+#include "cmSystemTools.h"
+
+namespace {
+
+Json::Value ReadJson(std::string const& fileName)
+{
+  // Open the specified file.
+  cmsys::ifstream file(fileName.c_str(), std::ios::in | std::ios::binary);
+  if (!file) {
+#if JSONCPP_VERSION_HEXA < 0x01070300
+    return Json::Value::null;
+#else
+    return Json::Value::nullSingleton();
+#endif
+  }
+
+  // Read file content and translate JSON.
+  Json::Value data;
+  Json::CharReaderBuilder builder;
+  builder["collectComments"] = false;
+  if (!Json::parseFromStream(builder, file, &data, nullptr)) {
+#if JSONCPP_VERSION_HEXA < 0x01070300
+    return Json::Value::null;
+#else
+    return Json::Value::nullSingleton();
+#endif
+  }
+
+  return data;
+}
+
+bool CheckSchemaVersion(Json::Value const& data)
+{
+  std::string const& version = data["cps_version"].asString();
+
+  // Check that a valid version is specified.
+  if (version.empty()) {
+    return false;
+  }
+
+  // Check that we understand this version.
+  return cmSystemTools::VersionCompare(cmSystemTools::OP_GREATER_EQUAL,
+                                       version, "0.12") &&
+    cmSystemTools::VersionCompare(cmSystemTools::OP_LESS, version, "1");
+
+  // TODO Eventually this probably needs to return the version tuple, and
+  // should share code with cmPackageInfoReader::ParseVersion.
+}
+
+} // namespace
+
+std::unique_ptr<cmPackageInfoReader> cmPackageInfoReader::Read(
+  std::string const& path)
+{
+  // Read file and perform some basic validation:
+  //   - the input is valid JSON
+  //   - the input is a JSON object
+  //   - the input has a "cps_version" that we (in theory) know how to parse
+  Json::Value data = ReadJson(path);
+  if (!data.isObject() || !CheckSchemaVersion(data)) {
+    return nullptr;
+  }
+
+  //   - the input has a "name" attribute that is a non-empty string
+  Json::Value const& name = data["name"];
+  if (!name.isString() || name.empty()) {
+    return nullptr;
+  }
+
+  //   - the input has a "components" attribute that is a JSON object
+  if (!data["components"].isObject()) {
+    return nullptr;
+  }
+
+  // Seems sane enough to hand back to the caller.
+  std::unique_ptr<cmPackageInfoReader> reader{ new cmPackageInfoReader };
+  reader->Data = data;
+
+  return reader;
+}
+
+std::string cmPackageInfoReader::GetName() const
+{
+  return this->Data["name"].asString();
+}
+
+cm::optional<std::string> cmPackageInfoReader::GetVersion() const
+{
+  Json::Value const& version = this->Data["version"];
+  if (version.isString()) {
+    return version.asString();
+  }
+  return cm::nullopt;
+}
+
+std::vector<unsigned> cmPackageInfoReader::ParseVersion() const
+{
+  // Check that we have a version.
+  cm::optional<std::string> const& version = this->GetVersion();
+  if (!version) {
+    return {};
+  }
+
+  std::vector<unsigned> result;
+  cm::string_view remnant{ *version };
+
+  // Check if we know how to parse the version.
+  Json::Value const& schema = this->Data["version_schema"];
+  if (schema.isNull() || cmStrCaseEq(schema.asString(), "simple"_s)) {
+    // Keep going until we run out of parts.
+    while (!remnant.empty()) {
+      std::string::size_type n = remnant.find('.');
+      cm::string_view part = remnant.substr(0, n);
+      if (n == std::string::npos) {
+        remnant = {};
+      } else {
+        remnant = remnant.substr(n + 1);
+      }
+
+      unsigned long const value = std::stoul(std::string{ part }, &n);
+      if (n == 0 || value > std::numeric_limits<unsigned>::max()) {
+        // The part was not a valid number or is too big.
+        return {};
+      }
+      result.push_back(static_cast<unsigned>(value));
+    }
+  }
+
+  return result;
+}

+ 42 - 0
Source/cmPackageInfoReader.h

@@ -0,0 +1,42 @@
+/* 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 <memory>
+#include <string>
+#include <vector>
+
+#include <cm/optional>
+
+#include <cm3p/json/value.h>
+
+// class cmExecutionStatus;
+
+/** \class cmPackageInfoReader
+ * \brief Read and parse CPS files.
+ *
+ * This class encapsulates the functionality to read package configuration
+ * files which use the Common Package Specification, and provides utilities to
+ * translate the declarations therein into imported targets.
+ */
+class cmPackageInfoReader
+{
+public:
+  static std::unique_ptr<cmPackageInfoReader> Read(std::string const& path);
+
+  std::string GetName() const;
+  cm::optional<std::string> GetVersion() const;
+
+  /// If the package uses the 'simple' version scheme, obtain the version as
+  /// a numeric tuple.  Returns an empty vector for other schemes or if no
+  /// version is specified.
+  std::vector<unsigned> ParseVersion() const;
+
+private:
+  cmPackageInfoReader() = default;
+
+  std::string Path;
+  Json::Value Data;
+};

+ 1 - 0
Tests/CMakeLists.txt

@@ -352,6 +352,7 @@ if(BUILD_TESTING)
   # add a bunch of standard build-and-test style tests
   ADD_TEST_MACRO(CommandLineTest CommandLineTest)
   ADD_TEST_MACRO(FindPackageCMakeTest FindPackageCMakeTest)
+  ADD_TEST_MACRO(FindPackageCpsTest FindPackageCpsTest)
   ADD_TEST_MACRO(StringFileTest StringFileTest)
   ADD_TEST_MACRO(TryCompile TryCompile)
   ADD_TEST_MACRO(SystemInformation SystemInformation)

+ 38 - 0
Tests/FindPackageCpsTest/CMakeLists.txt

@@ -0,0 +1,38 @@
+cmake_minimum_required(VERSION 3.31)
+project(FindPackageCpsTest)
+
+# Protect tests from running inside the default install prefix.
+set(CMAKE_INSTALL_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/NotDefaultPrefix")
+
+# Disable built-in search paths.
+set(CMAKE_FIND_USE_PACKAGE_ROOT_PATH OFF)
+set(CMAKE_FIND_USE_CMAKE_ENVIRONMENT_PATH OFF)
+set(CMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH OFF)
+set(CMAKE_FIND_USE_CMAKE_SYSTEM_PATH OFF)
+set(CMAKE_FIND_USE_INSTALL_PREFIX OFF)
+
+# Enable framework searching.
+set(CMAKE_FIND_FRAMEWORK FIRST)
+
+add_executable(FindPackageCpsTest FindPackageTest.cxx)
+
+###############################################################################
+# Test a basic package search.
+set(CMAKE_PREFIX_PATH ${CMAKE_CURRENT_SOURCE_DIR})
+
+find_package(Sample CONFIG)
+if(NOT Sample_FOUND)
+  message(SEND_ERROR "Sample not found !")
+elseif(NOT Sample_VERSION STREQUAL "2.10.11")
+  message(SEND_ERROR "Sample wrong version ${Sample_VERSION} !")
+elseif(NOT Sample_VERSION_MAJOR EQUAL 2)
+  message(SEND_ERROR "Sample wrong major version ${Sample_VERSION_MAJOR} !")
+elseif(NOT Sample_VERSION_MINOR EQUAL 10)
+  message(SEND_ERROR "Sample wrong minor version ${Sample_VERSION_MINOR} !")
+elseif(NOT Sample_VERSION_PATCH EQUAL 11)
+  message(SEND_ERROR "Sample wrong patch version ${Sample_VERSION_PATCH} !")
+elseif(NOT Sample_VERSION_TWEAK EQUAL 0)
+  message(SEND_ERROR "Sample wrong tweak version ${Sample_VERSION_TWEAK} !")
+endif()
+
+set(CMAKE_PREFIX_PATH)

+ 4 - 0
Tests/FindPackageCpsTest/FindPackageTest.cxx

@@ -0,0 +1,4 @@
+int main()
+{
+  return 0;
+}

+ 8 - 0
Tests/FindPackageCpsTest/cps/sample.cps

@@ -0,0 +1,8 @@
+{
+  "cps_version": "0.13",
+  "name": "Sample",
+  "version": "2.10.11",
+  "compat_version": "2.0.0",
+  "cps_path": "@prefix@/cps",
+  "components": {}
+}

+ 1 - 0
bootstrap

@@ -468,6 +468,7 @@ CMAKE_CXX_SOURCES="\
   cmGccDepfileLexerHelper \
   cmGccDepfileReader \
   cmReturnCommand \
+  cmPackageInfoReader \
   cmPlaceholderExpander \
   cmPlistParser \
   cmRulePlaceholderExpander \