| /* 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 <initializer_list> |
| #include <limits> |
| #include <unordered_map> |
| #include <utility> |
| |
| #include <cmext/string_view> |
| |
| #include <cm3p/json/reader.h> |
| #include <cm3p/json/value.h> |
| #include <cm3p/json/version.h> |
| |
| #include "cmsys/FStream.hxx" |
| #include "cmsys/RegularExpression.hxx" |
| |
| #include "cmExecutionStatus.h" |
| #include "cmListFileCache.h" |
| #include "cmMakefile.h" |
| #include "cmMessageType.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<std::string, std::string> 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 |
| }; |
| |
| enum LanguageGlobOption |
| { |
| DisallowGlob, |
| AllowGlob, |
| }; |
| |
| cm::string_view MapLanguage(cm::string_view lang, |
| LanguageGlobOption glob = AllowGlob) |
| { |
| if (glob == AllowGlob && lang == "*"_s) { |
| return "*"_s; |
| } |
| auto const li = Languages.find(cmSystemTools::LowerCase(lang)); |
| if (li != Languages.end()) { |
| return li->second; |
| } |
| return {}; |
| } |
| |
| 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. |
| 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.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<std::string::size_type>(end - start) }; |
| } |
| |
| // Get list-of-strings value from object. |
| std::vector<std::string> ReadList(Json::Value const& arr) |
| { |
| std::vector<std::string> result; |
| |
| if (arr.isArray()) { |
| for (Json::Value const& val : arr) { |
| if (val.isString()) { |
| result.push_back(val.asString()); |
| } |
| } |
| } |
| |
| return result; |
| } |
| |
| std::vector<std::string> ReadList(Json::Value const& data, char const* key) |
| { |
| return ReadList(data[key]); |
| } |
| |
| 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; |
| } |
| |
| 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()); |
| } |
| |
| template <typename Transform> |
| void AppendLanguageProperties(cmMakefile* makefile, cmTarget* target, |
| cm::string_view property, |
| cm::string_view configuration, |
| Json::Value const& data, char const* key, |
| Transform transform) |
| { |
| Json::Value const& value = data[key]; |
| if (value.isArray()) { |
| for (std::string v : ReadList(value)) { |
| AppendProperty(makefile, target, property, configuration, |
| transform(std::move(v))); |
| } |
| } else if (value.isObject()) { |
| for (auto vi = value.begin(), ve = value.end(); vi != ve; ++vi) { |
| cm::string_view const originalLang = IterKey(vi); |
| cm::string_view const lang = MapLanguage(originalLang); |
| if (lang.empty()) { |
| makefile->IssueMessage(MessageType::WARNING, |
| cmStrCat(R"(ignoring unknown language ")"_s, |
| originalLang, R"(" in )"_s, key, |
| " for "_s, target->GetName())); |
| continue; |
| } |
| |
| if (lang == "*"_s) { |
| for (std::string v : ReadList(*vi)) { |
| AppendProperty(makefile, target, property, configuration, |
| transform(std::move(v))); |
| } |
| } else { |
| for (std::string v : ReadList(*vi)) { |
| v = cmStrCat("$<$<COMPILE_LANGUAGE:"_s, lang, ">:"_s, |
| transform(std::move(v)), '>'); |
| AppendProperty(makefile, target, property, configuration, v); |
| } |
| } |
| } |
| } |
| } |
| |
| 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("$<$<CONFIG:"_s, configuration, ">:"_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"); |
| } |
| } |
| |
| std::string BuildDefinition(std::string const& name, Json::Value const& value) |
| { |
| if (!value.isNull() && value.isConvertibleTo(Json::stringValue)) { |
| return cmStrCat(name, '=', value.asString()); |
| } |
| return name; |
| } |
| |
| void AddDefinition(cmMakefile* makefile, cmTarget* target, |
| cm::string_view configuration, |
| std::string const& definition) |
| { |
| AppendProperty(makefile, target, "COMPILE_DEFINITIONS"_s, configuration, |
| definition); |
| } |
| |
| using DefinitionLanguageMap = std::map<cm::string_view, Json::Value>; |
| using DefinitionsMap = std::map<std::string, DefinitionLanguageMap>; |
| |
| void AddDefinitions(cmMakefile* makefile, cmTarget* target, |
| cm::string_view configuration, |
| DefinitionsMap const& definitions) |
| { |
| for (auto const& di : definitions) { |
| auto const& g = di.second.find("*"_s); |
| if (g != di.second.end()) { |
| std::string const& def = BuildDefinition(di.first, g->second); |
| if (di.second.size() == 1) { |
| // Only the non-language-specific definition exists. |
| AddDefinition(makefile, target, configuration, def); |
| continue; |
| } |
| |
| // Create a genex to apply this definition to all languages except |
| // those that override it. |
| std::vector<cm::string_view> excludedLanguages; |
| for (auto const& li : di.second) { |
| if (li.first != "*"_s) { |
| excludedLanguages.emplace_back(li.first); |
| } |
| } |
| AddDefinition(makefile, target, configuration, |
| cmStrCat("$<$<NOT:$<COMPILE_LANGUAGE:"_s, |
| cmJoin(excludedLanguages, ","_s), ">>:"_s, def, |
| '>')); |
| } |
| |
| // Add language-specific definitions. |
| for (auto const& li : di.second) { |
| if (li.first != "*"_s) { |
| AddDefinition(makefile, target, configuration, |
| cmStrCat("$<$<COMPILE_LANGUAGE:"_s, li.first, ">:"_s, |
| BuildDefinition(di.first, li.second), '>')); |
| } |
| } |
| } |
| } |
| |
| } // namespace |
| |
| std::unique_ptr<cmPackageInfoReader> cmPackageInfoReader::Read( |
| std::string const& path, cmPackageInfoReader const* parent) |
| { |
| // 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; |
| } |
| |
| 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<cmPackageInfoReader> reader{ new cmPackageInfoReader }; |
| 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; |
| } |
| |
| 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; |
| } |
| |
| std::string cmPackageInfoReader::ResolvePath(std::string path) const |
| { |
| cmSystemTools::ConvertToUnixSlashes(path); |
| if (cmHasPrefix(path, "@prefix@"_s)) { |
| return cmStrCat(this->Prefix, path.substr(8)); |
| } |
| if (!cmSystemTools::FileIsFullPath(path)) { |
| return cmStrCat(cmSystemTools::GetFilenamePath(this->Path), '/', path); |
| } |
| return path; |
| } |
| |
| void cmPackageInfoReader::SetOptionalProperty(cmTarget* target, |
| cm::string_view property, |
| cm::string_view configuration, |
| Json::Value const& value) const |
| { |
| 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, this->ResolvePath(value.asString())); |
| } |
| } |
| |
| void cmPackageInfoReader::SetTargetProperties( |
| cmMakefile* makefile, cmTarget* target, Json::Value const& data, |
| std::string const& package, cm::string_view configuration) const |
| { |
| // 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. |
| Json::Value const& defs = data["definitions"]; |
| DefinitionsMap definitionsMap; |
| for (auto ldi = defs.begin(), lde = defs.end(); ldi != lde; ++ldi) { |
| cm::string_view const originalLang = IterKey(ldi); |
| cm::string_view const lang = MapLanguage(originalLang); |
| if (lang.empty()) { |
| makefile->IssueMessage( |
| MessageType::WARNING, |
| cmStrCat(R"(ignoring unknown language ")"_s, originalLang, |
| R"(" in definitions for )"_s, target->GetName())); |
| continue; |
| } |
| |
| for (auto di = ldi->begin(), de = ldi->end(); di != de; ++di) { |
| definitionsMap[di.name()].emplace(lang, *di); |
| } |
| } |
| AddDefinitions(makefile, target, configuration, definitionsMap); |
| |
| // Add include directories. |
| AppendLanguageProperties(makefile, target, "INCLUDE_DIRECTORIES"_s, |
| configuration, data, "includes", |
| [this](std::string p) -> std::string { |
| return this->ResolvePath(std::move(p)); |
| }); |
| |
| // Add link name/location(s). |
| this->SetOptionalProperty(target, "LOCATION"_s, configuration, |
| data["location"]); |
| |
| this->SetOptionalProperty(target, "IMPLIB"_s, configuration, |
| data["link_location"]); |
| |
| this->SetOptionalProperty(target, "SONAME"_s, configuration, |
| data["link_name"]); |
| |
| // Add link languages. |
| for (std::string const& originalLang : ReadList(data, "link_languages")) { |
| cm::string_view const lang = MapLanguage(originalLang, DisallowGlob); |
| if (!lang.empty()) { |
| AppendProperty(makefile, target, "LINK_LANGUAGES"_s, configuration, |
| std::string{ lang }); |
| } |
| } |
| |
| // 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("$<LINK_ONLY:"_s, NormalizeTargetName(dep, package), '>'); |
| AppendProperty(makefile, target, "LINK_LIBRARIES"_s, configuration, lib); |
| } |
| } |
| |
| cmTarget* cmPackageInfoReader::AddLibraryComponent( |
| cmMakefile* makefile, cmStateEnums::TargetType type, std::string const& name, |
| Json::Value const& data, std::string const& package) const |
| { |
| // Create the imported target. |
| cmTarget* const target = makefile->AddImportedTarget(name, type, false); |
| |
| // Set target properties. |
| this->SetTargetProperties(makefile, target, data, package, {}); |
| auto const& cfgData = data["configurations"]; |
| for (auto ci = cfgData.begin(), ce = cfgData.end(); ci != ce; ++ci) { |
| this->SetTargetProperties(makefile, target, *ci, package, IterKey(ci)); |
| } |
| |
| // Set default configurations. |
| if (!this->DefaultConfigurations.empty()) { |
| target->SetProperty("IMPORTED_CONFIGURATIONS", |
| cmJoin(this->DefaultConfigurations, ";"_s)); |
| } |
| |
| return target; |
| } |
| |
| 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 = this->AddLibraryComponent( |
| makefile, cmStateEnums::SHARED_LIBRARY, fullName, *ci, package); |
| } else if (type == "module"_s) { |
| target = this->AddLibraryComponent( |
| makefile, cmStateEnums::MODULE_LIBRARY, fullName, *ci, package); |
| } else if (type == "archive"_s) { |
| target = this->AddLibraryComponent( |
| makefile, cmStateEnums::STATIC_LIBRARY, fullName, *ci, package); |
| } else if (type == "interface"_s) { |
| target = this->AddLibraryComponent( |
| makefile, cmStateEnums::INTERFACE_LIBRARY, fullName, *ci, package); |
| } else { |
| makefile->IssueMessage(MessageType::WARNING, |
| cmStrCat(R"(component ")"_s, fullName, |
| R"(" has unknown type ")"_s, type, |
| R"(" and was not imported)"_s)); |
| } |
| |
| if (target) { |
| this->ComponentTargets.emplace(std::string{ name }, target); |
| } |
| } |
| |
| // Read default components. |
| std::vector<std::string> 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. |
| this->SetTargetProperties(makefile, ti->second, *ci, package, |
| configuration); |
| } |
| |
| return true; |
| } |