diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index cb758bcce0..31bc8054e9 100644 --- a/Source/CMakeLists.txt +++ b/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 diff --git a/Source/cmFindPackageCommand.cxx b/Source/cmFindPackageCommand.cxx index 41974c7e59..9ada7700a2 100644 --- a/Source/cmFindPackageCommand.cxx +++ b/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)); - // 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; - } + if (ext == ".cps"_s) { + std::unique_ptr reader = + cmPackageInfoReader::Read(config_file); + if (reader && reader->GetName() == this->Name) { + cm::optional cpsVersion = reader->GetVersion(); + if (cpsVersion) { + // TODO: Implement version check for CPS + this->VersionFound = (version = std::move(*cpsVersion)); - // 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; - } + std::vector const& versionParts = reader->ParseVersion(); + this->VersionFoundCount = static_cast(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); - // If no version was requested a versionless package is acceptable. - if (!haveResult && this->Version.empty()) { - result = 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; + } + + // If no version was requested a versionless package is acceptable. + if (!haveResult && this->Version.empty()) { + result = true; + } } ConfigFileInfo configFileInfo; diff --git a/Source/cmFindPackageCommand.h b/Source/cmFindPackageCommand.h index e4e0a7f4e7..3893e97b06 100644 --- a/Source/cmFindPackageCommand.h +++ b/Source/cmFindPackageCommand.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,7 @@ #include #include "cmFindCommon.h" +#include "cmPackageInfoReader.h" #include "cmPolicies.h" // IWYU insists we should forward-declare instead of including , @@ -283,6 +285,8 @@ private: }; std::vector ConsideredConfigs; + std::unique_ptr CpsReader; + friend struct std::hash; }; diff --git a/Source/cmPackageInfoReader.cxx b/Source/cmPackageInfoReader.cxx new file mode 100644 index 0000000000..326fe3368b --- /dev/null +++ b/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 + +#include +#include + +#include +#include +#include + +#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::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 reader{ new cmPackageInfoReader }; + reader->Data = data; + + return reader; +} + +std::string cmPackageInfoReader::GetName() const +{ + return this->Data["name"].asString(); +} + +cm::optional cmPackageInfoReader::GetVersion() const +{ + Json::Value const& version = this->Data["version"]; + if (version.isString()) { + return version.asString(); + } + return cm::nullopt; +} + +std::vector cmPackageInfoReader::ParseVersion() const +{ + // Check that we have a version. + cm::optional const& version = this->GetVersion(); + if (!version) { + return {}; + } + + std::vector 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::max()) { + // The part was not a valid number or is too big. + return {}; + } + result.push_back(static_cast(value)); + } + } + + return result; +} diff --git a/Source/cmPackageInfoReader.h b/Source/cmPackageInfoReader.h new file mode 100644 index 0000000000..d135387287 --- /dev/null +++ b/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 +#include +#include + +#include + +#include + +// 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 Read(std::string const& path); + + std::string GetName() const; + cm::optional 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 ParseVersion() const; + +private: + cmPackageInfoReader() = default; + + std::string Path; + Json::Value Data; +}; diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index f102bb43b0..2b0e02a120 100644 --- a/Tests/CMakeLists.txt +++ b/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) diff --git a/Tests/FindPackageCpsTest/CMakeLists.txt b/Tests/FindPackageCpsTest/CMakeLists.txt new file mode 100644 index 0000000000..c7e6008799 --- /dev/null +++ b/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) diff --git a/Tests/FindPackageCpsTest/FindPackageTest.cxx b/Tests/FindPackageCpsTest/FindPackageTest.cxx new file mode 100644 index 0000000000..f8b643afbf --- /dev/null +++ b/Tests/FindPackageCpsTest/FindPackageTest.cxx @@ -0,0 +1,4 @@ +int main() +{ + return 0; +} diff --git a/Tests/FindPackageCpsTest/cps/sample.cps b/Tests/FindPackageCpsTest/cps/sample.cps new file mode 100644 index 0000000000..41ec3c34b7 --- /dev/null +++ b/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": {} +} diff --git a/bootstrap b/bootstrap index b41197f8ff..e6d16f05db 100755 --- a/bootstrap +++ b/bootstrap @@ -468,6 +468,7 @@ CMAKE_CXX_SOURCES="\ cmGccDepfileLexerHelper \ cmGccDepfileReader \ cmReturnCommand \ + cmPackageInfoReader \ cmPlaceholderExpander \ cmPlistParser \ cmRulePlaceholderExpander \