Files
cmake/Source/cmCxxModuleMetadata.cxx
T
Vito Gamberini 3101d70902 c++modules: Restore use of local target for import std
Restore use of a project-local `STATIC` library to provide the stdlib's
module initialization symbols, as CMake 4.2's experimental support did.

In commit fa10dc6c22 (Experimental/CXXModules: Implement EcoStd Module
Metadata parser, 2025-10-21, v4.3.0-rc1~483^2) we switched to an
imported target to provide the BMI under the assumption that stdlib
implementations provide the symbols themselves, but they don't yet.

While refactoring the module metadata parser's properties logic, also
fix a relative path issue for local arguments.

Fixes: #27426, #27626, #27635
2026-03-04 16:40:37 -05:00

599 lines
16 KiB
C++

/* 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 "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('/', 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<cm::string_view> 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);
for (auto const& module : meta.Modules) {
std::string sourcePath = props.NormalizePath(module.SourcePath);
props.Sources.insert(sourcePath);
// Module metadata files can reference files in different roots,
// just use the immediate parent directory as a base directory
props.BaseDirs.insert(cmSystemTools::GetFilenamePath(sourcePath));
if (module.LocalArguments) {
for (auto const& incDir : module.LocalArguments->IncludeDirectories) {
props.AllIncludeDirectories.emplace(incDir);
}
for (auto const& sysIncDir :
module.LocalArguments->SystemIncludeDirectories) {
props.AllIncludeDirectories.emplace(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("CXX_MODULES", "CXX_MODULES",
cmFileSetVisibility::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);
}
}