From 3d52d70b84b114adf36a176c6f05aa0f42b1326f Mon Sep 17 00:00:00 2001 From: Matthew Woehlke Date: Fri, 26 Jul 2024 16:22:35 -0400 Subject: export: Add initial CPS support Add initial support for exporting (install only, for now) Common Package Specification (https://cps-org.github.io/cps/) format package descriptions. This has some limitations, such as not supporting generator expressions (as these cannot be portably exported), and only partially supporting transitive dependencies, but should be usable for at least some simple cases. (Actually, $ is theoretically supportable, but is not yet implemented.) This still needs tests; these will be added in the next commit. Other potential improvements include support for language-specific compile definitions and inferring some package properties from project properties. Additionally, there is no module support yet; this is partly pending on having a tool agnostic format for providing the necessary information. --- Help/command/install.rst | 59 ++++ Help/dev/experimental.rst | 20 ++ Source/CMakeLists.txt | 6 + Source/cmExperimental.cxx | 10 + Source/cmExperimental.h | 1 + Source/cmExportFileGenerator.cxx | 14 +- Source/cmExportFileGenerator.h | 5 + Source/cmExportInstallPackageInfoGenerator.cxx | 197 +++++++++++ Source/cmExportInstallPackageInfoGenerator.h | 66 ++++ Source/cmExportPackageInfoGenerator.cxx | 452 +++++++++++++++++++++++++ Source/cmExportPackageInfoGenerator.h | 116 +++++++ Source/cmInstallCommand.cxx | 139 ++++++++ Source/cmInstallPackageInfoExportGenerator.cxx | 36 ++ Source/cmInstallPackageInfoExportGenerator.h | 36 ++ 14 files changed, 1155 insertions(+), 2 deletions(-) create mode 100644 Source/cmExportInstallPackageInfoGenerator.cxx create mode 100644 Source/cmExportInstallPackageInfoGenerator.h create mode 100644 Source/cmExportPackageInfoGenerator.cxx create mode 100644 Source/cmExportPackageInfoGenerator.h create mode 100644 Source/cmInstallPackageInfoExportGenerator.cxx create mode 100644 Source/cmInstallPackageInfoExportGenerator.h diff --git a/Help/command/install.rst b/Help/command/install.rst index ffc1926..bbb70b8 100644 --- a/Help/command/install.rst +++ b/Help/command/install.rst @@ -19,6 +19,7 @@ Synopsis install(`SCRIPT`_ [...]) install(`CODE`_ [...]) install(`EXPORT`_ [...]) + install(`PACKAGE_INFO`_ [...]) install(`RUNTIME_DEPENDENCY_SET`_ [...]) Introduction @@ -906,6 +907,61 @@ Signatures ``mp_myexe`` as if the target were built in its own tree. .. signature:: + install(PACKAGE_INFO [...]) + + .. versionadded:: 3.31 + .. note:: + + Experimental. Gated by ``CMAKE_EXPERIMENTAL_EXPORT_PACKAGE_INFO``. + + Installs a |CPS|_ file exporting targets for dependent projects: + + .. code-block:: cmake + + install(PACKAGE_INFO EXPORT + [APPENDIX ] + [DESTINATION ] + [LOWER_CASE_FILE] + [VERSION + [COMPAT_VERSION ] + [VERSION_SCHEMA ]] + [DEFAULT_TARGETS ...] + [DEFAULT_CONFIGURATIONS ...] + [PERMISSIONS ...] + [CONFIGURATIONS ...] + [COMPONENT ] + [EXCLUDE_FROM_ALL]) + + The ``PACKAGE_INFO`` form generates and installs a |CPS| file which describes + installed targets such that they can be consumed by another project. + Target installations are associated with the export ```` + using the ``EXPORT`` option of the :command:`install(TARGETS)` signature + documented above. Unlike :command:`install(EXPORT)`, this information is not + expressed in CMake code, and can be consumed by tools other than CMake. When + imported into another CMake project, the imported targets will be prefixed + with ``::``. By default, the generated file will be called + ``[-].cps``. If ``LOWER_CASE_FILE`` is given, + the package name as it appears on disk (in both the file name and install + destination) will be first converted to lower case. + + If ``DESTINATION`` is not specified, a platform-specific default is used. + + If ``APPENDIX`` is specified, rather than generating a top level package + specification, the specified targets will be exported as an appendix to the + named package. Appendices may be used to separate less commonly used targets + (along with their external dependencies) from the rest of a package. This + enables consumers to ignore transitive dependencies for targets that they + don't use, and also allows a single logical "package" to be composed of + artifacts produced by multiple build trees. + + Appendices are not permitted to change basic package metadata; therefore, + none of ``VERSION``, ``COMPAT_VERSION``, ``VERSION_SCHEMA``, + ``DEFAULT_TARGETS`` or ``DEFAULT_CONFIGURATIONS`` may be specified in + combination with ``APPENDIX``. Additionally, it is strongly recommended that + use of ``LOWER_CASE_FILE`` should be consistent between the main package and + any appendices. + +.. signature:: install(RUNTIME_DEPENDENCY_SET [...]) .. versionadded:: 3.21 @@ -1097,3 +1153,6 @@ and by CPack. You can also invoke this script manually with This is an environment variable rather than a CMake variable. It allows you to change the installation prefix on UNIX systems. See :envvar:`DESTDIR` for details. + +.. _CPS: https://cps-org.github.io/cps/ +.. |CPS| replace:: Common Package Specification diff --git a/Help/dev/experimental.rst b/Help/dev/experimental.rst index 35ea34f..fb33112 100644 --- a/Help/dev/experimental.rst +++ b/Help/dev/experimental.rst @@ -39,6 +39,23 @@ When activated, this experimental feature provides the following: using the ``CMAKE_EXPORT_FIND_PACKAGE_NAME`` variable and/or ``EXPORT_FIND_PACKAGE_NAME`` target property. +Export |CPS| Package Information +================================ + +In order to activate support for this experimental feature, set + +* variable ``CMAKE_EXPERIMENTAL_EXPORT_PACKAGE_INFO`` to +* value ``b80be207-778e-46ba-8080-b23bba22639e``. + +This UUID may change in future versions of CMake. Be sure to use the value +documented here by the source tree of the version of CMake with which you are +experimenting. + +When activated, this experimental feature provides the following: + +* The experimental ``install(PACKAGE_INFO)`` command is available to export + package information in the |CPS|_ format. + C++ ``import std`` support ========================== @@ -60,3 +77,6 @@ When activated, this experimental feature provides the following: * Targets with the property set to a true value and at least ``cxx_std_23`` may use ``import std;`` in any scanned C++ source file. + +.. _CPS: https://cps-org.github.io/cps/ +.. |CPS| replace:: Common Package Specification diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 6841f1d..67360bc 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -217,6 +217,10 @@ add_library( cmExportInstallCMakeConfigGenerator.cxx cmExportInstallFileGenerator.h cmExportInstallFileGenerator.cxx + cmExportInstallPackageInfoGenerator.h + cmExportInstallPackageInfoGenerator.cxx + cmExportPackageInfoGenerator.h + cmExportPackageInfoGenerator.cxx cmExportTryCompileFileGenerator.h cmExportTryCompileFileGenerator.cxx cmExportSet.h @@ -336,6 +340,8 @@ add_library( cmInstallFilesGenerator.cxx cmInstallImportedRuntimeArtifactsGenerator.h cmInstallImportedRuntimeArtifactsGenerator.cxx + cmInstallPackageInfoExportGenerator.h + cmInstallPackageInfoExportGenerator.cxx cmInstallRuntimeDependencySet.h cmInstallRuntimeDependencySet.cxx cmInstallRuntimeDependencySetGenerator.h diff --git a/Source/cmExperimental.cxx b/Source/cmExperimental.cxx index a2e6e70..4504c07 100644 --- a/Source/cmExperimental.cxx +++ b/Source/cmExperimental.cxx @@ -46,6 +46,16 @@ cmExperimental::FeatureData LookupTable[] = { {}, cmExperimental::TryCompileCondition::Always, false }, + // ExportPackageInfo + { "ExportPackageInfo", + "b80be207-778e-46ba-8080-b23bba22639e", + "CMAKE_EXPERIMENTAL_EXPORT_PACKAGE_INFO", + "CMake's support for exporting package information in the Common Package " + "Specification format. It is meant only for experimentation and feedback " + "to CMake developers.", + {}, + cmExperimental::TryCompileCondition::Always, + false }, }; static_assert(sizeof(LookupTable) / sizeof(LookupTable[0]) == static_cast(cmExperimental::Feature::Sentinel), diff --git a/Source/cmExperimental.h b/Source/cmExperimental.h index 05764f9..46a9bc4 100644 --- a/Source/cmExperimental.h +++ b/Source/cmExperimental.h @@ -20,6 +20,7 @@ public: ExportPackageDependencies, WindowsKernelModeDriver, CxxImportStd, + ExportPackageInfo, Sentinel, }; diff --git a/Source/cmExportFileGenerator.cxx b/Source/cmExportFileGenerator.cxx index 24ed273..5e9461d 100644 --- a/Source/cmExportFileGenerator.cxx +++ b/Source/cmExportFileGenerator.cxx @@ -331,6 +331,14 @@ void cmExportFileGenerator::PopulateCustomTransitiveInterfaceProperties( } } +bool cmExportFileGenerator::NoteLinkedTarget( + cmGeneratorTarget const* /*target*/, std::string const& /*linkedName*/, + cmGeneratorTarget const* /*linkedTarget*/) +{ + // Default implementation does nothing; only needed by some generators. + return true; +} + bool cmExportFileGenerator::AddTargetNamespace(std::string& input, cmGeneratorTarget const* target, cmLocalGenerator const* lg) @@ -352,8 +360,9 @@ bool cmExportFileGenerator::AddTargetNamespace(std::string& input, if (tgt->IsImported()) { input = tgt->GetName(); - return true; + return this->NoteLinkedTarget(target, input, tgt); } + if (this->ExportedTargets.find(tgt) != this->ExportedTargets.end()) { input = this->Namespace + tgt->GetExportName(); } else { @@ -365,7 +374,8 @@ bool cmExportFileGenerator::AddTargetNamespace(std::string& input, input = tgt->GetName(); } } - return true; + + return this->NoteLinkedTarget(target, input, tgt); } void cmExportFileGenerator::ResolveTargetsInGeneratorExpressions( diff --git a/Source/cmExportFileGenerator.h b/Source/cmExportFileGenerator.h index f765493..b1c9ce3 100644 --- a/Source/cmExportFileGenerator.h +++ b/Source/cmExportFileGenerator.h @@ -95,6 +95,11 @@ protected: std::string const& config, std::string const& suffix) = 0; + /** Record a target referenced by an exported target. */ + virtual bool NoteLinkedTarget(cmGeneratorTarget const* target, + std::string const& linkedName, + cmGeneratorTarget const* linkedTarget); + /** Each subclass knows how to deal with a target that is missing from an * export set. */ virtual void HandleMissingTarget(std::string& link_libs, diff --git a/Source/cmExportInstallPackageInfoGenerator.cxx b/Source/cmExportInstallPackageInfoGenerator.cxx new file mode 100644 index 0000000..b9b715e --- /dev/null +++ b/Source/cmExportInstallPackageInfoGenerator.cxx @@ -0,0 +1,197 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#include "cmExportInstallPackageInfoGenerator.h" + +#include +#include +#include +#include + +#include + +#include "cmExportSet.h" +#include "cmGeneratorExpression.h" +#include "cmGeneratorTarget.h" +#include "cmInstallExportGenerator.h" +#include "cmLocalGenerator.h" +#include "cmMakefile.h" +#include "cmStateTypes.h" +#include "cmStringAlgorithms.h" +#include "cmSystemTools.h" +#include "cmTarget.h" +#include "cmTargetExport.h" + +cmExportInstallPackageInfoGenerator::cmExportInstallPackageInfoGenerator( + cmInstallExportGenerator* iegen, std::string packageName, + std::string version, std::string versionCompat, std::string versionSchema, + std::vector defaultTargets, + std::vector defaultConfigurations) + : cmExportPackageInfoGenerator( + std::move(packageName), std::move(version), std::move(versionCompat), + std::move(versionSchema), std::move(defaultTargets), + std::move(defaultConfigurations)) + , cmExportInstallFileGenerator(iegen) +{ +} + +std::string cmExportInstallPackageInfoGenerator::GetConfigImportFileGlob() + const +{ + std::string glob = cmStrCat(this->FileBase, "@*", this->FileExt); + return glob; +} + +std::string const& cmExportInstallPackageInfoGenerator::GetExportName() const +{ + return this->GetPackageName(); +} + +bool cmExportInstallPackageInfoGenerator::GenerateMainFile(std::ostream& os) +{ + std::vector allTargets; + { + auto visitor = [&](cmTargetExport const* te) { allTargets.push_back(te); }; + + if (!this->CollectExports(visitor)) { + return false; + } + } + + if (!this->CheckDefaultTargets()) { + return false; + } + + Json::Value root = this->GeneratePackageInfo(); + Json::Value& components = root["components"]; + + // Compute the relative import prefix for the file + std::string const& packagePath = this->GenerateImportPrefix(); + if (packagePath.empty()) { + return false; + } + root["cps_path"] = packagePath; + + bool requiresConfigFiles = false; + // Create all the imported targets. + for (cmTargetExport const* te : allTargets) { + cmGeneratorTarget* gt = te->Target; + cmStateEnums::TargetType targetType = this->GetExportTargetType(te); + + Json::Value* const component = + this->GenerateImportTarget(components, gt, targetType); + if (!component) { + return false; + } + + ImportPropertyMap properties; + if (!this->PopulateInterfaceProperties(te, properties)) { + return false; + } + this->PopulateInterfaceLinkLibrariesProperty( + gt, cmGeneratorExpression::InstallInterface, properties); + + if (targetType != cmStateEnums::INTERFACE_LIBRARY) { + requiresConfigFiles = true; + } + + // Set configuration-agnostic properties for component. + this->GenerateInterfaceProperties(*component, gt, properties); + } + + this->GeneratePackageRequires(root); + + // Write the primary packing information file. + this->WritePackageInfo(root, os); + + bool result = true; + + // Generate an import file for each configuration. + if (requiresConfigFiles) { + for (std::string const& c : this->Configurations) { + if (!this->GenerateImportFileConfig(c)) { + result = false; + } + } + } + + return result; +} + +void cmExportInstallPackageInfoGenerator::GenerateImportTargetsConfig( + std::ostream& os, std::string const& config, std::string const& suffix) +{ + Json::Value root; + root["name"] = this->GetPackageName(); + root["configuration"] = config; + + Json::Value& components = root["components"]; + + for (auto const& te : this->GetExportSet()->GetTargetExports()) { + // Collect import properties for this target. + if (this->GetExportTargetType(te.get()) == + cmStateEnums::INTERFACE_LIBRARY) { + continue; + } + + ImportPropertyMap properties; + std::set importedLocations; + + this->PopulateImportProperties(config, suffix, te.get(), properties, + importedLocations); + + this->GenerateInterfaceConfigProperties(components, te->Target, suffix, + properties); + } + + this->WritePackageInfo(root, os); +} + +std::string cmExportInstallPackageInfoGenerator::GenerateImportPrefix() const +{ + std::string expDest = this->IEGen->GetDestination(); + if (cmSystemTools::FileIsFullPath(expDest)) { + std::string const& installPrefix = + this->IEGen->GetLocalGenerator()->GetMakefile()->GetSafeDefinition( + "CMAKE_INSTALL_PREFIX"); + if (cmHasPrefix(expDest, installPrefix)) { + auto n = installPrefix.length(); + while (n < expDest.length() && expDest[n] == '/') { + ++n; + } + expDest = expDest.substr(n); + } else { + this->ReportError( + cmStrCat("install(PACKAGE_INFO \"", this->GetExportName(), + "\" ...) specifies DESTINATION \"", expDest, + "\" which is not a subdirectory of the install prefix.")); + return {}; + } + } + + if (expDest.empty()) { + return this->GetInstallPrefix(); + } + return cmStrCat(this->GetImportPrefixWithSlash(), expDest); +} + +std::string cmExportInstallPackageInfoGenerator::InstallNameDir( + cmGeneratorTarget const* target, std::string const& config) +{ + std::string install_name_dir; + + cmMakefile* mf = target->Target->GetMakefile(); + if (mf->IsOn("CMAKE_PLATFORM_HAS_INSTALLNAME")) { + install_name_dir = + target->GetInstallNameDirForInstallTree(config, "@prefix@"); + } + + return install_name_dir; +} + +std::string cmExportInstallPackageInfoGenerator::GetCxxModulesDirectory() const +{ + // TODO: Implement a not-CMake-specific mechanism for providing module + // information. + // return IEGen->GetCxxModuleDirectory(); + return {}; +} diff --git a/Source/cmExportInstallPackageInfoGenerator.h b/Source/cmExportInstallPackageInfoGenerator.h new file mode 100644 index 0000000..5861b05 --- /dev/null +++ b/Source/cmExportInstallPackageInfoGenerator.h @@ -0,0 +1,66 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#pragma once + +#include "cmConfigure.h" // IWYU pragma: keep + +#include +#include +#include + +#include "cmExportInstallFileGenerator.h" +#include "cmExportPackageInfoGenerator.h" + +class cmGeneratorTarget; +class cmInstallExportGenerator; + +/** \class cmExportInstallPackageInfoGenerator + * \brief Generate files exporting targets from an install tree. + * + * cmExportInstallPackageInfoGenerator generates files exporting targets from + * an installation tree. The files are placed in a temporary location for + * installation by cmInstallExportGenerator. The file format is the Common + * Package Specification (https://cps-org.github.io/cps/). + * + * One main file is generated that describes the imported targets. Additional, + * per-configuration files describe target locations and settings for each + * configuration. + * + * This is used to implement the INSTALL(PACKAGE_INFO) command. + */ +class cmExportInstallPackageInfoGenerator + : public cmExportPackageInfoGenerator + , public cmExportInstallFileGenerator +{ +public: + /** Construct with the export installer that will install the + files. */ + cmExportInstallPackageInfoGenerator( + cmInstallExportGenerator* iegen, std::string packageName, + std::string version, std::string versionCompat, std::string versionSchema, + std::vector defaultTargets, + std::vector defaultConfigurations); + + /** Compute the globbing expression used to load per-config import + files from the main file. */ + std::string GetConfigImportFileGlob() const override; + +protected: + std::string const& GetExportName() const override; + + // Implement virtual methods from the superclass. + bool GenerateMainFile(std::ostream& os) override; + void GenerateImportTargetsConfig(std::ostream& os, std::string const& config, + std::string const& suffix) override; + + char GetConfigFileNameSeparator() const override { return '@'; } + + /** Generate the cps_path, which determines the import prefix. */ + std::string GenerateImportPrefix() const; + + std::string InstallNameDir(cmGeneratorTarget const* target, + std::string const& config) override; + + std::string GetCxxModulesDirectory() const override; + // TODO: Generate C++ module info in a not-CMake-specific format. +}; diff --git a/Source/cmExportPackageInfoGenerator.cxx b/Source/cmExportPackageInfoGenerator.cxx new file mode 100644 index 0000000..7625953 --- /dev/null +++ b/Source/cmExportPackageInfoGenerator.cxx @@ -0,0 +1,452 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#include "cmExportPackageInfoGenerator.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include "cmExportSet.h" +#include "cmFindPackageStack.h" +#include "cmGeneratorExpression.h" +#include "cmGeneratorTarget.h" +#include "cmList.h" +#include "cmMakefile.h" +#include "cmMessageType.h" +#include "cmStringAlgorithms.h" +#include "cmSystemTools.h" +#include "cmTarget.h" +#include "cmValue.h" + +constexpr char const* cmExportPackageInfoGenerator::CPS_VERSION_STR; + +cmExportPackageInfoGenerator::cmExportPackageInfoGenerator( + std::string packageName, std::string version, std::string versionCompat, + std::string versionSchema, std::vector defaultTargets, + std::vector defaultConfigurations) + : PackageName(std::move(packageName)) + , PackageVersion(std::move(version)) + , PackageVersionCompat(std::move(versionCompat)) + , PackageVersionSchema(std::move(versionSchema)) + , DefaultTargets(std::move(defaultTargets)) + , DefaultConfigurations(std::move(defaultConfigurations)) +{ +} + +cm::string_view cmExportPackageInfoGenerator::GetImportPrefixWithSlash() const +{ + return "@prefix@/"_s; +} + +bool cmExportPackageInfoGenerator::GenerateImportFile(std::ostream& os) +{ + return this->GenerateMainFile(os); +} + +void cmExportPackageInfoGenerator::WritePackageInfo( + Json::Value const& packageInfo, std::ostream& os) const +{ + Json::StreamWriterBuilder builder; + builder["indentation"] = " "; + builder["commentStyle"] = "None"; + std::unique_ptr const writer(builder.newStreamWriter()); + writer->write(packageInfo, &os); +} + +namespace { +template +void buildArray(Json::Value& object, std::string const& property, + T const& values) +{ + if (!values.empty()) { + Json::Value& array = object[property]; + for (auto const& item : values) { + array.append(item); + } + } +} +} + +bool cmExportPackageInfoGenerator::CheckDefaultTargets() const +{ + bool result = true; + std::set exportedTargetNames; + for (auto const* te : this->ExportedTargets) { + exportedTargetNames.emplace(te->GetExportName()); + } + + for (auto const& name : this->DefaultTargets) { + if (!cm::contains(exportedTargetNames, name)) { + this->ReportError( + cmStrCat("Package \"", this->GetPackageName(), + "\" specifies DEFAULT_TARGETS \"", name, + "\", which is not a target in the export set \"", + this->GetExportSet()->GetName(), "\".")); + result = false; + } + } + + return result; +} + +Json::Value cmExportPackageInfoGenerator::GeneratePackageInfo() const +{ + Json::Value package; + + package["name"] = this->GetPackageName(); + package["cps_version"] = this->CPS_VERSION_STR; + + if (!this->PackageVersion.empty()) { + package["version"] = this->PackageVersion; + if (!this->PackageVersion.empty()) { + package["compat_version"] = this->PackageVersionCompat; + } + if (!this->PackageVersion.empty()) { + package["version_schema"] = this->PackageVersionSchema; + } + } + + buildArray(package, "default_components", this->DefaultTargets); + buildArray(package, "configurations", this->DefaultConfigurations); + + // TODO: description, website, license + + return package; +} + +void cmExportPackageInfoGenerator::GeneratePackageRequires( + Json::Value& package) const +{ + if (!this->Requirements.empty()) { + Json::Value& requirements = package["requires"]; + for (auto const& requirement : this->Requirements) { + // TODO: version, hint + requirements[requirement] = Json::Value{}; + } + } +} + +Json::Value* cmExportPackageInfoGenerator::GenerateImportTarget( + Json::Value& components, cmGeneratorTarget const* target, + cmStateEnums::TargetType targetType) const +{ + auto const& name = target->GetExportName(); + if (name.empty()) { + return nullptr; + } + + Json::Value& component = components[name]; + Json::Value& type = component["type"]; + switch (targetType) { + case cmStateEnums::EXECUTABLE: + type = "executable"; + break; + case cmStateEnums::STATIC_LIBRARY: + type = "archive"; + break; + case cmStateEnums::SHARED_LIBRARY: + type = "dylib"; + break; + case cmStateEnums::MODULE_LIBRARY: + type = "module"; + break; + case cmStateEnums::INTERFACE_LIBRARY: + type = "interface"; + break; + default: + type = "unknown"; + break; + } + return &component; +} + +bool cmExportPackageInfoGenerator::GenerateInterfaceProperties( + Json::Value& component, cmGeneratorTarget const* target, + ImportPropertyMap const& properties) const +{ + bool result = true; + + this->GenerateInterfaceLinkProperties(result, component, target, properties); + + this->GenerateInterfaceCompileFeatures(result, component, target, + properties); + this->GenerateInterfaceCompileDefines(result, component, target, properties); + + this->GenerateInterfaceListProperty(result, component, target, + "compile_flags", "COMPILE_OPTIONS"_s, + properties); + this->GenerateInterfaceListProperty(result, component, target, "link_flags", + "LINK_OPTIONS"_s, properties); + this->GenerateInterfaceListProperty(result, component, target, + "link_directories", "LINK_DIRECTORIES"_s, + properties); + this->GenerateInterfaceListProperty(result, component, target, "includes", + "INCLUDE_DIRECTORIES"_s, properties); + + // TODO: description, license + + return result; +} + +namespace { +bool forbidGeneratorExpressions(std::string const& propertyName, + std::string const& propertyValue, + cmGeneratorTarget const* target) +{ + std::string const& evaluatedValue = cmGeneratorExpression::Preprocess( + propertyValue, cmGeneratorExpression::StripAllGeneratorExpressions); + if (evaluatedValue != propertyValue) { + target->Makefile->IssueMessage( + MessageType::FATAL_ERROR, + cmStrCat("Property \"", propertyName, "\" of target \"", + target->GetName(), + "\" contains a generator expression. This is not allowed.")); + return false; + } + return true; +} +} + +bool cmExportPackageInfoGenerator::NoteLinkedTarget( + cmGeneratorTarget const* target, std::string const& linkedName, + cmGeneratorTarget const* linkedTarget) +{ + if (cm::contains(this->ExportedTargets, linkedTarget)) { + // Target is internal to this package. + this->LinkTargets.emplace(linkedName, + cmStrCat(':', linkedTarget->GetExportName())); + return true; + } + + if (linkedTarget->IsImported()) { + // Target is imported from a found package. + auto pkgName = [linkedTarget]() -> std::string { + auto const& pkgStack = linkedTarget->Target->GetFindPackageStack(); + if (!pkgStack.Empty()) { + return pkgStack.Top().Name; + } + + return linkedTarget->Target->GetProperty("EXPORT_FIND_PACKAGE_NAME"); + }(); + + if (pkgName.empty()) { + target->Makefile->IssueMessage( + MessageType::FATAL_ERROR, + cmStrCat("Target \"", target->GetName(), + "\" references imported target \"", linkedName, + "\" which does not come from any known package.")); + return false; + } + + auto const& prefix = cmStrCat(pkgName, "::"); + if (!cmHasPrefix(linkedName, prefix)) { + target->Makefile->IssueMessage( + MessageType::FATAL_ERROR, + cmStrCat("Target \"", target->GetName(), "\" references target \"", + linkedName, "\", which comes from the \"", pkgName, + "\" package, but does not belong to the package's " + "canonical namespace. This is not allowed.")); + return false; + } + + // TODO: Record package version, hint. + this->Requirements.emplace(pkgName); + this->LinkTargets.emplace( + linkedName, cmStrCat(pkgName, ':', linkedName.substr(prefix.length()))); + return true; + } + + // Target belongs to another export from this build. + auto const& exportInfo = this->FindExportInfo(linkedTarget); + if (exportInfo.first.size() == 1) { + auto const& linkNamespace = exportInfo.second; + if (!cmHasSuffix(linkNamespace, "::")) { + target->Makefile->IssueMessage( + MessageType::FATAL_ERROR, + cmStrCat("Target \"", target->GetName(), "\" references target \"", + linkedName, + "\", which does not use the standard namespace separator. " + "This is not allowed.")); + return false; + } + + auto pkgName = + cm::string_view{ linkNamespace.data(), linkNamespace.size() - 2 }; + + if (pkgName == this->GetPackageName()) { + this->LinkTargets.emplace(linkedName, + cmStrCat(':', linkedTarget->GetExportName())); + } else { + this->Requirements.emplace(pkgName); + this->LinkTargets.emplace( + linkedName, cmStrCat(pkgName, ':', linkedTarget->GetExportName())); + } + return true; + } + + // cmExportFileGenerator::HandleMissingTarget should have complained about + // this already. (In fact, we probably shouldn't ever get here.) + return false; +} + +void cmExportPackageInfoGenerator::GenerateInterfaceLinkProperties( + bool& result, Json::Value& component, cmGeneratorTarget const* target, + ImportPropertyMap const& properties) const +{ + auto const& iter = properties.find("INTERFACE_LINK_LIBRARIES"); + if (iter == properties.end()) { + return; + } + + // TODO: Support $. + if (!forbidGeneratorExpressions(iter->first, iter->second, target)) { + result = false; + return; + } + + std::vector buildRequires; + // std::vector linkRequires; TODO + std::vector linkLibraries; + + for (auto const& name : cmList{ iter->second }) { + auto const& ti = this->LinkTargets.find(name); + if (ti != this->LinkTargets.end()) { + if (ti->second.empty()) { + result = false; + } else { + buildRequires.emplace_back(ti->second); + } + } else { + linkLibraries.emplace_back(name); + } + } + + buildArray(component, "requires", buildRequires); + // buildArray(component, "link_requires", linkRequires); TODO + buildArray(component, "link_libraries", linkLibraries); +} + +void cmExportPackageInfoGenerator::GenerateInterfaceCompileFeatures( + bool& result, Json::Value& component, cmGeneratorTarget const* target, + ImportPropertyMap const& properties) const +{ + auto const& iter = properties.find("INTERFACE_COMPILE_FEATURES"); + if (iter == properties.end()) { + return; + } + + if (!forbidGeneratorExpressions(iter->first, iter->second, target)) { + result = false; + return; + } + + std::set features; + for (auto const& value : cmList{ iter->second }) { + if (cmHasLiteralPrefix(value, "c_std_")) { + auto suffix = cm::string_view{ value }.substr(6, 2); + features.emplace(cmStrCat("cxx", suffix)); + } else if (cmHasLiteralPrefix(value, "cxx_std_")) { + auto suffix = cm::string_view{ value }.substr(8, 2); + features.emplace(cmStrCat("c++", suffix)); + } + } + + buildArray(component, "compile_features", features); +} + +void cmExportPackageInfoGenerator::GenerateInterfaceCompileDefines( + bool& result, Json::Value& component, cmGeneratorTarget const* target, + ImportPropertyMap const& properties) const +{ + auto const& iter = properties.find("INTERFACE_COMPILE_DEFINITIONS"); + if (iter == properties.end()) { + return; + } + + // TODO: Support language-specific defines. + if (!forbidGeneratorExpressions(iter->first, iter->second, target)) { + result = false; + return; + } + + Json::Value defines; + for (auto const& def : cmList{ iter->second }) { + auto const n = def.find('='); + if (n == std::string::npos) { + defines[def] = Json::Value{}; + } else { + defines[def.substr(0, n)] = def.substr(n + 1); + } + } + + if (!defines.empty()) { + component["compile_definitions"]["*"] = std::move(defines); + } +} + +void cmExportPackageInfoGenerator::GenerateInterfaceListProperty( + bool& result, Json::Value& component, cmGeneratorTarget const* target, + std::string const& outName, cm::string_view inName, + ImportPropertyMap const& properties) const +{ + auto const& prop = cmStrCat("INTERFACE_", inName); + auto const& iter = properties.find(prop); + if (iter == properties.end()) { + return; + } + + if (!forbidGeneratorExpressions(prop, iter->second, target)) { + result = false; + return; + } + + Json::Value& array = component[outName]; + for (auto const& value : cmList{ iter->second }) { + array.append(value); + } +} + +void cmExportPackageInfoGenerator::GenerateInterfaceConfigProperties( + Json::Value& components, cmGeneratorTarget const* target, + std::string const& suffix, ImportPropertyMap const& properties) const +{ + Json::Value component; + auto const suffixLength = suffix.length(); + + for (auto const& p : properties) { + if (!cmHasSuffix(p.first, suffix)) { + continue; + } + auto const n = p.first.length() - suffixLength - 9; + auto const prop = cm::string_view{ p.first }.substr(9, n); + + if (prop == "LOCATION") { + component["location"] = p.second; + } else if (prop == "IMPLIB") { + component["link_location"] = p.second; + } else if (prop == "LINK_INTERFACE_LANGUAGES") { + std::vector languages; + for (auto const& lang : cmList{ p.second }) { + auto ll = cmSystemTools::LowerCase(lang); + if (ll == "cxx") { + languages.emplace_back("cpp"); + } else { + languages.emplace_back(std::move(ll)); + } + } + buildArray(component, "link_languages", languages); + } + } + + if (!component.empty()) { + components[target->GetExportName()] = component; + } +} diff --git a/Source/cmExportPackageInfoGenerator.h b/Source/cmExportPackageInfoGenerator.h new file mode 100644 index 0000000..1fb1703 --- /dev/null +++ b/Source/cmExportPackageInfoGenerator.h @@ -0,0 +1,116 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#pragma once + +#include "cmConfigure.h" // IWYU pragma: keep + +#include +#include +#include +#include +#include + +#include + +#include "cmExportFileGenerator.h" +#include "cmStateTypes.h" + +class cmGeneratorTarget; +namespace Json { +class Value; +} + +/** \class cmExportPackageInfoGenerator + * \brief Generate Common Package Specification package information files + * exporting targets from a build or install tree. + * + * cmExportPackageInfoGenerator is the superclass for + * cmExportBuildPackageInfoGenerator and cmExportInstallPackageInfoGenerator. + * It contains common code generation routines for the two kinds of export + * implementations. + */ +class cmExportPackageInfoGenerator : virtual public cmExportFileGenerator +{ +public: + cmExportPackageInfoGenerator(std::string packageName, std::string version, + std::string versionCompat, + std::string versionSchema, + std::vector defaultTargets, + std::vector defaultConfigurations); + + using cmExportFileGenerator::GenerateImportFile; + +protected: + std::string const& GetPackageName() const { return this->PackageName; } + + void WritePackageInfo(Json::Value const& packageInfo, + std::ostream& os) const; + + // Methods to implement export file code generation. + bool GenerateImportFile(std::ostream& os) override; + + bool CheckDefaultTargets() const; + + Json::Value GeneratePackageInfo() const; + Json::Value* GenerateImportTarget(Json::Value& components, + cmGeneratorTarget const* target, + cmStateEnums::TargetType targetType) const; + + void GeneratePackageRequires(Json::Value& package) const; + + using ImportPropertyMap = std::map; + bool GenerateInterfaceProperties(Json::Value& component, + cmGeneratorTarget const* target, + ImportPropertyMap const& properties) const; + void GenerateInterfaceConfigProperties( + Json::Value& components, cmGeneratorTarget const* target, + std::string const& suffix, ImportPropertyMap const& properties) const; + + cm::string_view GetImportPrefixWithSlash() const override; + + std::string GetCxxModuleFile(std::string const& /*name*/) const override + { + // TODO + return {}; + } + + void GenerateCxxModuleConfigInformation(std::string const& /*name*/, + std::ostream& /*os*/) const override + { + // TODO + } + + bool NoteLinkedTarget(cmGeneratorTarget const* target, + std::string const& linkedName, + cmGeneratorTarget const* linkedTarget) override; + +private: + void GenerateInterfaceLinkProperties( + bool& result, Json::Value& component, cmGeneratorTarget const* target, + ImportPropertyMap const& properties) const; + + void GenerateInterfaceCompileFeatures( + bool& result, Json::Value& component, cmGeneratorTarget const* target, + ImportPropertyMap const& properties) const; + + void GenerateInterfaceCompileDefines( + bool& result, Json::Value& component, cmGeneratorTarget const* target, + ImportPropertyMap const& properties) const; + + void GenerateInterfaceListProperty( + bool& result, Json::Value& component, cmGeneratorTarget const* target, + std::string const& outName, cm::string_view inName, + ImportPropertyMap const& properties) const; + + std::string const PackageName; + std::string const PackageVersion; + std::string const PackageVersionCompat; + std::string const PackageVersionSchema; + std::vector DefaultTargets; + std::vector DefaultConfigurations; + + std::map LinkTargets; + std::set Requirements; + + static constexpr char const* CPS_VERSION_STR = "0.12.0"; +}; diff --git a/Source/cmInstallCommand.cxx b/Source/cmInstallCommand.cxx index 3160585..f4cc4c3 100644 --- a/Source/cmInstallCommand.cxx +++ b/Source/cmInstallCommand.cxx @@ -36,6 +36,7 @@ #include "cmInstallGenerator.h" #include "cmInstallGetRuntimeDependenciesGenerator.h" #include "cmInstallImportedRuntimeArtifactsGenerator.h" +#include "cmInstallPackageInfoExportGenerator.h" #include "cmInstallRuntimeDependencySet.h" #include "cmInstallRuntimeDependencySetGenerator.h" #include "cmInstallScriptGenerator.h" @@ -2162,6 +2163,143 @@ bool HandleExportMode(std::vector const& args, return true; } +bool HandlePackageInfoMode(std::vector const& args, + cmExecutionStatus& status) +{ +#ifndef CMAKE_BOOTSTRAP + if (!cmExperimental::HasSupportEnabled( + status.GetMakefile(), cmExperimental::Feature::ExportPackageInfo)) { + status.SetError("does not recognize sub-command PACKAGE_INFO"); + return false; + } + + Helper helper(status); + + // This is the PACKAGE_INFO mode. + cmInstallCommandArguments ica(helper.DefaultComponentName); + + ArgumentParser::NonEmpty pkg; + ArgumentParser::NonEmpty appendix; + ArgumentParser::NonEmpty exportName; + bool lowerCase = false; + ArgumentParser::NonEmpty version; + ArgumentParser::NonEmpty versionCompat; + ArgumentParser::NonEmpty versionSchema; + ArgumentParser::NonEmpty> defaultTargets; + ArgumentParser::NonEmpty> defaultConfigs; + ArgumentParser::NonEmpty cxxModulesDirectory; + + // TODO: Support DESTINATION. + ica.Bind("PACKAGE_INFO"_s, pkg); + ica.Bind("EXPORT"_s, exportName); + ica.Bind("APPENDIX"_s, appendix); + ica.Bind("LOWER_CASE_FILE"_s, lowerCase); + ica.Bind("VERSION"_s, version); + ica.Bind("COMPAT_VERSION"_s, versionCompat); + ica.Bind("VERSION_SCHEMA"_s, versionSchema); + ica.Bind("DEFAULT_TARGETS"_s, defaultTargets); + ica.Bind("DEFAULT_CONFIGURATIONS"_s, defaultConfigs); + // ica.Bind("CXX_MODULES_DIRECTORY"_s, cxxModulesDirectory); TODO? + + std::vector unknownArgs; + ica.Parse(args, &unknownArgs); + + if (!unknownArgs.empty()) { + // Unknown argument. + status.SetError( + cmStrCat(args[0], " given unknown argument \"", unknownArgs[0], "\".")); + return false; + } + + if (!ica.Finalize()) { + return false; + } + + if (exportName.empty()) { + status.SetError(cmStrCat(args[0], " missing EXPORT.")); + return false; + } + + if (version.empty()) { + if (!versionCompat.empty()) { + status.SetError("COMPAT_VERSION requires VERSION."); + return false; + } + if (!versionSchema.empty()) { + status.SetError("VERSION_SCHEMA requires VERSION."); + return false; + } + } else { + if (!appendix.empty()) { + status.SetError("APPENDIX and VERSION are mutually exclusive."); + return false; + } + } + if (!appendix.empty()) { + if (!defaultTargets.empty()) { + status.SetError("APPENDIX and DEFAULT_TARGETS are mutually exclusive."); + return false; + } + if (!defaultConfigs.empty()) { + status.SetError("APPENDIX and DEFAULT_CONFIGURATIONS " + "are mutually exclusive."); + return false; + } + } + + // Validate the package name. + if (!cmGeneratorExpression::IsValidTargetName(pkg) || + pkg.find(':') != std::string::npos) { + status.SetError( + cmStrCat(args[0], " given invalid package name \"", pkg, "\".")); + return false; + } + + // Construct the case-normalized package name and the file name. + std::string const pkgNameOnDisk = + (lowerCase ? cmSystemTools::LowerCase(pkg) : pkg); + std::string pkgFileName = [&]() -> std::string { + if (appendix.empty()) { + return cmStrCat(pkgNameOnDisk, ".cps"); + } + return cmStrCat(pkgNameOnDisk, '-', appendix, ".cps"); + }(); + + // Get or construct the destination path. + std::string dest = ica.GetDestination(); + if (dest.empty()) { + if (helper.Makefile->GetSafeDefinition("CMAKE_SYSTEM_NAME") == "Windows") { + dest = std::string{ "cps"_s }; + } else { + dest = cmStrCat(helper.GetLibraryDestination(nullptr), "/cps/", + pkgNameOnDisk); + } + } + + cmExportSet& exportSet = + helper.Makefile->GetGlobalGenerator()->GetExportSets()[exportName]; + + cmInstallGenerator::MessageLevel message = + cmInstallGenerator::SelectMessageLevel(helper.Makefile); + + // Create the export install generator. + helper.Makefile->AddInstallGenerator( + cm::make_unique( + &exportSet, dest, ica.GetPermissions(), ica.GetConfigurations(), + ica.GetComponent(), message, ica.GetExcludeFromAll(), + std::move(pkgFileName), std::move(pkg), std::move(version), + std::move(versionCompat), std::move(versionSchema), + std::move(defaultTargets), std::move(defaultConfigs), + std::move(cxxModulesDirectory), helper.Makefile->GetBacktrace())); + + return true; +#else + static_cast(args); + status.SetError("PACKAGE_INFO not supported in bootstrap cmake"); + return false; +#endif +} + bool HandleRuntimeDependencySetMode(std::vector const& args, cmExecutionStatus& status) { @@ -2525,6 +2663,7 @@ bool cmInstallCommand(std::vector const& args, { "DIRECTORY"_s, HandleDirectoryMode }, { "EXPORT"_s, HandleExportMode }, { "EXPORT_ANDROID_MK"_s, HandleExportAndroidMKMode }, + { "PACKAGE_INFO"_s, HandlePackageInfoMode }, { "RUNTIME_DEPENDENCY_SET"_s, HandleRuntimeDependencySetMode }, }; diff --git a/Source/cmInstallPackageInfoExportGenerator.cxx b/Source/cmInstallPackageInfoExportGenerator.cxx new file mode 100644 index 0000000..4ff045b --- /dev/null +++ b/Source/cmInstallPackageInfoExportGenerator.cxx @@ -0,0 +1,36 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#include "cmInstallPackageInfoExportGenerator.h" + +#include + +#include + +#include "cmExportInstallFileGenerator.h" +#include "cmExportInstallPackageInfoGenerator.h" +#include "cmListFileCache.h" + +class cmExportSet; + +cmInstallPackageInfoExportGenerator::cmInstallPackageInfoExportGenerator( + cmExportSet* exportSet, std::string destination, std::string filePermissions, + std::vector const& configurations, std::string component, + MessageLevel message, bool excludeFromAll, std::string filename, + std::string packageName, std::string version, std::string versionCompat, + std::string versionSchema, std::vector defaultTargets, + std::vector defaultConfigurations, + std::string cxxModulesDirectory, cmListFileBacktrace backtrace) + : cmInstallExportGenerator( + exportSet, std::move(destination), std::move(filePermissions), + configurations, std::move(component), message, excludeFromAll, + std::move(filename), packageName + "::", std::move(cxxModulesDirectory), + std::move(backtrace)) +{ + this->EFGen = cm::make_unique( + this, std::move(packageName), std::move(version), std::move(versionCompat), + std::move(versionSchema), std::move(defaultTargets), + std::move(defaultConfigurations)); +} + +cmInstallPackageInfoExportGenerator::~cmInstallPackageInfoExportGenerator() = + default; diff --git a/Source/cmInstallPackageInfoExportGenerator.h b/Source/cmInstallPackageInfoExportGenerator.h new file mode 100644 index 0000000..c79df84 --- /dev/null +++ b/Source/cmInstallPackageInfoExportGenerator.h @@ -0,0 +1,36 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#pragma once + +#include +#include + +#include "cmInstallExportGenerator.h" + +class cmExportSet; +class cmListFileBacktrace; + +/** \class cmInstallPackageInfoGenerator + * \brief Generate rules for creating CPS package info files. + */ +class cmInstallPackageInfoExportGenerator : public cmInstallExportGenerator +{ +public: + cmInstallPackageInfoExportGenerator( + cmExportSet* exportSet, std::string destination, + std::string filePermissions, + std::vector const& configurations, std::string component, + MessageLevel message, bool excludeFromAll, std::string filename, + std::string packageName, std::string version, std::string versionCompat, + std::string versionSchema, std::vector defaultTargets, + std::vector defaultConfigurations, + std::string cxxModulesDirectory, cmListFileBacktrace backtrace); + cmInstallPackageInfoExportGenerator( + cmInstallPackageInfoExportGenerator const&) = delete; + ~cmInstallPackageInfoExportGenerator() override; + + cmInstallPackageInfoExportGenerator& operator=( + cmInstallPackageInfoExportGenerator const&) = delete; + + char const* InstallSubcommand() const override { return "PACKAGE_INFO"; } +}; -- cgit v0.12