FILE_SET: enforce uniqueness of files

A file should be unique for a target if specified as part of a file set.

Issue: #27035
This commit is contained in:
Marc Chevrier
2026-03-06 16:54:29 +01:00
committed by Brad King
parent 29192751b1
commit 167e903c15
25 changed files with 175 additions and 52 deletions

View File

@@ -77,7 +77,13 @@ File Sets
Adds a file set to a target, or adds files to an existing file set. Targets
have zero or more named file sets. Each file set has a name, a type, a scope of
``INTERFACE``, ``PUBLIC``, or ``PRIVATE``, one or more base directories, and
files within those directories. The acceptable types include:
files within those directories.
.. versionchanged:: 4.4
A file may only belong to at most one file set in a target. See policy
:policy:`CMP0211`.
The acceptable types include:
``HEADERS``

View File

@@ -94,6 +94,14 @@ Supported Policies
The following policies are supported.
Policies Introduced by CMake 4.4
--------------------------------
.. toctree::
:maxdepth: 1
CMP0211: A file may belong to at most one file set in a target. </policy/CMP0211>
Policies Introduced by CMake 4.3
--------------------------------

22
Help/policy/CMP0211.rst Normal file
View File

@@ -0,0 +1,22 @@
CMP0211
-------
.. versionadded:: 4.4
A file may belong to at most one :ref:`file set <file sets>` in a target.
In CMake 4.3 and below, it was possible to specify the same file
in multiple file sets in a target. In CMake 4.4 and above, a file
may belong to at most one file set in a target. This policy provides
compatibility for projects that have not been updated accordingly.
The ``OLD`` behavior for this policy is to accept the same file in
multiple file sets in a target. The ``NEW`` behavior for this policy
is to accept a given file in at most one file set in a target.
.. |INTRODUCED_IN_CMAKE_VERSION| replace:: 4.4
.. |WARNS_OR_DOES_NOT_WARN| replace::
warns when a file is part of multiple file sets in a target
.. include:: include/STANDARD_ADVICE.rst
.. include:: include/DEPRECATED.rst

View File

@@ -0,0 +1,5 @@
FILE_SET-enforce-files-unicity
------------------------------
* A file may belong to at most one :ref:`file set <file sets>` in a target.
See policy :policy:`CMP0211`.

View File

@@ -119,24 +119,11 @@ TdiSourceInfo CollationInformationSources(cmGeneratorTarget const* gt,
}
}
// Detect duplicate sources.
std::set<std::string> visited_sources;
for (auto const& files_per_dir : files_per_dirs.first) {
for (auto const& file : files_per_dir.second) {
auto const full_file = cmSystemTools::CollapseFullPath(file);
auto lookup = sf_map.find(full_file);
if (lookup == sf_map.end()) {
if (visited_sources.count(full_file)) {
// Duplicate source; raise an author warning.
gt->Makefile->IssueMessage(
MessageType::AUTHOR_WARNING,
cmStrCat("Target \"", tgt->GetName(), "\" has source file\n ",
file, "\nin a \"FILE_SET TYPE ",
cm::FileSetMetadata::CXX_MODULES,
"\" multiple times."));
continue;
}
gt->Makefile->IssueMessage(MessageType::FATAL_ERROR,
cmStrCat("Target \"", tgt->GetName(),
"\" has source file\n ", file,
@@ -146,7 +133,6 @@ TdiSourceInfo CollationInformationSources(cmGeneratorTarget const* gt,
"scheduled for compilation."));
continue;
}
visited_sources.insert(full_file);
auto const* sf = lookup->second.first;
CompileType const ct = lookup->second.second;

View File

@@ -6,6 +6,7 @@
#include <map>
#include <sstream>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
@@ -144,14 +145,19 @@ std::vector<BT<std::string>> const& cmGeneratorFileSet::GetFileEntries() const
std::vector<std::unique_ptr<cmCompiledGeneratorExpression>> const&
cmGeneratorFileSet::CompileFileEntries() const
{
std::unordered_set<std::string> uniqueSrcs;
if (this->CompiledFileEntries.empty() &&
!this->FileSet->GetFileEntries().empty()) {
for (auto const& entry : this->FileSet->GetFileEntries()) {
for (auto const& ex : cmList{ entry.Value }) {
cmGeneratorExpression ge(
*this->FileSet->GetMakefile()->GetCMakeInstance(), entry.Backtrace);
auto cge = ge.Parse(ex);
this->CompiledFileEntries.push_back(std::move(cge));
if (uniqueSrcs.insert(ex).second) {
cmGeneratorExpression ge(
*this->FileSet->GetMakefile()->GetCMakeInstance(),
entry.Backtrace);
auto cge = ge.Parse(ex);
this->CompiledFileEntries.push_back(std::move(cge));
}
}
}
}

View File

@@ -4,6 +4,8 @@
#include "cmGeneratorTarget.h"
/* clang-format on */
#include "cmConfigure.h"
#include <cstddef>
#include <functional>
#include <map>
@@ -34,6 +36,7 @@
#include "cmLocalGenerator.h"
#include "cmMakefile.h"
#include "cmMessageType.h"
#include "cmPolicies.h"
#include "cmSourceFile.h"
#include "cmSourceFileLocation.h"
#include "cmSourceGroup.h"
@@ -93,7 +96,7 @@ void AddFileSetEntries(cmGeneratorTarget const* headTarget,
EvaluateTargetPropertyEntries(headTarget, context, dagChecker, sources);
}
bool processSources(cmGeneratorTarget const* tgt,
bool processSources(cmGeneratorTarget const* tgt, std::string const& config,
EvaluatedTargetPropertyEntries& entries,
std::vector<BT<std::string>>& srcs,
std::unordered_set<std::string>& uniqueSrcs,
@@ -155,6 +158,28 @@ bool processSources(cmGeneratorTarget const* tgt,
if (debugSources) {
usedSources += cmStrCat(" * ", src, '\n');
}
} else {
if (auto const* fileSet =
tgt->GetGeneratorFileSets()->GetFileSetForSource(config, src)) {
switch (tgt->GetPolicyStatusCMP0211()) {
case cmPolicies::WARN:
tgt->GetLocalGenerator()->IssueMessage(
MessageType::AUTHOR_WARNING,
cmStrCat(cmPolicies::GetPolicyWarning(cmPolicies::CMP0211),
"\nIn target \"", tgt->GetName(), "\" the file\n ",
src, "\nalready belongs to file set \"",
fileSet->GetName(), "\"."));
CM_FALLTHROUGH;
case cmPolicies::OLD:
break;
default:
tgt->GetLocalGenerator()->IssueMessage(
MessageType::FATAL_ERROR,
cmStrCat("In target \"", tgt->GetName(), "\" the file\n ",
src, "\nalready belongs to file set \"",
fileSet->GetName(), "\"."));
}
}
}
}
if (!usedSources.empty()) {
@@ -193,23 +218,24 @@ std::vector<BT<std::string>> cmGeneratorTarget::GetSourceFilePaths(
std::unordered_set<std::string> uniqueSrcs;
bool contextDependentDirectSources =
processSources(this, entries, files, uniqueSrcs, debugSources);
processSources(this, config, entries, files, uniqueSrcs, debugSources);
// Collect INTERFACE_SOURCES of all direct link-dependencies.
EvaluatedTargetPropertyEntries linkInterfaceSourcesEntries;
AddInterfaceEntries(this, "INTERFACE_SOURCES", context, &dagChecker,
linkInterfaceSourcesEntries, IncludeRuntimeInterface::No,
UseTo::Compile);
bool contextDependentInterfaceSources = processSources(
this, linkInterfaceSourcesEntries, files, uniqueSrcs, debugSources);
bool contextDependentInterfaceSources =
processSources(this, config, linkInterfaceSourcesEntries, files,
uniqueSrcs, debugSources);
// Collect TARGET_OBJECTS of direct object link-dependencies.
bool contextDependentObjects = false;
if (this->GetType() != cmStateEnums::OBJECT_LIBRARY) {
EvaluatedTargetPropertyEntries linkObjectsEntries;
AddObjectEntries(this, context, &dagChecker, linkObjectsEntries);
contextDependentObjects = processSources(this, linkObjectsEntries, files,
uniqueSrcs, debugSources);
contextDependentObjects = processSources(this, config, linkObjectsEntries,
files, uniqueSrcs, debugSources);
// Note that for imported targets or multi-config generators supporting
// cross-config builds the paths to the object files must be per-config,
// so contextDependentObjects will be true here even if object libraries
@@ -243,8 +269,8 @@ std::vector<BT<std::string>> cmGeneratorTarget::GetSourceFilePaths(
#endif
};
bool contextDependentFileSets =
processSources(this, fileSetEntries, files, uniqueSrcs, debugSources,
processFileSetEntry);
processSources(this, config, fileSetEntries, files, uniqueSrcs,
debugSources, processFileSetEntry);
// Determine if sources are context-dependent or not.
if (!contextDependentDirectSources && !contextDependentInterfaceSources &&

View File

@@ -629,7 +629,10 @@ class cmMakefile;
4, 3, 0, WARN) \
SELECT(POLICY, CMP0210, \
"CMAKE_<LANG>_LINK_FLAGS adds link flags to all target types.", 4, \
3, 0, WARN)
3, 0, WARN) \
SELECT(POLICY, CMP0211, \
"A file may belong to at most one file set in a target.", 4, 4, 0, \
WARN)
#define CM_SELECT_ID(F, A1, A2, A3, A4, A5, A6) F(A1)
#define CM_FOR_EACH_POLICY_ID(POLICY) \
@@ -684,7 +687,8 @@ class cmMakefile;
F(CMP0203) \
F(CMP0204) \
F(CMP0209) \
F(CMP0210)
F(CMP0210) \
F(CMP0211)
#define CM_FOR_EACH_CUSTOM_COMMAND_POLICY(F) \
F(CMP0116) \

View File

@@ -1,8 +0,0 @@
CMake Warning \(dev\) in CMakeLists\.txt:
Target "duplicate_sources" has source file
[^
]*/Tests/RunCMake/CXXModulesCompile/duplicate-sources/duplicate\.cxx
in a "FILE_SET TYPE CXX_MODULES" multiple times\.
This warning is for project developers\. Use -Wno-dev to suppress it\.

View File

@@ -40,11 +40,11 @@ if(NOT srcs MATCHES "[^;]*bar.h;[^;]*foo.h$")
endif()
target_sources(foo PRIVATE FILE_SET foo TYPE HEADERS FILES foo.h)
target_sources(foo PRIVATE FILE_SET foo TYPE HEADERS FILES h1.h)
get_property(srcs FILE_SET foo TARGET foo PROPERTY SOURCES)
if(NOT srcs MATCHES ".*/foo.h$")
message(SEND_ERROR "wrong sources: '${srcs}' instead of 'foo.h'")
if(NOT srcs MATCHES ".*/h1.h$")
message(SEND_ERROR "wrong sources: '${srcs}' instead of 'h1.h'")
endif()
get_property(srcs FILE_SET foo TARGET foo PROPERTY INTERFACE_SOURCES)
if(srcs)
@@ -52,11 +52,11 @@ if(srcs)
endif()
set_property(FILE_SET foo TARGET foo PROPERTY SOURCES bar.h)
set_property(FILE_SET foo TARGET foo PROPERTY SOURCES h2.h)
get_property(srcs FILE_SET foo TARGET foo PROPERTY SOURCES)
if(NOT srcs MATCHES "[^;]*bar.h$")
message(SEND_ERROR "wrong sources: '${srcs}' instead of 'bar.h'")
if(NOT srcs MATCHES "[^;]*h2.h$")
message(SEND_ERROR "wrong sources: '${srcs}' instead of 'h2.h'")
endif()
get_property(srcs FILE_SET foo TARGET foo PROPERTY INTERFACE_SOURCES)
if(srcs)
@@ -64,11 +64,11 @@ if(srcs)
endif()
set_property(FILE_SET foo TARGET foo PROPERTY INTERFACE_SOURCES foo.h)
set_property(FILE_SET foo TARGET foo PROPERTY INTERFACE_SOURCES h2.h)
get_property(srcs FILE_SET foo TARGET foo PROPERTY SOURCES)
if(NOT srcs MATCHES "[^;]*bar.h$")
message(SEND_ERROR "wrong sources: '${srcs}' instead of 'bar.h'")
if(NOT srcs MATCHES "[^;]*h2.h$")
message(SEND_ERROR "wrong sources: '${srcs}' instead of 'h2.h'")
endif()
get_property(srcs FILE_SET foo TARGET foo PROPERTY INTERFACE_SOURCES)
if(srcs)

View File

View File

View File

@@ -54,6 +54,7 @@
\* CMP0204
\* CMP0209
\* CMP0210
\* CMP0211
Call Stack \(most recent call first\):
CMakeLists\.txt:3 \(include\)

View File

@@ -0,0 +1,9 @@
enable_language(C)
add_library(foo1 STATIC lib1.c)
target_sources(foo1 PRIVATE FILE_SET h1 TYPE HEADERS FILES h1.h)
target_sources(foo1 PRIVATE FILE_SET h2 TYPE HEADERS FILES h1.h)
add_library(foo2 STATIC lib1.c h1.h)
target_sources(foo2 PRIVATE FILE_SET h1 TYPE HEADERS FILES h1.h)

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,14 @@
CMake Error in CMakeLists\.txt:
In target "foo1" the file
.+/Tests/RunCMake/target_sources/h1\.h
already belongs to file set "h2"\.
CMake Error in CMakeLists\.txt:
In target "foo2" the file
.+/Tests/RunCMake/target_sources/h1\.h
already belongs to file set "h1"\.

View File

@@ -0,0 +1,4 @@
cmake_policy(SET CMP0211 NEW)
include(CMP0211-Common.cmake)

View File

@@ -0,0 +1,4 @@
cmake_policy(SET CMP0211 OLD)
include(CMP0211-Common.cmake)

View File

@@ -0,0 +1,23 @@
CMake Warning \(dev\) in CMakeLists\.txt:
Policy CMP0211 is not set: A file may belong to at most one file set in a
target\. Run "cmake --help-policy CMP0211" for policy details\. Use the
cmake_policy command to set the policy and suppress this warning\.
In target "foo1" the file
.+/Tests/RunCMake/target_sources/h1\.h
already belongs to file set "h2"\.
This warning is for project developers\. Use -Wno-dev to suppress it\.
CMake Warning \(dev\) in CMakeLists\.txt:
Policy CMP0211 is not set: A file may belong to at most one file set in a
target\. Run "cmake --help-policy CMP0211" for policy details\. Use the
cmake_policy command to set the policy and suppress this warning\.
In target "foo2" the file
.+/Tests/RunCMake/target_sources/h1\.h
already belongs to file set "h1"\.
This warning is for project developers\. Use -Wno-dev to suppress it\.

View File

@@ -0,0 +1,2 @@
include(CMP0211-Common.cmake)

View File

@@ -0,0 +1,6 @@
enable_language(C)
add_library(lib1 STATIC empty.c)
# files are silently de-duplicated
target_sources(lib1 PRIVATE FILE_SET HEADERS FILES h1.h h1.h)

View File

@@ -56,20 +56,20 @@ assert_prop_eq(lib1 HEADER_SET_d "")
assert_prop_eq(lib1 INCLUDE_DIRECTORIES "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/.>;$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/dir>")
assert_prop_eq(lib1 INTERFACE_INCLUDE_DIRECTORIES "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/.>;$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>;$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>")
target_sources(lib1 PUBLIC FILE_SET HEADERS BASE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}" FILES h1.h)
target_sources(lib1 PUBLIC FILE_SET HEADERS BASE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}" FILES h4.h)
assert_prop_eq(lib1 INTERFACE_HEADER_SETS "a;c;d;HEADERS")
assert_prop_eq(lib1 HEADER_DIRS "${CMAKE_CURRENT_SOURCE_DIR}")
assert_prop_eq(lib1 HEADER_SET "${CMAKE_CURRENT_SOURCE_DIR}/h1.h")
assert_prop_eq(lib1 HEADER_SET "${CMAKE_CURRENT_SOURCE_DIR}/h4.h")
assert_prop_eq(lib1 HEADER_DIRS_HEADERS "${CMAKE_CURRENT_SOURCE_DIR}")
assert_prop_eq(lib1 HEADER_SET_HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/h1.h")
assert_prop_eq(lib1 HEADER_SET_HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/h4.h")
assert_prop_eq(lib1 INCLUDE_DIRECTORIES "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/.>;$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/dir>;$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>")
assert_prop_eq(lib1 INTERFACE_INCLUDE_DIRECTORIES "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/.>;$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>;$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>;$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>")
target_sources(lib1 PUBLIC FILE_SET HEADERS FILES h2.h)
target_sources(lib1 PUBLIC FILE_SET HEADERS FILES h5.h)
assert_prop_eq(lib1 INTERFACE_HEADER_SETS "a;c;d;HEADERS")
assert_prop_eq(lib1 HEADER_DIRS "${CMAKE_CURRENT_SOURCE_DIR}")
assert_prop_eq(lib1 HEADER_SET "${CMAKE_CURRENT_SOURCE_DIR}/h1.h;${CMAKE_CURRENT_SOURCE_DIR}/h2.h")
assert_prop_eq(lib1 HEADER_SET "${CMAKE_CURRENT_SOURCE_DIR}/h4.h;${CMAKE_CURRENT_SOURCE_DIR}/h5.h")
assert_prop_eq(lib1 HEADER_DIRS_HEADERS "${CMAKE_CURRENT_SOURCE_DIR}")
assert_prop_eq(lib1 HEADER_SET_HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/h1.h;${CMAKE_CURRENT_SOURCE_DIR}/h2.h")
assert_prop_eq(lib1 HEADER_SET_HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/h4.h;${CMAKE_CURRENT_SOURCE_DIR}/h5.h")
assert_prop_eq(lib1 INCLUDE_DIRECTORIES "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/.>;$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/dir>;$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>")
assert_prop_eq(lib1 INTERFACE_INCLUDE_DIRECTORIES "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/.>;$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>;$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>;$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>")

View File

@@ -43,9 +43,13 @@ run_cmake(FileSetCustomTarget)
run_cmake(FileSetBadName)
run_cmake(FileSetWrongSyntax)
run_cmake(FileSetDirect)
run_cmake(FileSetDuplicateSource)
if(APPLE)
run_cmake(FileSetFramework)
endif()
run_cmake(CMP0211-OLD)
run_cmake(CMP0211-NEW)
run_cmake(CMP0211-WARN)
set(RunCMake_TEST_NO_CLEAN 1)
set(RunCMake_TEST_BINARY_DIR "${RunCMake_BINARY_DIR}/FileSetGeneratedDependency-build")

View File

View File