diff --git a/Source/cmFindPackageCommand.cxx b/Source/cmFindPackageCommand.cxx index 9ada7700a2..0d717fadab 100644 --- a/Source/cmFindPackageCommand.cxx +++ b/Source/cmFindPackageCommand.cxx @@ -17,6 +17,7 @@ #include "cmsys/Directory.hxx" #include "cmsys/FStream.hxx" +#include "cmsys/Glob.hxx" #include "cmsys/RegularExpression.hxx" #include "cmsys/String.h" @@ -1504,10 +1505,14 @@ bool cmFindPackageCommand::HandlePackageMode( // Parse the configuration file. if (this->CpsReader) { - // FIXME TODO - // 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); } else if (this->ReadListFile(this->FileFound, DoPolicyScope)) { // The package has been found. found = true; @@ -1825,6 +1830,62 @@ bool cmFindPackageCommand::ReadListFile(const std::string& f, return false; } +bool cmFindPackageCommand::ImportTargetConfigurations( + std::string const& base, cmPackageInfoReader* parent) +{ + // Find supplemental configuration files. + cmsys::Glob glob; + glob.RecurseOff(); + if (glob.FindFiles(cmStrCat(cmSystemTools::GetFilenamePath(base), "/"_s, + cmSystemTools::GetFilenameWithoutExtension(base), + "@*.[Cc][Pp][Ss]"_s))) { + + // 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; + } + } + } + } + + return true; +} + +bool cmFindPackageCommand::ImportAppendices(std::string const& base) +{ + // Find package appendices. + cmsys::Glob glob; + glob.RecurseOff(); + if (glob.FindFiles(cmStrCat(cmSystemTools::GetFilenamePath(base), "/"_s, + cmSystemTools::GetFilenameWithoutExtension(base), + "[-:]*.[Cc][Pp][Ss]"_s))) { + + // Try to read supplemental data from each appendix 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())) { + return false; + } + } + } + } + + return true; +} + void cmFindPackageCommand::AppendToFoundProperty(const bool found) { cmList foundContents; diff --git a/Source/cmFindPackageCommand.h b/Source/cmFindPackageCommand.h index 3893e97b06..7631c2c266 100644 --- a/Source/cmFindPackageCommand.h +++ b/Source/cmFindPackageCommand.h @@ -137,6 +137,9 @@ private: DoPolicyScope }; bool ReadListFile(const std::string& f, PolicyScopeRule psr); + bool ImportTargetConfigurations(std::string const& base, + cmPackageInfoReader* parent); + bool ImportAppendices(std::string const& base); void StoreVersionFound(); void SetConfigDirCacheVariable(const std::string& value); diff --git a/Source/cmPackageInfoReader.cxx b/Source/cmPackageInfoReader.cxx index 326fe3368b..f5b29c8653 100644 --- a/Source/cmPackageInfoReader.cxx +++ b/Source/cmPackageInfoReader.cxx @@ -2,7 +2,10 @@ file Copyright.txt or https://cmake.org/licensing for details. */ #include "cmPackageInfoReader.h" +#include #include +#include +#include #include #include @@ -12,12 +15,52 @@ #include #include "cmsys/FStream.hxx" +#include "cmsys/RegularExpression.hxx" +#include "cmExecutionStatus.h" +#include "cmListFileCache.h" +#include "cmMakefile.h" +#include "cmMessageType.h" +#include "cmStateTypes.h" #include "cmStringAlgorithms.h" #include "cmSystemTools.h" +#include "cmTarget.h" namespace { +// Map of CPS language names to CMake language name. Case insensitivity is +// achieved by converting the CPS value to lower case, so keys in this map must +// be lower case. +std::unordered_map Languages = { + // clang-format off + { "c", "C" }, + { "c++", "CXX" }, + { "cpp", "CXX" }, + { "cxx", "CXX" }, + { "objc", "OBJC" }, + { "objc++", "OBJCXX" }, + { "objcpp", "OBJCXX" }, + { "objcxx", "OBJCXX" }, + { "swift", "swift" }, + { "hip", "HIP" }, + { "cuda", "CUDA" }, + { "ispc", "ISPC" }, + { "c#", "CSharp" }, + { "csharp", "CSharp" }, + { "fortran", "Fortran" }, + // clang-format on +}; + +std::string GetRealPath(std::string const& path) +{ + return cmSystemTools::GetRealPath(path); +} + +std::string GetRealDir(std::string const& path) +{ + return cmSystemTools::GetFilenamePath(cmSystemTools::GetRealPath(path)); +} + Json::Value ReadJson(std::string const& fileName) { // Open the specified file. @@ -56,17 +99,270 @@ bool CheckSchemaVersion(Json::Value const& data) // Check that we understand this version. return cmSystemTools::VersionCompare(cmSystemTools::OP_GREATER_EQUAL, - version, "0.12") && + version, "0.13") && cmSystemTools::VersionCompare(cmSystemTools::OP_LESS, version, "1"); // TODO Eventually this probably needs to return the version tuple, and // should share code with cmPackageInfoReader::ParseVersion. } +bool ComparePathSuffix(std::string const& path, std::string const& suffix) +{ + std::string const& tail = path.substr(path.size() - suffix.size()); + return cmSystemTools::ComparePath(tail, suffix); +} + +std::string DeterminePrefix(std::string const& filepath, + Json::Value const& data) +{ + // First check if an absolute prefix was supplied. + std::string prefix = data["prefix"].asString(); + if (!prefix.empty()) { + // Ensure that the specified prefix is valid. + if (cmsys::SystemTools::FileIsFullPath(prefix) && + cmsys::SystemTools::FileIsDirectory(prefix)) { + cmSystemTools::ConvertToUnixSlashes(prefix); + return prefix; + } + // The specified absolute prefix is not valid. + return {}; + } + + // Get and validate prefix-relative path. + std::string relPath = data["cps_path"].asString(); + cmSystemTools::ConvertToUnixSlashes(relPath); + if (relPath.empty() || !cmHasLiteralPrefix(relPath, "@prefix@/")) { + // The relative prefix is not valid. + return {}; + } + relPath = relPath.substr(8); + + // Get directory portion of the absolute path. + std::string const& absPath = cmSystemTools::GetFilenamePath(filepath); + if (ComparePathSuffix(absPath, relPath)) { + return absPath.substr(0, absPath.size() - relPath.size()); + } + + for (auto* const f : { GetRealPath, GetRealDir }) { + std::string const& tmpPath = (*f)(absPath); + if (!cmSystemTools::ComparePath(tmpPath, absPath) && + ComparePathSuffix(tmpPath, relPath)) { + return tmpPath.substr(0, tmpPath.size() - relPath.size()); + } + } + + return {}; +} + +// Extract key name from value iterator as string_view. +cm::string_view IterKey(Json::Value::const_iterator const& iter) +{ + char const* end; + char const* const start = iter.memberName(&end); + return { start, static_cast(end - start) }; +} + +// Get list-of-strings value from object. +std::vector ReadList(Json::Value const& data, char const* key) +{ + std::vector result; + + Json::Value const& arr = data[key]; + if (arr.isArray()) { + for (Json::Value const& val : arr) { + if (val.isString()) { + result.push_back(val.asString()); + } + } + } + + return result; +} + +std::string NormalizeTargetName(std::string const& name, + std::string const& context) +{ + if (cmHasLiteralPrefix(name, ":")) { + return cmStrCat(context, name); + } + + std::string::size_type const n = name.find_first_of(':'); + if (n != std::string::npos) { + cm::string_view v{ name }; + return cmStrCat(v.substr(0, n), ':', v.substr(n)); + } + return name; +} + +std::string ResolvePrefix(cm::string_view str, cm::string_view prefix) +{ + if (cmHasPrefix(str, "@prefix@"_s)) { + return cmStrCat(prefix, str.substr(8)); + } + return std::string{ str }; +} + +void AppendProperty(cmMakefile* makefile, cmTarget* target, + cm::string_view property, cm::string_view configuration, + std::string const& value) +{ + std::string fullprop; + if (configuration.empty()) { + fullprop = cmStrCat("INTERFACE_"_s, property); + } else { + fullprop = cmStrCat("INTERFACE_"_s, property, '_', + cmSystemTools::UpperCase(configuration)); + } + + target->AppendProperty(fullprop, value, makefile->GetBacktrace()); +} + +void SetOptionalProperty(cmMakefile* /*makefile*/, cmTarget* target, + cm::string_view property, + cm::string_view configuration, + Json::Value const& value, std::string const& prefix) +{ + if (!value.isNull()) { + std::string fullprop; + if (configuration.empty()) { + fullprop = cmStrCat("IMPORTED_"_s, property); + } else { + fullprop = cmStrCat("IMPORTED_"_s, property, '_', + cmSystemTools::UpperCase(configuration)); + } + + target->SetProperty(fullprop, ResolvePrefix(value.asString(), prefix)); + } +} + +void AddCompileFeature(cmMakefile* makefile, cmTarget* target, + cm::string_view configuration, std::string const& value) +{ + auto reLanguageLevel = []() -> cmsys::RegularExpression { + static cmsys::RegularExpression re{ "^[Cc]([+][+])?([0-9][0-9])$" }; + return re; + }(); + + if (reLanguageLevel.find(value)) { + std::string::size_type const n = reLanguageLevel.end() - 2; + cm::string_view const featurePrefix = (n == 3 ? "cxx_std_"_s : "c_std_"_s); + if (configuration.empty()) { + AppendProperty(makefile, target, "COMPILE_FEATURES"_s, {}, + cmStrCat(featurePrefix, value.substr(n))); + } else { + std::string const& feature = + cmStrCat("$<$:"_s, featurePrefix, + value.substr(n), '>'); + AppendProperty(makefile, target, "COMPILE_FEATURES"_s, {}, feature); + } + } else if (cmStrCaseEq(value, "gnu"_s)) { + // Not implemented in CMake at this time + } else if (cmStrCaseEq(value, "threads"_s)) { + AppendProperty(makefile, target, "LINK_LIBRARIES"_s, configuration, + "Threads::Threads"); + } +} + +void AddLinkFeature(cmMakefile* makefile, cmTarget* target, + cm::string_view configuration, std::string const& value) +{ + if (cmStrCaseEq(value, "thread"_s)) { + AppendProperty(makefile, target, "LINK_LIBRARIES"_s, configuration, + "Threads::Threads"); + } +} + +void SetTargetProperties(cmMakefile* makefile, cmTarget* target, + Json::Value const& data, std::string const& package, + std::string const& prefix, + cm::string_view configuration) +{ + // Add compile and link features. + for (std::string const& def : ReadList(data, "compile_features")) { + AddCompileFeature(makefile, target, configuration, def); + } + + for (std::string const& def : ReadList(data, "link_features")) { + AddLinkFeature(makefile, target, configuration, def); + } + + // Add compile definitions. + for (std::string const& def : ReadList(data, "definitions")) { + AppendProperty(makefile, target, "COMPILE_DEFINITIONS"_s, configuration, + def); + } + + // Add include directories. + for (std::string const& inc : ReadList(data, "includes")) { + AppendProperty(makefile, target, "INCLUDE_DIRECTORIES"_s, configuration, + ResolvePrefix(inc, prefix)); + } + + // Add link name/location(s). + SetOptionalProperty(makefile, target, "LOCATION"_s, configuration, + data["location"], prefix); + + SetOptionalProperty(makefile, target, "IMPLIB"_s, configuration, + data["link_location"], prefix); + + SetOptionalProperty(makefile, target, "SONAME"_s, configuration, + data["link_name"], prefix); + + // Add link languages. + for (std::string const& lang : ReadList(data, "link_languages")) { + auto const li = Languages.find(cmSystemTools::LowerCase(lang)); + if (li != Languages.end()) { + AppendProperty(makefile, target, "LINK_LANGUAGES"_s, configuration, + li->second); + } + } + + // Add transitive dependencies. + for (std::string const& dep : ReadList(data, "requires")) { + AppendProperty(makefile, target, "LINK_LIBRARIES"_s, configuration, + NormalizeTargetName(dep, package)); + } + + for (std::string const& dep : ReadList(data, "link_requires")) { + std::string const& lib = + cmStrCat("$'); + AppendProperty(makefile, target, "LINK_LIBRARIES"_s, configuration, lib); + } +} + +cmTarget* AddLibraryComponent(cmMakefile* makefile, + cmStateEnums::TargetType type, + std::string const& name, Json::Value const& data, + std::vector const& configurations, + std::string const& package, + std::string const& prefix, + cm::string_view configuration) +{ + // Create the imported target. + cmTarget* const target = makefile->AddImportedTarget(name, type, false); + + // Set target properties. + SetTargetProperties(makefile, target, data, package, prefix, configuration); + if (configuration.empty()) { + Json::Value const& cfgData = data["configurations"]; + for (auto ci = cfgData.begin(), ce = cfgData.end(); ci != ce; ++ci) { + SetTargetProperties(makefile, target, *ci, package, prefix, IterKey(ci)); + } + } + + // Set default configurations. + if (!configurations.empty()) { + target->SetProperty("IMPORTED_CONFIGURATIONS", + cmJoin(configurations, ";"_s)); + } + + return target; +} + } // namespace std::unique_ptr cmPackageInfoReader::Read( - std::string const& path) + std::string const& path, cmPackageInfoReader const* parent) { // Read file and perform some basic validation: // - the input is valid JSON @@ -88,9 +384,25 @@ std::unique_ptr cmPackageInfoReader::Read( return nullptr; } + std::string prefix = (parent ? parent->Prefix : DeterminePrefix(path, data)); + if (prefix.empty()) { + return nullptr; + } + // Seems sane enough to hand back to the caller. std::unique_ptr reader{ new cmPackageInfoReader }; - reader->Data = data; + reader->Data = std::move(data); + reader->Prefix = std::move(prefix); + reader->Path = path; + + // Determine other information we need to know immediately, or (if this is + // a supplemental reader) copy from the parent. + if (parent) { + reader->ComponentTargets = parent->ComponentTargets; + reader->DefaultConfigurations = parent->DefaultConfigurations; + } else { + reader->DefaultConfigurations = ReadList(reader->Data, "configurations"); + } return reader; } @@ -144,3 +456,110 @@ std::vector cmPackageInfoReader::ParseVersion() const return result; } + +bool cmPackageInfoReader::ImportTargets(cmMakefile* makefile, + cmExecutionStatus& status) +{ + std::string const& package = this->GetName(); + + // Read components. + Json::Value const& components = this->Data["components"]; + + for (auto ci = components.begin(), ce = components.end(); ci != ce; ++ci) { + cm::string_view const& name = IterKey(ci); + std::string const& type = + cmSystemTools::LowerCase((*ci)["type"].asString()); + + // Get and validate full target name. + std::string const& fullName = cmStrCat(package, "::"_s, name); + { + std::string msg; + if (!makefile->EnforceUniqueName(fullName, msg)) { + status.SetError(msg); + return false; + } + } + + cmTarget* target = nullptr; + if (type == "symbolic"_s) { + // TODO + } else if (type == "dylib"_s) { + target = AddLibraryComponent(makefile, cmStateEnums::SHARED_LIBRARY, + fullName, *ci, this->DefaultConfigurations, + package, this->Prefix, {}); + } else if (type == "module"_s) { + target = AddLibraryComponent(makefile, cmStateEnums::MODULE_LIBRARY, + fullName, *ci, this->DefaultConfigurations, + package, this->Prefix, {}); + } else if (type == "archive"_s) { + target = AddLibraryComponent(makefile, cmStateEnums::STATIC_LIBRARY, + fullName, *ci, this->DefaultConfigurations, + package, this->Prefix, {}); + } else if (type == "interface"_s) { + target = AddLibraryComponent(makefile, cmStateEnums::INTERFACE_LIBRARY, + fullName, *ci, this->DefaultConfigurations, + package, this->Prefix, {}); + } else { + makefile->IssueMessage(MessageType::WARNING, + cmStrCat("component "_s, fullName, + " has unknown type "_s, type, + " and was not imported"_s)); + } + + if (target) { + this->ComponentTargets.emplace(std::string{ name }, target); + } + } + + // Read default components. + std::vector const& defaultComponents = + ReadList(this->Data, "default_components"); + if (!defaultComponents.empty()) { + std::string msg; + if (!makefile->EnforceUniqueName(package, msg)) { + status.SetError(msg); + return false; + } + + cmTarget* const target = makefile->AddImportedTarget( + package, cmStateEnums::INTERFACE_LIBRARY, false); + for (std::string const& name : defaultComponents) { + std::string const& fullName = cmStrCat(package, "::"_s, name); + AppendProperty(makefile, target, "LINK_LIBRARIES"_s, {}, fullName); + } + } + + return true; +} + +bool cmPackageInfoReader::ImportTargetConfigurations( + cmMakefile* makefile, cmExecutionStatus& status) const +{ + std::string const& configuration = this->Data["configuration"].asString(); + + if (configuration.empty()) { + makefile->IssueMessage(MessageType::WARNING, + cmStrCat("supplemental file "_s, this->Path, + " does not specify a configuration"_s)); + return true; + } + + std::string const& package = this->GetName(); + Json::Value const& components = this->Data["components"]; + + for (auto ci = components.begin(), ce = components.end(); ci != ce; ++ci) { + // Get component name and look up target. + cm::string_view const& name = IterKey(ci); + auto const& ti = this->ComponentTargets.find(std::string{ name }); + if (ti == this->ComponentTargets.end()) { + status.SetError(cmStrCat("component "_s, name, " was not found"_s)); + return false; + } + + // Read supplemental data for component. + SetTargetProperties(makefile, ti->second, *ci, package, this->Prefix, + configuration); + } + + return true; +} diff --git a/Source/cmPackageInfoReader.h b/Source/cmPackageInfoReader.h index d135387287..af917f346c 100644 --- a/Source/cmPackageInfoReader.h +++ b/Source/cmPackageInfoReader.h @@ -4,6 +4,7 @@ #include "cmConfigure.h" // IWYU pragma: keep +#include #include #include #include @@ -12,7 +13,9 @@ #include -// class cmExecutionStatus; +class cmExecutionStatus; +class cmMakefile; +class cmTarget; /** \class cmPackageInfoReader * \brief Read and parse CPS files. @@ -24,7 +27,8 @@ class cmPackageInfoReader { public: - static std::unique_ptr Read(std::string const& path); + static std::unique_ptr Read( + std::string const& path, cmPackageInfoReader const* parent = nullptr); std::string GetName() const; cm::optional GetVersion() const; @@ -34,9 +38,20 @@ public: /// version is specified. std::vector ParseVersion() const; + /// Create targets for components specified in the CPS file. + bool ImportTargets(cmMakefile* makefile, cmExecutionStatus& status); + + /// Add configuration-specific properties for targets. + bool ImportTargetConfigurations(cmMakefile* makefile, + cmExecutionStatus& status) const; + private: cmPackageInfoReader() = default; std::string Path; Json::Value Data; + std::string Prefix; + + std::map ComponentTargets; + std::vector DefaultConfigurations; }; diff --git a/Tests/FindPackageCpsTest/include/foo.h b/Tests/FindPackageCpsTest/include/foo.h new file mode 100644 index 0000000000..2392aee71d --- /dev/null +++ b/Tests/FindPackageCpsTest/include/foo.h @@ -0,0 +1 @@ +/* empty header file */