FILE_SET: Add VERIFY_HEADER_SETS target property

Fixes: #23338
This commit is contained in:
Kyle Edwards 2022-02-24 18:09:53 -05:00
parent fdbef2a2be
commit c798744f81
47 changed files with 478 additions and 0 deletions

View File

@ -380,6 +380,7 @@ Properties on Targets
/prop_tgt/UNITY_BUILD_CODE_BEFORE_INCLUDE
/prop_tgt/UNITY_BUILD_MODE
/prop_tgt/UNITY_BUILD_UNIQUE_ID
/prop_tgt/VERIFY_HEADER_SETS
/prop_tgt/VERSION
/prop_tgt/VISIBILITY_INLINES_HIDDEN
/prop_tgt/VS_CONFIGURATION_TYPE

View File

@ -503,6 +503,7 @@ Variables that Control the Build
/variable/CMAKE_UNITY_BUILD_BATCH_SIZE
/variable/CMAKE_UNITY_BUILD_UNIQUE_ID
/variable/CMAKE_USE_RELATIVE_PATHS
/variable/CMAKE_VERIFY_HEADER_SETS
/variable/CMAKE_VISIBILITY_INLINES_HIDDEN
/variable/CMAKE_VS_GLOBALS
/variable/CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD

View File

@ -0,0 +1,24 @@
VERIFY_HEADER_SETS
------------------
.. versionadded:: 3.24
Used to verify that all headers in a target's header sets can be included on
their own.
When this property is set to true, and the target is an object library, static
library, shared library, or executable with exports enabled, and the target
has one or more header sets, an object library target named
``<target_name>_verify_header_sets`` is created. This verification target has
one source file per header in the header sets. Each source file only includes
its associated header file. The verification target links against the original
target to get all of its usage requirements. The verification target has its
:prop_tgt:`EXCLUDE_FROM_ALL` and :prop_tgt:`DISABLE_PRECOMPILE_HEADERS`
properties set to true, and its :prop_tgt:`AUTOMOC`, :prop_tgt:`AUTORCC`,
:prop_tgt:`AUTOUIC`, and :prop_tgt:`UNITY_BUILD` properties set to false.
If the header's :prop_sf:`LANGUAGE` property is set, the value of that property
is used to determine the language with which to compile the header file.
Otherwise, if the target has any C++ sources, the header is compiled as C++.
Otherwise, if the target has any C sources, the header is compiled as C.
Otherwise, the header file is not compiled.

View File

@ -0,0 +1,7 @@
verify-header-sets
------------------
* A new :prop_tgt:`VERIFY_HEADER_SETS` target property was added, which can be
used to verify that all headers in header sets can be used on their own.
* A new :variable:`CMAKE_VERIFY_HEADER_SETS` variable was added, which is used
to initialize the :prop_tgt:`VERIFY_HEADER_SETS` target property.

View File

@ -0,0 +1,17 @@
CMAKE_VERIFY_HEADER_SETS
------------------------
.. versionadded:: 3.24
This variable is used to initialize the :prop_tgt:`VERIFY_HEADER_SETS`
property of targets when they are created. Setting it to true
enables header set verification.
Projects should not set this variable, it is intended as a developer
control to be set on the :manual:`cmake(1)` command line or other
equivalent methods. The developer must have the ability to enable or
disable header set verification according to the capabilities of their own
machine and compiler.
By default, this variable is not set, which will result in header set
verification being disabled.

View File

@ -8522,3 +8522,176 @@ cmGeneratorTarget::ManagedType cmGeneratorTarget::GetManagedType(
// has to be set manually for C# targets.
return this->IsCSharpOnly() ? ManagedType::Managed : ManagedType::Native;
}
bool cmGeneratorTarget::AddHeaderSetVerification()
{
if (!this->GetPropertyAsBool("VERIFY_HEADER_SETS")) {
return true;
}
if (this->GetType() != cmStateEnums::STATIC_LIBRARY &&
this->GetType() != cmStateEnums::SHARED_LIBRARY &&
this->GetType() != cmStateEnums::UNKNOWN_LIBRARY &&
this->GetType() != cmStateEnums::OBJECT_LIBRARY &&
this->GetType() != cmStateEnums::INTERFACE_LIBRARY &&
!this->IsExecutableWithExports()) {
return true;
}
cmTarget* verifyTarget = nullptr;
auto interfaceFileSetEntries = this->Target->GetInterfaceHeaderSetsEntries();
std::set<cmFileSet*> fileSets;
auto const addFileSets = [&fileSets, this](const cmBTStringRange& entries) {
for (auto const& entry : entries) {
for (auto const& name : cmExpandedList(entry.Value)) {
fileSets.insert(this->Target->GetFileSet(name));
}
}
};
addFileSets(interfaceFileSetEntries);
cm::optional<std::set<std::string>> languages;
for (auto* fileSet : fileSets) {
auto dirCges = fileSet->CompileDirectoryEntries();
auto fileCges = fileSet->CompileFileEntries();
static auto const contextSensitive =
[](const std::unique_ptr<cmCompiledGeneratorExpression>& cge) {
return cge->GetHadContextSensitiveCondition();
};
bool dirCgesContextSensitive = false;
bool fileCgesContextSensitive = false;
std::vector<std::string> dirs;
std::map<std::string, std::vector<std::string>> filesPerDir;
bool first = true;
for (auto const& config : this->Makefile->GetGeneratorConfigs(
cmMakefile::GeneratorConfigQuery::IncludeEmptyConfig)) {
if (first || dirCgesContextSensitive) {
dirs = fileSet->EvaluateDirectoryEntries(dirCges, this->LocalGenerator,
config, this);
dirCgesContextSensitive =
std::any_of(dirCges.begin(), dirCges.end(), contextSensitive);
}
if (first || fileCgesContextSensitive) {
filesPerDir.clear();
for (auto const& fileCge : fileCges) {
fileSet->EvaluateFileEntry(dirs, filesPerDir, fileCge,
this->LocalGenerator, config, this);
if (fileCge->GetHadContextSensitiveCondition()) {
fileCgesContextSensitive = true;
}
}
}
for (auto const& files : filesPerDir) {
for (auto const& file : files.second) {
std::string filename = this->GenerateHeaderSetVerificationFile(
*this->Makefile->GetOrCreateSource(file), files.first, languages);
if (filename.empty()) {
continue;
}
if (!verifyTarget) {
{
cmMakefile::PolicyPushPop polScope(this->Makefile);
this->Makefile->SetPolicy(cmPolicies::CMP0119, cmPolicies::NEW);
verifyTarget = this->Makefile->AddLibrary(
cmStrCat(this->GetName(), "_verify_header_sets"),
cmStateEnums::OBJECT_LIBRARY, {}, true);
}
verifyTarget->AddLinkLibrary(
*this->Makefile, this->GetName(),
cmTargetLinkLibraryType::GENERAL_LibraryType);
verifyTarget->SetProperty("AUTOMOC", "OFF");
verifyTarget->SetProperty("AUTORCC", "OFF");
verifyTarget->SetProperty("AUTOUIC", "OFF");
verifyTarget->SetProperty("DISABLE_PRECOMPILE_HEADERS", "ON");
verifyTarget->SetProperty("UNITY_BUILD", "OFF");
}
if (fileCgesContextSensitive) {
filename = cmStrCat("$<$<CONFIG:", config, ">:", filename, ">");
}
verifyTarget->AddSource(filename);
}
}
if (!dirCgesContextSensitive && !fileCgesContextSensitive) {
break;
}
first = false;
}
}
if (verifyTarget) {
this->LocalGenerator->AddGeneratorTarget(
cm::make_unique<cmGeneratorTarget>(verifyTarget, this->LocalGenerator));
}
return true;
}
std::string cmGeneratorTarget::GenerateHeaderSetVerificationFile(
cmSourceFile& source, const std::string& dir,
cm::optional<std::set<std::string>>& languages) const
{
std::string extension;
std::string language = source.GetOrDetermineLanguage();
if (language.empty()) {
if (!languages) {
languages.emplace();
for (auto const& tgtSource : this->GetAllConfigSources()) {
auto const& tgtSourceLanguage =
tgtSource.Source->GetOrDetermineLanguage();
if (tgtSourceLanguage == "CXX") {
languages->insert("CXX");
break; // C++ overrides everything else, so we don't need to keep
// checking.
}
if (tgtSourceLanguage == "C") {
languages->insert("C");
}
}
}
if (languages->count("CXX")) {
language = "CXX";
} else if (languages->count("C")) {
language = "C";
}
}
if (language == "C") {
extension = ".c";
} else if (language == "CXX") {
extension = ".cxx";
} else {
return "";
}
std::string headerFilename = dir;
if (!headerFilename.empty()) {
headerFilename += '/';
}
headerFilename += source.GetLocation().GetName();
auto filename = cmStrCat(this->LocalGenerator->GetCurrentBinaryDirectory(),
'/', this->GetName(), "_verify_header_sets/",
headerFilename, extension);
auto* verificationSource = this->Makefile->GetOrCreateSource(filename);
verificationSource->SetProperty("LANGUAGE", language);
cmSystemTools::MakeDirectory(cmSystemTools::GetFilenamePath(filename));
cmGeneratedFileStream fout(filename);
fout.SetCopyIfDifferent(true);
fout << "#include <" << headerFilename << ">\n";
fout.close();
return filename;
}

View File

@ -868,6 +868,11 @@ public:
std::vector<std::string> GetGeneratedISPCObjects(
std::string const& config) const;
bool AddHeaderSetVerification();
std::string GenerateHeaderSetVerificationFile(
cmSourceFile& source, const std::string& dir,
cm::optional<std::set<std::string>>& languages) const;
private:
void AddSourceCommon(const std::string& src, bool before = false);

View File

@ -1499,6 +1499,11 @@ bool cmGlobalGenerator::Compute()
return false;
}
// Iterate through all targets and add verification targets for header sets
if (!this->AddHeaderSetVerification()) {
return false;
}
// Iterate through all targets and set up AUTOMOC, AUTOUIC and AUTORCC
if (!this->QtAutoGen()) {
return false;
@ -1720,6 +1725,27 @@ bool cmGlobalGenerator::QtAutoGen()
#endif
}
bool cmGlobalGenerator::AddHeaderSetVerification()
{
for (auto const& gen : this->LocalGenerators) {
// Because AddHeaderSetVerification() adds generator targets, we need to
// cache the existing list of generator targets before starting.
std::vector<cmGeneratorTarget*> genTargets;
genTargets.reserve(gen->GetGeneratorTargets().size());
for (auto const& tgt : gen->GetGeneratorTargets()) {
genTargets.push_back(tgt.get());
}
for (auto* tgt : genTargets) {
if (!tgt->AddHeaderSetVerification()) {
return false;
}
}
}
return true;
}
bool cmGlobalGenerator::AddAutomaticSources()
{
for (const auto& lg : this->LocalGenerators) {

View File

@ -573,6 +573,8 @@ protected:
/// @return true on success
bool QtAutoGen();
bool AddHeaderSetVerification();
bool AddAutomaticSources();
std::string SelectMakeProgram(const std::string& makeProgram,

View File

@ -425,6 +425,7 @@ cmTarget::cmTarget(std::string const& name, cmStateEnums::TargetType type,
}
initProp("FOLDER");
initProp("VERIFY_HEADER_SETS");
if (this->GetGlobalGenerator()->IsXcode()) {
initProp("XCODE_GENERATE_SCHEME");

View File

@ -970,6 +970,8 @@ add_RunCMake_test(CMakePresetsTest
-DCMake_TEST_JSON_SCHEMA=${CMake_TEST_JSON_SCHEMA}
)
add_RunCMake_test(VerifyHeaderSets)
if(${CMAKE_GENERATOR} MATCHES "Make|Ninja")
add_RunCMake_test(TransformDepfile)
endif()

View File

@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.23)
project(${RunCMake_TEST} NONE)
include(${RunCMake_TEST}.cmake)

View File

@ -0,0 +1,42 @@
include(RunCMake)
function(run_cmake_build name target)
if(NOT BUILD_CONFIG)
set(BUILD_CONFIG Debug)
endif()
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${name}-build)
set(RunCMake_TEST_NO_CLEAN 1)
run_cmake_command(${name}-${target}-${BUILD_CONFIG}-build ${CMAKE_COMMAND} --build . --config ${BUILD_CONFIG} --target ${target})
endfunction()
set(RunCMake_TEST_OPTIONS -DCMAKE_VERIFY_HEADER_SETS=ON)
if(NOT RunCMake_GENERATOR_IS_MULTI_CONFIG)
list(APPEND RunCMake_TEST_OPTIONS -DCMAKE_BUILD_TYPE=Debug)
endif()
run_cmake(VerifyHeaderSets)
unset(RunCMake_TEST_OPTIONS)
run_cmake_build(VerifyHeaderSets static_verify_header_sets)
run_cmake_build(VerifyHeaderSets shared_verify_header_sets)
run_cmake_build(VerifyHeaderSets object_verify_header_sets)
run_cmake_build(VerifyHeaderSets interface_verify_header_sets)
run_cmake_build(VerifyHeaderSets exe_verify_header_sets)
run_cmake_build(VerifyHeaderSets export_exe_verify_header_sets)
run_cmake_build(VerifyHeaderSets none_verify_header_sets)
run_cmake_build(VerifyHeaderSets property_off_verify_header_sets)
run_cmake_build(VerifyHeaderSets private_verify_header_sets)
run_cmake_build(VerifyHeaderSets a_h_verify_header_sets)
run_cmake_build(VerifyHeaderSets dir_c_h_verify_header_sets)
run_cmake_build(VerifyHeaderSets dir_cxx_h_verify_header_sets)
if(NOT RunCMake_GENERATOR STREQUAL "Xcode")
run_cmake_build(VerifyHeaderSets config_verify_header_sets)
if(RunCMake_GENERATOR_IS_MULTI_CONFIG)
set(BUILD_CONFIG Release)
run_cmake_build(VerifyHeaderSets config_verify_header_sets)
unset(BUILD_CONFIG)
endif()
endif()
run_cmake_build(VerifyHeaderSets lang_test_c_verify_header_sets)
run_cmake_build(VerifyHeaderSets lang_test_cxx_verify_header_sets)

View File

@ -0,0 +1,33 @@
function(check_file target filename)
set(full_filename "${RunCMake_TEST_BINARY_DIR}/${target}_verify_header_sets/${filename}")
if(NOT EXISTS "${full_filename}")
string(APPEND RunCMake_TEST_FAILED "File ${full_filename} should exist but does not\n")
set(RunCMake_TEST_FAILED "${RunCMake_TEST_FAILED}" PARENT_SCOPE)
return()
endif()
if(filename MATCHES "^(.*)(\\.[a-z]+)$")
set(header_filename "${CMAKE_MATCH_1}")
endif()
set(expected_contents "#include <${header_filename}>\n")
file(READ "${full_filename}" actual_contents)
if(NOT actual_contents STREQUAL expected_contents)
string(REPLACE "\n" "\n " expected_contents_formatted "${expected_contents}")
string(REPLACE "\n" "\n " actual_contents_formatted "${actual_contents}")
string(APPEND RunCMake_TEST_FAILED "Expected contents of ${full_filename}:\n ${expected_contents_formatted}\nActual contents:\n ${actual_contents_formatted}\n")
set(RunCMake_TEST_FAILED "${RunCMake_TEST_FAILED}" PARENT_SCOPE)
return()
endif()
endfunction()
check_file(static a.h.c)
check_file(static dir/c.h.c)
check_file(static dir/cxx.h.cxx)
if(NOT RunCMake_GENERATOR STREQUAL "Xcode")
check_file(config debug.h.c)
if(RunCMake_GENERATOR_IS_MULTI_CONFIG)
check_file(config release.h.c)
endif()
endif()

View File

@ -0,0 +1 @@
(Compiled in debug mode)?

View File

@ -0,0 +1 @@
(Compiled in debug mode)?

View File

@ -0,0 +1 @@
(Compiled in release mode)?

View File

@ -0,0 +1 @@
(Compiled in release mode)?

View File

@ -0,0 +1,60 @@
enable_language(C CXX)
set_property(SOURCE a.h PROPERTY LANGUAGE C)
set_property(SOURCE dir/c.h PROPERTY LANGUAGE C)
set_property(SOURCE dir/cxx.h PROPERTY LANGUAGE CXX)
add_library(static STATIC lib.c)
target_sources(static INTERFACE FILE_SET HEADERS FILES a.h dir/c.h dir/cxx.h)
add_library(shared SHARED lib.c)
target_sources(shared INTERFACE FILE_SET HEADERS FILES a.h dir/c.h dir/cxx.h)
add_library(object OBJECT lib.c)
target_sources(object INTERFACE FILE_SET HEADERS FILES a.h dir/c.h dir/cxx.h)
add_library(interface INTERFACE)
target_sources(interface INTERFACE FILE_SET HEADERS FILES a.h dir/c.h dir/cxx.h)
add_executable(exe main.c)
target_sources(exe INTERFACE FILE_SET HEADERS FILES a.h dir/c.h dir/cxx.h)
add_executable(export_exe main.c)
set_property(TARGET export_exe PROPERTY ENABLE_EXPORTS TRUE)
target_sources(export_exe INTERFACE FILE_SET HEADERS FILES a.h dir/c.h dir/cxx.h)
add_library(none STATIC lib.c)
add_library(property_off STATIC lib.c)
target_sources(property_off INTERFACE FILE_SET HEADERS FILES a.h dir/c.h dir/cxx.h)
set_property(TARGET property_off PROPERTY VERIFY_HEADER_SETS OFF)
add_library(private STATIC lib.c)
target_sources(private PRIVATE FILE_SET HEADERS FILES a.h dir/c.h dir/cxx.h)
add_library(a_h STATIC lib.c)
target_compile_definitions(a_h INTERFACE TEST_A_H)
target_sources(a_h INTERFACE FILE_SET HEADERS FILES a.h dir/c.h dir/cxx.h)
add_library(dir_c_h STATIC lib.c)
target_compile_definitions(dir_c_h INTERFACE TEST_DIR_C_H)
target_sources(dir_c_h INTERFACE FILE_SET HEADERS FILES a.h dir/c.h dir/cxx.h)
add_library(dir_cxx_h STATIC lib.c)
target_compile_definitions(dir_cxx_h INTERFACE TEST_DIR_CXX_H)
target_sources(dir_cxx_h INTERFACE FILE_SET HEADERS FILES a.h dir/c.h dir/cxx.h)
set_property(SOURCE debug.h PROPERTY LANGUAGE C)
set_property(SOURCE release.h PROPERTY LANGUAGE C)
if(NOT CMAKE_GENERATOR STREQUAL "Xcode")
add_library(config STATIC lib.c)
target_sources(config INTERFACE FILE_SET HEADERS FILES $<IF:$<CONFIG:Debug>,debug.h,release.h>)
endif()
add_library(lang_test_c STATIC lib.c)
target_sources(lang_test_c INTERFACE FILE_SET HEADERS FILES lang_test.h)
add_library(lang_test_cxx STATIC lib.c lib.cxx)
target_compile_definitions(lang_test_cxx INTERFACE EXPECT_CXX)
target_sources(lang_test_cxx INTERFACE FILE_SET HEADERS FILES lang_test.h)

View File

@ -0,0 +1,5 @@
#ifdef TEST_A_H
# error "TEST_A_H defined"
#endif
extern void a_h(void);

View File

@ -0,0 +1,3 @@
#error "Compiled in debug mode"
extern void debug_h(void);

View File

@ -0,0 +1,8 @@
#ifdef TEST_DIR_C_H
# error "TEST_DIR_C_H defined"
#endif
#ifdef __cplusplus
# error "__cplusplus defined"
#endif
extern void dir_c_h(void);

View File

@ -0,0 +1,8 @@
#ifdef TEST_DIR_CXX_H
# error "TEST_DIR_CXX_H defined"
#endif
#ifndef __cplusplus
# error "__cplusplus not defined"
#endif
extern void dir_cxx_h(void);

View File

@ -0,0 +1,8 @@
#if defined(__cplusplus) && !defined(EXPECT_CXX)
# error "__cplusplus defined but EXPECT_CXX not defined"
#endif
#if !defined(__cplusplus) && defined(EXPECT_CXX)
# error "__cplusplus not defined but EXPECT_CXX defined"
#endif
extern void lang_test_h(void);

View File

@ -0,0 +1,6 @@
#ifdef _WIN32
__declspec(dllexport)
#endif
void lib_c(void)
{
}

View File

@ -0,0 +1,6 @@
#ifdef _WIN32
__declspec(dllexport)
#endif
void lib_cxx(void)
{
}

View File

@ -0,0 +1,11 @@
int main(void)
{
return 0;
}
#ifdef _WIN32
__declspec(dllexport)
#endif
void main_c(void)
{
}

View File

@ -0,0 +1,3 @@
#error "Compiled in release mode"
extern void release_h(void);