| /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying | |
| file LICENSE.rst or https://cmake.org/licensing for details. */ | |
| #include "cmCxxModuleMetadata.h" | |
| #include <algorithm> | |
| #include <set> | |
| #include <string> | |
| #include <utility> | |
| #include <cmext/string_view> | |
| #include <cm3p/json/value.h> | |
| #include <cm3p/json/writer.h> | |
| #include "cmsys/FStream.hxx" | |
| #include "cmFileSet.h" | |
| #include "cmFileSetMetadata.h" | |
| #include "cmGeneratedFileStream.h" | |
| #include "cmJSONState.h" | |
| #include "cmListFileCache.h" | |
| #include "cmStringAlgorithms.h" | |
| #include "cmSystemTools.h" | |
| #include "cmTarget.h" | |
| namespace { | |
| bool JsonIsStringArray(Json::Value const& v) | |
| { | |
| return v.isArray() && | |
| std::all_of(v.begin(), v.end(), | |
| [](Json::Value const& it) { return it.isString(); }); | |
| } | |
| bool ParsePreprocessorDefine(Json::Value& dval, | |
| cmCxxModuleMetadata::PreprocessorDefineData& out, | |
| cmJSONState* state) | |
| { | |
| if (!dval.isObject()) { | |
| state->AddErrorAtValue("each entry in 'definitions' must be an object", | |
| &dval); | |
| return false; | |
| } | |
| if (!dval.isMember("name") || !dval["name"].isString() || | |
| dval["name"].asString().empty()) { | |
| state->AddErrorAtValue( | |
| "preprocessor definition requires a non-empty 'name'", &dval["name"]); | |
| return false; | |
| } | |
| out.Name = dval["name"].asString(); | |
| if (dval.isMember("value")) { | |
| if (dval["value"].isString()) { | |
| out.Value = dval["value"].asString(); | |
| } else if (!dval["value"].isNull()) { | |
| state->AddErrorAtValue( | |
| "'value' in preprocessor definition must be string or null", | |
| &dval["value"]); | |
| return false; | |
| } | |
| } | |
| if (dval.isMember("undef")) { | |
| if (!dval["undef"].isBool()) { | |
| state->AddErrorAtValue( | |
| "'undef' in preprocessor definition must be boolean", &dval["undef"]); | |
| return false; | |
| } | |
| out.Undef = dval["undef"].asBool(); | |
| } | |
| return true; | |
| } | |
| bool ParseCMakeLocalArgumentsVendor( | |
| Json::Value& cmlav, cmCxxModuleMetadata::LocalArgumentsData& out, | |
| cmJSONState* state) | |
| { | |
| if (!cmlav.isObject()) { | |
| state->AddErrorAtValue("'vendor' must be an object", &cmlav); | |
| return false; | |
| } | |
| if (cmlav.isMember("compile-options")) { | |
| if (!JsonIsStringArray(cmlav["compile-options"])) { | |
| state->AddErrorAtValue("'compile-options' must be an array of strings", | |
| &cmlav["compile-options"]); | |
| return false; | |
| } | |
| for (auto const& s : cmlav["compile-options"]) { | |
| out.CompileOptions.push_back(s.asString()); | |
| } | |
| } | |
| if (cmlav.isMember("compile-features")) { | |
| if (!JsonIsStringArray(cmlav["compile-features"])) { | |
| state->AddErrorAtValue("'compile-features' must be an array of strings", | |
| &cmlav["compile-features"]); | |
| return false; | |
| } | |
| for (auto const& s : cmlav["compile-features"]) { | |
| out.CompileFeatures.push_back(s.asString()); | |
| } | |
| } | |
| return true; | |
| } | |
| bool ParseLocalArguments(Json::Value& lav, | |
| cmCxxModuleMetadata::LocalArgumentsData& out, | |
| cmJSONState* state) | |
| { | |
| if (!lav.isObject()) { | |
| state->AddErrorAtValue("'local-arguments' must be an object", &lav); | |
| return false; | |
| } | |
| if (lav.isMember("include-directories")) { | |
| if (!JsonIsStringArray(lav["include-directories"])) { | |
| state->AddErrorAtValue( | |
| "'include-directories' must be an array of strings", | |
| &lav["include-directories"]); | |
| return false; | |
| } | |
| for (auto const& s : lav["include-directories"]) { | |
| out.IncludeDirectories.push_back(s.asString()); | |
| } | |
| } | |
| if (lav.isMember("system-include-directories")) { | |
| if (!JsonIsStringArray(lav["system-include-directories"])) { | |
| state->AddErrorAtValue( | |
| "'system-include-directories' must be an array of strings", | |
| &lav["system-include-directories"]); | |
| return false; | |
| } | |
| for (auto const& s : lav["system-include-directories"]) { | |
| out.SystemIncludeDirectories.push_back(s.asString()); | |
| } | |
| } | |
| if (lav.isMember("definitions")) { | |
| if (!lav["definitions"].isArray()) { | |
| state->AddErrorAtValue("'definitions' must be an array", | |
| &lav["definitions"]); | |
| return false; | |
| } | |
| for (Json::Value& dval : lav["definitions"]) { | |
| out.Definitions.emplace_back(); | |
| if (!ParsePreprocessorDefine(dval, out.Definitions.back(), state)) { | |
| return false; | |
| } | |
| } | |
| } | |
| if (lav.isMember("vendor")) { | |
| if (!ParseCMakeLocalArgumentsVendor(lav["vendor"], out, state)) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| bool ParseModule(Json::Value& mval, cmCxxModuleMetadata::ModuleData& mod, | |
| cmJSONState* state) | |
| { | |
| if (!mval.isObject()) { | |
| state->AddErrorAtValue("each entry in 'modules' must be an object", &mval); | |
| return false; | |
| } | |
| if (!mval.isMember("logical-name") || !mval["logical-name"].isString() || | |
| mval["logical-name"].asString().empty()) { | |
| state->AddErrorAtValue( | |
| "module entries require a non-empty 'logical-name' string", | |
| &mval["logical-name"]); | |
| return false; | |
| } | |
| mod.LogicalName = mval["logical-name"].asString(); | |
| if (!mval.isMember("source-path") || !mval["source-path"].isString() || | |
| mval["source-path"].asString().empty()) { | |
| state->AddErrorAtValue( | |
| "module entries require a non-empty 'source-path' string", | |
| &mval["source-path"]); | |
| return false; | |
| } | |
| mod.SourcePath = mval["source-path"].asString(); | |
| if (mval.isMember("is-interface")) { | |
| if (!mval["is-interface"].isBool()) { | |
| state->AddErrorAtValue("'is-interface' must be boolean", | |
| &mval["is-interface"]); | |
| return false; | |
| } | |
| mod.IsInterface = mval["is-interface"].asBool(); | |
| } else { | |
| mod.IsInterface = true; | |
| } | |
| if (mval.isMember("is-std-library")) { | |
| if (!mval["is-std-library"].isBool()) { | |
| state->AddErrorAtValue("'is-std-library' must be boolean", | |
| &mval["is-std-library"]); | |
| return false; | |
| } | |
| mod.IsStdLibrary = mval["is-std-library"].asBool(); | |
| } else { | |
| mod.IsStdLibrary = false; | |
| } | |
| if (mval.isMember("local-arguments")) { | |
| mod.LocalArguments.emplace(); | |
| if (!ParseLocalArguments(mval["local-arguments"], *mod.LocalArguments, | |
| state)) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| bool ParseRoot(Json::Value& root, cmCxxModuleMetadata& meta, | |
| cmJSONState* state) | |
| { | |
| if (!root.isMember("version") || !root["version"].isInt()) { | |
| state->AddErrorAtValue( | |
| "Top-level member 'version' is required and must be an integer", &root); | |
| return false; | |
| } | |
| meta.Version = root["version"].asInt(); | |
| if (root.isMember("revision")) { | |
| if (!root["revision"].isInt()) { | |
| state->AddErrorAtValue("'revision' must be an integer", | |
| &root["revision"]); | |
| return false; | |
| } | |
| meta.Revision = root["revision"].asInt(); | |
| } | |
| if (meta.Version != 1) { | |
| state->AddErrorAtValue(cmStrCat("Module manifest version number, '", | |
| meta.Version, '.', meta.Revision, | |
| "' is newer than max supported (1.1)"), | |
| &root); | |
| return false; | |
| } | |
| if (root.isMember("modules")) { | |
| if (!root["modules"].isArray()) { | |
| state->AddErrorAtValue("'modules' must be an array", &root["modules"]); | |
| return false; | |
| } | |
| for (Json::Value& mval : root["modules"]) { | |
| meta.Modules.emplace_back(); | |
| if (!ParseModule(mval, meta.Modules.back(), state)) { | |
| return false; | |
| } | |
| } | |
| } | |
| return true; | |
| } | |
| } // namespace | |
| cmCxxModuleMetadata::ParseResult cmCxxModuleMetadata::LoadFromFile( | |
| std::string const& path) | |
| { | |
| ParseResult res; | |
| Json::Value root; | |
| cmJSONState parseState(path, &root); | |
| if (!parseState.errors.empty()) { | |
| res.Error = parseState.GetErrorMessage(); | |
| return res; | |
| } | |
| cmCxxModuleMetadata meta; | |
| if (!ParseRoot(root, meta, &parseState)) { | |
| res.Error = parseState.GetErrorMessage(); | |
| return res; | |
| } | |
| meta.MetadataFilePath = path; | |
| res.Meta = std::move(meta); | |
| return res; | |
| } | |
| namespace { | |
| Json::Value SerializePreprocessorDefine( | |
| cmCxxModuleMetadata::PreprocessorDefineData const& d) | |
| { | |
| Json::Value dv(Json::objectValue); | |
| dv["name"] = d.Name; | |
| if (d.Value) { | |
| dv["value"] = *d.Value; | |
| } | |
| if (d.Undef) { | |
| dv["undef"] = d.Undef; | |
| } | |
| return dv; | |
| } | |
| Json::Value SerializeCMakeLocalArgumentsVendor( | |
| cmCxxModuleMetadata::LocalArgumentsData const& la) | |
| { | |
| Json::Value vend(Json::objectValue); | |
| if (!la.CompileOptions.empty()) { | |
| Json::Value& opts = vend["compile-options"] = Json::arrayValue; | |
| for (auto const& s : la.CompileOptions) { | |
| opts.append(s); | |
| } | |
| } | |
| if (!la.CompileFeatures.empty()) { | |
| Json::Value& feats = vend["compile-features"] = Json::arrayValue; | |
| for (auto const& s : la.CompileFeatures) { | |
| feats.append(s); | |
| } | |
| } | |
| return vend; | |
| } | |
| Json::Value SerializeLocalArguments( | |
| cmCxxModuleMetadata::LocalArgumentsData const& la) | |
| { | |
| Json::Value lav(Json::objectValue); | |
| if (!la.IncludeDirectories.empty()) { | |
| Json::Value& inc = lav["include-directories"] = Json::arrayValue; | |
| for (auto const& s : la.IncludeDirectories) { | |
| inc.append(s); | |
| } | |
| } | |
| if (!la.SystemIncludeDirectories.empty()) { | |
| Json::Value& sinc = lav["system-include-directories"] = Json::arrayValue; | |
| for (auto const& s : la.SystemIncludeDirectories) { | |
| sinc.append(s); | |
| } | |
| } | |
| if (!la.Definitions.empty()) { | |
| Json::Value& defs = lav["definitions"] = Json::arrayValue; | |
| for (auto const& d : la.Definitions) { | |
| defs.append(SerializePreprocessorDefine(d)); | |
| } | |
| } | |
| Json::Value vend = SerializeCMakeLocalArgumentsVendor(la); | |
| if (!vend.empty()) { | |
| Json::Value& cmvend = lav["vendor"] = Json::objectValue; | |
| cmvend["cmake"] = std::move(vend); | |
| } | |
| return lav; | |
| } | |
| Json::Value SerializeModule(std::string& manifestRoot, | |
| cmCxxModuleMetadata::ModuleData const& m) | |
| { | |
| Json::Value mv(Json::objectValue); | |
| mv["logical-name"] = m.LogicalName; | |
| if (cmSystemTools::FileIsFullPath(m.SourcePath)) { | |
| mv["source-path"] = m.SourcePath; | |
| } else { | |
| mv["source-path"] = cmSystemTools::ForceToRelativePath( | |
| manifestRoot, cmStrCat('/', m.SourcePath)); | |
| } | |
| mv["is-interface"] = m.IsInterface; | |
| mv["is-std-library"] = m.IsStdLibrary; | |
| if (m.LocalArguments) { | |
| mv["local-arguments"] = SerializeLocalArguments(*m.LocalArguments); | |
| } | |
| return mv; | |
| } | |
| } // namespace | |
| Json::Value cmCxxModuleMetadata::ToJsonValue(cmCxxModuleMetadata const& meta) | |
| { | |
| Json::Value root(Json::objectValue); | |
| root["version"] = meta.Version; | |
| root["revision"] = meta.Revision; | |
| Json::Value& modules = root["modules"] = Json::arrayValue; | |
| std::string manifestRoot = | |
| cmSystemTools::GetFilenamePath(meta.MetadataFilePath); | |
| if (!cmSystemTools::FileIsFullPath(meta.MetadataFilePath)) { | |
| manifestRoot = cmStrCat('/', std::move(manifestRoot)); | |
| } | |
| for (auto const& m : meta.Modules) { | |
| modules.append(SerializeModule(manifestRoot, m)); | |
| } | |
| return root; | |
| } | |
| cmCxxModuleMetadata::SaveResult cmCxxModuleMetadata::SaveToFile( | |
| std::string const& path, cmCxxModuleMetadata const& meta) | |
| { | |
| SaveResult st; | |
| cmGeneratedFileStream ofs(path); | |
| if (!ofs.is_open()) { | |
| st.Error = "Unable to open temp file for writing"; | |
| return st; | |
| } | |
| Json::StreamWriterBuilder wbuilder; | |
| wbuilder["indentation"] = " "; | |
| ofs << Json::writeString(wbuilder, ToJsonValue(meta)); | |
| ofs.Close(); | |
| if (!ofs.good()) { | |
| st.Error = cmStrCat("Write failed for file: "_s, path); | |
| return st; | |
| } | |
| st.Ok = true; | |
| return st; | |
| } | |
| namespace { | |
| struct MetaDataProperties | |
| { | |
| std::string MetadataDir; | |
| std::set<cm::string_view> AllCompileFeatures; | |
| std::set<cm::string_view> AllCompileOptions; | |
| std::set<std::string> AllIncludeDirectories; | |
| std::set<std::string> AllCompileDefinitions; | |
| std::set<std::string> BaseDirs; | |
| std::set<std::string> Sources; | |
| std::string NormalizePath(std::string const& in) const | |
| { | |
| std::string out = in; | |
| if (!cmSystemTools::FileIsFullPath(in)) { | |
| out = cmStrCat(MetadataDir, '/', in); | |
| } | |
| return cmSystemTools::CollapseFullPath(out); | |
| } | |
| }; | |
| MetaDataProperties CollectMetaProperties(cmCxxModuleMetadata const& meta) | |
| { | |
| MetaDataProperties props; | |
| props.MetadataDir = cmSystemTools::GetFilenamePath(meta.MetadataFilePath); | |
| #ifndef _WIN32 | |
| props.BaseDirs.insert("/"); | |
| #endif | |
| for (auto const& module : meta.Modules) { | |
| std::string sourcePath = props.NormalizePath(module.SourcePath); | |
| props.Sources.insert(sourcePath); | |
| #ifdef _WIN32 | |
| std::string root; | |
| cmSystemTools::SplitPathRootComponent(sourcePath, &root); | |
| props.BaseDirs.insert(root); | |
| #endif | |
| if (module.LocalArguments) { | |
| for (auto const& incDir : module.LocalArguments->IncludeDirectories) { | |
| props.AllIncludeDirectories.emplace(props.NormalizePath(incDir)); | |
| } | |
| for (auto const& sysIncDir : | |
| module.LocalArguments->SystemIncludeDirectories) { | |
| props.AllIncludeDirectories.emplace(props.NormalizePath(sysIncDir)); | |
| } | |
| for (auto const& opt : module.LocalArguments->CompileOptions) { | |
| props.AllCompileOptions.emplace(opt); | |
| } | |
| for (auto const& opt : module.LocalArguments->CompileFeatures) { | |
| props.AllCompileFeatures.emplace(opt); | |
| } | |
| for (auto const& def : module.LocalArguments->Definitions) { | |
| if (!def.Undef) { | |
| if (def.Value) { | |
| props.AllCompileDefinitions.emplace( | |
| cmStrCat(def.Name, "="_s, *def.Value)); | |
| } else { | |
| props.AllCompileDefinitions.emplace(def.Name); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| return props; | |
| } | |
| void PopulateFileSet(cmTarget& target, MetaDataProperties const& props) | |
| { | |
| auto fileSet = | |
| target.GetOrCreateFileSet(std::string{ cm::FileSetMetadata::CXX_MODULES }, | |
| std::string{ cm::FileSetMetadata::CXX_MODULES }, | |
| cm::FileSetMetadata::Visibility::Public); | |
| for (auto const& source : props.Sources) { | |
| fileSet.first->AddFileEntry(source); | |
| } | |
| for (auto const& baseDir : props.BaseDirs) { | |
| fileSet.first->AddDirectoryEntry(baseDir); | |
| } | |
| } | |
| void PopulateLocalTarget(cmTarget& target, MetaDataProperties const& props) | |
| { | |
| if (!props.AllIncludeDirectories.empty()) { | |
| target.AppendProperty("INCLUDE_DIRECTORIES", | |
| cmJoin(props.AllIncludeDirectories, ";")); | |
| } | |
| if (!props.AllCompileDefinitions.empty()) { | |
| target.AppendProperty("COMPILE_DEFINITIONS", | |
| cmJoin(props.AllCompileDefinitions, ";")); | |
| } | |
| if (!props.AllCompileOptions.empty()) { | |
| target.AppendProperty("COMPILE_OPTIONS", | |
| cmJoin(props.AllCompileOptions, ";")); | |
| } | |
| if (!props.AllCompileFeatures.empty()) { | |
| target.AppendProperty("COMPILE_FEATURES", | |
| cmJoin(props.AllCompileFeatures, ";")); | |
| } | |
| } | |
| void PopulateImportedTarget(cmTarget& target, MetaDataProperties const& props, | |
| cmCxxModuleMetadata const& meta, | |
| std::vector<std::string> const& configs) | |
| { | |
| if (!props.AllIncludeDirectories.empty()) { | |
| target.SetProperty("IMPORTED_CXX_MODULES_INCLUDE_DIRECTORIES", | |
| cmJoin(props.AllIncludeDirectories, ";")); | |
| } | |
| if (!props.AllCompileDefinitions.empty()) { | |
| target.SetProperty("IMPORTED_CXX_MODULES_COMPILE_DEFINITIONS", | |
| cmJoin(props.AllCompileDefinitions, ";")); | |
| } | |
| if (!props.AllCompileOptions.empty()) { | |
| target.SetProperty("IMPORTED_CXX_MODULES_COMPILE_OPTIONS", | |
| cmJoin(props.AllCompileOptions, ";")); | |
| } | |
| if (!props.AllCompileFeatures.empty()) { | |
| target.SetProperty("IMPORTED_CXX_MODULES_COMPILE_FEATURES", | |
| cmJoin(props.AllCompileFeatures, ";")); | |
| } | |
| for (auto const& config : configs) { | |
| std::vector<std::string> moduleList; | |
| for (auto const& module : meta.Modules) { | |
| if (module.IsInterface) { | |
| moduleList.push_back(cmStrCat(module.LogicalName, "="_s, | |
| props.NormalizePath(module.SourcePath))); | |
| } | |
| } | |
| if (!moduleList.empty()) { | |
| std::string upperConfig = cmSystemTools::UpperCase(config); | |
| std::string propertyName = | |
| cmStrCat("IMPORTED_CXX_MODULES_"_s, upperConfig); | |
| target.SetProperty(propertyName, cmJoin(moduleList, ";")); | |
| } | |
| } | |
| } | |
| } // namespace | |
| void cmCxxModuleMetadata::PopulateTarget( | |
| cmTarget& target, cmCxxModuleMetadata const& meta, | |
| std::vector<std::string> const& configs) | |
| { | |
| auto props = CollectMetaProperties(meta); | |
| PopulateFileSet(target, props); | |
| if (target.IsImported()) { | |
| PopulateImportedTarget(target, props, meta, configs); | |
| } else { | |
| PopulateLocalTarget(target, props); | |
| } | |
| } |