diff --git a/Source/cmFindPackageCommand.cxx b/Source/cmFindPackageCommand.cxx index 37b7a0a308..69cc2d6c97 100644 --- a/Source/cmFindPackageCommand.cxx +++ b/Source/cmFindPackageCommand.cxx @@ -23,6 +23,7 @@ #include "cmAlgorithms.h" #include "cmDependencyProvider.h" +#include "cmExecutionStatus.h" #include "cmExperimental.h" #include "cmList.h" #include "cmListFileCache.h" @@ -55,8 +56,6 @@ # endif #endif -class cmExecutionStatus; - namespace { using pdt = cmFindPackageCommand::PackageDescriptionType; @@ -560,6 +559,41 @@ void cmFindPackageCommand::AppendSearchPathGroups() this->LabeledPaths.emplace(PathLabel::SystemRegistry, cmSearchPath{ this }); } +void cmFindPackageCommand::InheritOptions(cmFindPackageCommand* other) +{ + this->RequiredCMakeVersion = other->RequiredCMakeVersion; + this->LibraryArchitecture = other->LibraryArchitecture; + this->UseLib32Paths = other->UseLib32Paths; + this->UseLib64Paths = other->UseLib64Paths; + this->UseLibx32Paths = other->UseLibx32Paths; + this->NoUserRegistry = other->NoUserRegistry; + this->NoSystemRegistry = other->NoSystemRegistry; + this->UseRealPath = other->UseRealPath; + this->SortOrder = other->SortOrder; + this->SortDirection = other->SortDirection; + + this->GlobalScope = other->GlobalScope; + this->RegistryView = other->RegistryView; + this->NoDefaultPath = other->NoDefaultPath; + this->NoPackageRootPath = other->NoPackageRootPath; + this->NoCMakePath = other->NoCMakePath; + this->NoCMakeEnvironmentPath = other->NoCMakeEnvironmentPath; + this->NoSystemEnvironmentPath = other->NoSystemEnvironmentPath; + this->NoCMakeSystemPath = other->NoCMakeSystemPath; + this->NoCMakeInstallPath = other->NoCMakeInstallPath; + this->FindRootPathMode = other->FindRootPathMode; + + this->SearchFrameworkLast = other->SearchFrameworkLast; + this->SearchFrameworkFirst = other->SearchFrameworkFirst; + this->SearchFrameworkOnly = other->SearchFrameworkOnly; + this->SearchAppBundleLast = other->SearchAppBundleLast; + this->SearchAppBundleFirst = other->SearchAppBundleFirst; + this->SearchAppBundleOnly = other->SearchAppBundleOnly; + this->SearchPathSuffixes = other->SearchPathSuffixes; + + this->Quiet = other->Quiet; +} + bool cmFindPackageCommand::InitialPass(std::vector const& args) { if (args.empty()) { @@ -645,7 +679,7 @@ bool cmFindPackageCommand::InitialPass(std::vector const& args) this->SortDirection = (*sd == "ASC") ? Asc : Dec; } - // Find what search path locations have been enabled/disable + // Find what search path locations have been enabled/disable. this->SelectDefaultSearchModes(); // Find the current root path mode. @@ -1510,12 +1544,7 @@ bool cmFindPackageCommand::HandlePackageMode( if (this->CpsReader) { // The package has been found. found = true; - - // Import targets. - cmPackageInfoReader* const reader = this->CpsReader.get(); - result = reader->ImportTargets(this->Makefile, this->Status) && - this->ImportTargetConfigurations(this->FileFound, reader) && - this->ImportAppendices(this->FileFound); + result = this->ReadPackage(); } else if (this->ReadListFile(this->FileFound, DoPolicyScope)) { // The package has been found. found = true; @@ -1816,6 +1845,36 @@ bool cmFindPackageCommand::FindEnvironmentConfig() }); } +cmFindPackageCommand::AppendixMap cmFindPackageCommand::FindAppendices( + std::string const& base) const +{ + AppendixMap appendices; + + // Find package appendices. + cmsys::Glob glob; + glob.RecurseOff(); + if (glob.FindFiles(cmStrCat(cmSystemTools::GetFilenamePath(base), "/"_s, + cmSystemTools::GetFilenameWithoutExtension(base), + "[-:]*.[Cc][Pp][Ss]"_s))) { + // Check glob results for valid appendices. + for (std::string const& extra : glob.GetFiles()) { + // Exclude configuration-specific files for now; we look at them later + // when we load their respective configuration-agnostic appendices. + if (extra.find('@') != std::string::npos) { + continue; + } + + std::unique_ptr reader = + cmPackageInfoReader::Read(extra, this->CpsReader.get()); + if (reader && reader->GetName() == this->Name) { + appendices.emplace(extra, std::move(reader)); + } + } + } + + return appendices; +} + bool cmFindPackageCommand::ReadListFile(const std::string& f, const PolicyScopeRule psr) { @@ -1833,53 +1892,114 @@ bool cmFindPackageCommand::ReadListFile(const std::string& f, return false; } -bool cmFindPackageCommand::ImportTargetConfigurations( - std::string const& base, cmPackageInfoReader* parent) +bool cmFindPackageCommand::ReadPackage() { - // Find supplemental configuration files. - cmsys::Glob glob; - glob.RecurseOff(); - if (glob.FindFiles(cmStrCat(cmSystemTools::GetFilenamePath(base), "/"_s, - cmSystemTools::GetFilenameWithoutExtension(base), - "@*.[Cc][Pp][Ss]"_s))) { + // Resolve any transitive dependencies. + if (!FindPackageDependencies(this->FileFound, *this->CpsReader, + this->Required)) { + return false; + } - // Try to read supplemental data from each file found. - for (std::string const& extra : glob.GetFiles()) { - std::unique_ptr const& reader = - cmPackageInfoReader::Read(extra, parent); - if (reader && reader->GetName() == this->Name) { - if (!reader->ImportTargetConfigurations(this->Makefile, - this->Status)) { - return false; - } + cmMakefile::CallRAII scope{ this->Makefile, this->FileFound, this->Status }; + + // Locate appendices. + cmFindPackageCommand::AppendixMap appendices = + this->FindAppendices(this->FileFound); + + auto iter = appendices.begin(); + while (iter != appendices.end()) { + bool providesRequiredComponents = false; // TODO + bool required = providesRequiredComponents && this->Required; + if (!this->FindPackageDependencies(iter->first, *iter->second, required)) { + if (providesRequiredComponents) { + return false; } + iter = appendices.erase(iter); + } else { + ++iter; + } + } + + // Import targets from root file. + if (!this->ImportPackageTargets(this->FileFound, *this->CpsReader)) { + return false; + } + + // Import targets from appendices. + for (auto const& appendix : appendices) { + cmMakefile::CallRAII appendixScope{ this->Makefile, appendix.first, + this->Status }; + if (!this->ImportPackageTargets(appendix.first, *appendix.second)) { + return false; } } return true; } -bool cmFindPackageCommand::ImportAppendices(std::string const& base) +bool cmFindPackageCommand::FindPackageDependencies( + std::string const& fileName, cmPackageInfoReader const& reader, + bool required) { - // Find package appendices. + // Get package requirements. + for (cmPackageRequirement const& dep : reader.GetRequirements()) { + cmExecutionStatus status{ *this->Makefile }; + cmMakefile::CallRAII scope{ this->Makefile, fileName, status }; + + // For each requirement, set up a nested instance to find it. + cmFindPackageCommand fp{ status }; + fp.InheritOptions(this); + + fp.Name = dep.Name; + fp.Required = required; + fp.UseFindModules = false; + fp.UseCpsFiles = true; + + fp.Version = dep.Version; + fp.VersionComplete = dep.Version; + fp.VersionCount = + parseVersion(fp.Version, fp.VersionMajor, fp.VersionMinor, + fp.VersionPatch, fp.VersionTweak); + + fp.Components = cmJoin(cmMakeRange(dep.Components), ";"_s); + fp.RequiredComponents = + std::set{ dep.Components.begin(), dep.Components.end() }; + + // TODO set hints + + // Try to find the requirement; fail if we can't. + if (!fp.FindPackage() || fp.FileFound.empty()) { + return false; + } + } + + // All requirements (if any) were found. + return true; +} + +bool cmFindPackageCommand::ImportPackageTargets(std::string const& fileName, + cmPackageInfoReader& reader) +{ + // Import base file. + if (!reader.ImportTargets(this->Makefile, this->Status)) { + return false; + } + + // Find supplemental configuration files. cmsys::Glob glob; glob.RecurseOff(); - if (glob.FindFiles(cmStrCat(cmSystemTools::GetFilenamePath(base), "/"_s, - cmSystemTools::GetFilenameWithoutExtension(base), - "[-:]*.[Cc][Pp][Ss]"_s))) { + if (glob.FindFiles( + cmStrCat(cmSystemTools::GetFilenamePath(fileName), "/"_s, + cmSystemTools::GetFilenameWithoutExtension(fileName), + "@*.[Cc][Pp][Ss]"_s))) { - // Try to read supplemental data from each appendix file found. + // Try to read supplemental data from each file found. for (std::string const& extra : glob.GetFiles()) { - // This loop should not consider configuration-specific files. - if (extra.find('@') != std::string::npos) { - continue; - } - - std::unique_ptr const& reader = - cmPackageInfoReader::Read(extra, this->CpsReader.get()); - if (reader && reader->GetName() == this->Name) { - if (!reader->ImportTargets(this->Makefile, this->Status) || - !this->ImportTargetConfigurations(extra, reader.get())) { + std::unique_ptr configReader = + cmPackageInfoReader::Read(extra, &reader); + if (configReader && configReader->GetName() == this->Name) { + if (!configReader->ImportTargetConfigurations(this->Makefile, + this->Status)) { return false; } } diff --git a/Source/cmFindPackageCommand.h b/Source/cmFindPackageCommand.h index 10a5026b64..a414f3aa5b 100644 --- a/Source/cmFindPackageCommand.h +++ b/Source/cmFindPackageCommand.h @@ -92,10 +92,12 @@ private: static PathLabel SystemRegistry; }; + void InheritOptions(cmFindPackageCommand* other); + // Try to find a package, assuming most state has already been set up. This // is used for recursive dependency solving, particularly when importing // packages via CPS. Bypasses providers if argsForProvider is empty. - bool FindPackage(std::vector const& argsForProvider); + bool FindPackage(std::vector const& argsForProvider = {}); bool FindPackageUsingModuleMode(); bool FindPackageUsingConfigMode(); @@ -137,9 +139,17 @@ private: DoPolicyScope }; bool ReadListFile(const std::string& f, PolicyScopeRule psr); - bool ImportTargetConfigurations(std::string const& base, - cmPackageInfoReader* parent); - bool ImportAppendices(std::string const& base); + bool ReadPackage(); + + using AppendixMap = + std::map>; + AppendixMap FindAppendices(std::string const& base) const; + bool FindPackageDependencies(std::string const& fileName, + cmPackageInfoReader const& reader, + bool required); + + bool ImportPackageTargets(std::string const& fileName, + cmPackageInfoReader& reader); void StoreVersionFound(); void SetConfigDirCacheVariable(const std::string& value); diff --git a/Source/cmPackageInfoReader.cxx b/Source/cmPackageInfoReader.cxx index e735304695..ceefb6f44d 100644 --- a/Source/cmPackageInfoReader.cxx +++ b/Source/cmPackageInfoReader.cxx @@ -464,6 +464,23 @@ std::vector cmPackageInfoReader::ParseVersion() const return result; } +std::vector cmPackageInfoReader::GetRequirements() const +{ + std::vector requirements; + + auto const& requirementObjects = this->Data["requires"]; + + for (auto ri = requirementObjects.begin(), re = requirementObjects.end(); + ri != re; ++ri) { + cmPackageRequirement r{ ri.name(), (*ri)["version"].asString(), + ReadList(*ri, "components"), + ReadList(*ri, "hints") }; + requirements.emplace_back(std::move(r)); + } + + return requirements; +} + std::string cmPackageInfoReader::ResolvePath(std::string path) const { cmSystemTools::ConvertToUnixSlashes(path); diff --git a/Source/cmPackageInfoReader.h b/Source/cmPackageInfoReader.h index 9f9510f45a..bcecd0e15c 100644 --- a/Source/cmPackageInfoReader.h +++ b/Source/cmPackageInfoReader.h @@ -20,6 +20,14 @@ class cmExecutionStatus; class cmMakefile; class cmTarget; +struct cmPackageRequirement +{ + std::string Name; + std::string Version; + std::vector Components; + std::vector Hints; +}; + /** \class cmPackageInfoReader * \brief Read and parse CPS files. * @@ -41,6 +49,8 @@ public: /// version is specified. std::vector ParseVersion() const; + std::vector GetRequirements() const; + /// Create targets for components specified in the CPS file. bool ImportTargets(cmMakefile* makefile, cmExecutionStatus& status); diff --git a/Tests/FindPackageCpsTest/CMakeLists.txt b/Tests/FindPackageCpsTest/CMakeLists.txt index 8b5a68e900..19f1bf32e9 100644 --- a/Tests/FindPackageCpsTest/CMakeLists.txt +++ b/Tests/FindPackageCpsTest/CMakeLists.txt @@ -161,3 +161,27 @@ endif() if(C_CXXONLY_H) message(SEND_ERROR "cmincludetest/cxxonly.h unexpectedly found in C mode ?!") endif() + +############################################################################### +# Find a package that has dependencies. + +find_package(Bar) +if(NOT Bar_FOUND) + message(SEND_ERROR "Bar not found !") +elseif(NOT Dep1_FOUND) + message(SEND_ERROR "Bar's Dep1 not found !") +elseif(NOT Dep2_FOUND) + message(SEND_ERROR "Bar's Dep2 not found !") +elseif(NOT Dep3_FOUND) + message(SEND_ERROR "Bar's Dep3 not found !") +elseif(NOT TARGET Dep1::Target) + message(SEND_ERROR "Dep1::Target missing !") +elseif(NOT TARGET Dep2::Target) + message(SEND_ERROR "Dep2::Target missing !") +elseif(NOT TARGET Dep3::Target) + message(SEND_ERROR "Dep3::Target missing !") +elseif(NOT TARGET Bar::Target1) + message(SEND_ERROR "Bar::Target1 missing !") +elseif(NOT TARGET Bar::Target2) + message(SEND_ERROR "Bar::Target2 missing !") +endif() diff --git a/Tests/FindPackageCpsTest/cmake/dep2-config.cmake b/Tests/FindPackageCpsTest/cmake/dep2-config.cmake new file mode 100644 index 0000000000..e2815f4ba3 --- /dev/null +++ b/Tests/FindPackageCpsTest/cmake/dep2-config.cmake @@ -0,0 +1,3 @@ +set(Dep2_FOUND TRUE) + +add_library(Dep2::Target INTERFACE IMPORTED) diff --git a/Tests/FindPackageCpsTest/cps/bar-extra.cps b/Tests/FindPackageCpsTest/cps/bar-extra.cps new file mode 100644 index 0000000000..688fc4a57d --- /dev/null +++ b/Tests/FindPackageCpsTest/cps/bar-extra.cps @@ -0,0 +1,13 @@ +{ + "cps_version": "0.13", + "name": "Bar", + "requires": { + "Dep3": null + }, + "components": { + "Target2": { + "type": "interface", + "requires": [ "Dep3:Target" ] + } + } +} diff --git a/Tests/FindPackageCpsTest/cps/bar.cps b/Tests/FindPackageCpsTest/cps/bar.cps new file mode 100644 index 0000000000..aeeebdae7e --- /dev/null +++ b/Tests/FindPackageCpsTest/cps/bar.cps @@ -0,0 +1,15 @@ +{ + "cps_version": "0.13", + "name": "Bar", + "cps_path": "@prefix@/cps", + "requires": { + "Dep1": null, + "Dep2": null + }, + "components": { + "Target1": { + "type": "interface", + "requires": [ "Dep1:Target", "Dep2:Target" ] + } + } +} diff --git a/Tests/FindPackageCpsTest/cps/dep1.cps b/Tests/FindPackageCpsTest/cps/dep1.cps new file mode 100644 index 0000000000..3b6917d873 --- /dev/null +++ b/Tests/FindPackageCpsTest/cps/dep1.cps @@ -0,0 +1,10 @@ +{ + "cps_version": "0.13", + "name": "Dep1", + "cps_path": "@prefix@/cps", + "components": { + "Target": { + "type": "interface" + } + } +} diff --git a/Tests/FindPackageCpsTest/cps/dep3.cps b/Tests/FindPackageCpsTest/cps/dep3.cps new file mode 100644 index 0000000000..6805f8ef05 --- /dev/null +++ b/Tests/FindPackageCpsTest/cps/dep3.cps @@ -0,0 +1,10 @@ +{ + "cps_version": "0.13", + "name": "Dep3", + "cps_path": "@prefix@/cps", + "components": { + "Target": { + "type": "interface" + } + } +} diff --git a/Tests/RunCMake/find_package/MissingTransitiveDependency-result.txt b/Tests/RunCMake/find_package/MissingTransitiveDependency-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/find_package/MissingTransitiveDependency-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/find_package/MissingTransitiveDependency-stderr.txt b/Tests/RunCMake/find_package/MissingTransitiveDependency-stderr.txt new file mode 100644 index 0000000000..ebb77a9227 --- /dev/null +++ b/Tests/RunCMake/find_package/MissingTransitiveDependency-stderr.txt @@ -0,0 +1,25 @@ +CMake Warning \(dev\) at MissingTransitiveDependency\.cmake:[0-9]+ \(find_package\): + CMake's support for importing package information in the Common Package + Specification format \(via find_package\) is experimental\. It is meant only + for experimentation and feedback to CMake developers. +Call Stack \(most recent call first\): + CMakeLists\.txt:[0-9]+ \(include\) +This warning is for project developers. Use -Wno-dev to suppress it. ++ +CMake Error in cps/[Ss]till[Ii]ncomplete\.cps: + Could not find a package configuration file provided by "DoesNotExist" with + any of the following names: + + DoesNotExist\.cps + doesnotexist\.cps + DoesNotExistConfig\.cmake + doesnotexist-config\.cmake + + Add the installation prefix of "DoesNotExist" to CMAKE_PREFIX_PATH or set + "DoesNotExist_DIR" to a directory containing one of the above files\. If + "DoesNotExist" provides a separate development package or SDK, be sure it + has been installed\. +Call Stack \(most recent call first\): + cps/[Ii]ncomplete\.cps + MissingTransitiveDependency\.cmake:[0-9]+ \(find_package\) + CMakeLists\.txt:[0-9]+ \(include\) diff --git a/Tests/RunCMake/find_package/MissingTransitiveDependency.cmake b/Tests/RunCMake/find_package/MissingTransitiveDependency.cmake new file mode 100644 index 0000000000..eca412511f --- /dev/null +++ b/Tests/RunCMake/find_package/MissingTransitiveDependency.cmake @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.31) + +set(CMAKE_EXPERIMENTAL_FIND_CPS_PACKAGES "e82e467b-f997-4464-8ace-b00808fff261") + +# 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) + +set(CMAKE_PREFIX_PATH ${CMAKE_CURRENT_SOURCE_DIR}) + +############################################################################### +# Test finding a package that is missing dependencies. +find_package(Incomplete REQUIRED) diff --git a/Tests/RunCMake/find_package/RunCMakeTest.cmake b/Tests/RunCMake/find_package/RunCMakeTest.cmake index 0552535e61..d5a7f8ba08 100644 --- a/Tests/RunCMake/find_package/RunCMakeTest.cmake +++ b/Tests/RunCMake/find_package/RunCMakeTest.cmake @@ -22,6 +22,7 @@ run_cmake_with_options(MissingConfigDebugPkg --debug-find-pkg=NotHere) run_cmake(MissingConfigOneName) run_cmake(MissingConfigRequired) run_cmake(MissingConfigVersion) +run_cmake(MissingTransitiveDependency) run_cmake(MixedModeOptions) run_cmake_with_options(ModuleModeDebugPkg --debug-find-pkg=Foo,Zot) run_cmake(PackageRoot) diff --git a/Tests/RunCMake/find_package/cps/incomplete.cps b/Tests/RunCMake/find_package/cps/incomplete.cps new file mode 100644 index 0000000000..4d3a104f6c --- /dev/null +++ b/Tests/RunCMake/find_package/cps/incomplete.cps @@ -0,0 +1,9 @@ +{ + "cps_version": "0.13", + "name": "Incomplete", + "cps_path": "@prefix@/cps", + "requires": { + "StillIncomplete": null + }, + "components": {} +} diff --git a/Tests/RunCMake/find_package/cps/stillincomplete.cps b/Tests/RunCMake/find_package/cps/stillincomplete.cps new file mode 100644 index 0000000000..12d23ac344 --- /dev/null +++ b/Tests/RunCMake/find_package/cps/stillincomplete.cps @@ -0,0 +1,9 @@ +{ + "cps_version": "0.13", + "name": "StillIncomplete", + "cps_path": "@prefix@/cps", + "requires": { + "DoesNotExist": null + }, + "components": {} +}