add_custom_command: Disallow exe-suffixed targets in DEPENDS

Two targets whose names differ only in a `.exe` suffix create a
conflict when adding target- and file-level dependencies for a custom
command. Add CMP0212 to provide compatibility with the old behavior.

Issue: #27612
This commit is contained in:
Tyler Yankee
2026-03-09 12:46:40 -04:00
parent 58ab03c031
commit 858fdd426b
25 changed files with 115 additions and 12 deletions

View File

@@ -188,6 +188,12 @@ Generating Files
cause the custom command to re-run whenever the target is
recompiled.
.. versionchanged:: 4.4
Previously an executable target could be specified using the name
of the target with ``.exe`` appended. This is no longer allowed.
See policy :policy:`CMP0212`.
2. If the argument is an absolute path, a file-level dependency
is created on that path.

View File

@@ -100,6 +100,7 @@ Policies Introduced by CMake 4.4
.. toctree::
:maxdepth: 1
CMP0212: add_custom_command DEPENDS does not strip .exe suffixes. </policy/CMP0212>
CMP0211: A file may belong to at most one file set in a target. </policy/CMP0211>
Policies Introduced by CMake 4.3

31
Help/policy/CMP0212.rst Normal file
View File

@@ -0,0 +1,31 @@
CMP0212
-------
.. versionadded:: 4.4
:command:`add_custom_command` ``DEPENDS`` does not strip ``.exe`` suffixes.
When searching for dependencies specified with the ``DEPENDS`` option of
:command:`add_custom_command`, CMake 4.3 and below applied a heuristic to
remove ``.exe`` from the end of ``DEPENDS`` argument values, which created a
target-level dependency on an executable with that stripped name if such a
target exists. This allowed using the name of the target output file as an
alternative spelling when naming dependencies. However, with the emergence of
CMake's target model, specifying the output file in this way is no longer
needed, and leads to conflicts in creating target- and file-level dependencies
when targets exist with the ``.exe`` suffix in the name itself.
The ``OLD`` behavior of this policy strips the ``.exe`` suffix from arguments
to ``DEPENDS`` when searching for target-level dependencies. The ``NEW``
behavior does not strip the ``.exe`` suffix in its search. CMake will otherwise
proceed normally with the other dependency search heuristics as specified in
:command:`add_custom_command` ``DEPENDS``. Users are encouraged to specify the
name of the executable target if a target-level dependency is desired, or use
the :genex:`TARGET_FILE` generator expression if a file-level dependency
is desired, rather than implying the target indirectly via its output file name.
.. |INTRODUCED_IN_CMAKE_VERSION| replace:: 4.4
.. |WARNS_OR_DOES_NOT_WARN| replace:: does *not* warn
.. include:: include/STANDARD_ADVICE.rst
.. include:: include/DEPRECATED.rst

View File

@@ -0,0 +1,6 @@
add_custom_command-DEPENDS-exe
------------------------------
* Names of executables given to the ``DEPENDS`` argument of
:command:`add_custom_command` no longer have ``.exe`` suffixes stripped to
establish target-level dependencies. See policy :policy:`CMP0212`.

View File

@@ -356,7 +356,8 @@ void cmFastbuildTargetGenerator::GetDepends(
for (auto dep : ccg.GetDepends()) {
LogMessage("Dep: " + dep);
auto orig = dep;
if (this->LocalCommonGenerator->GetRealDependency(dep, Config, dep)) {
if (this->LocalCommonGenerator->GetRealDependency(
dep, Config, dep, ccg.GetCC().GetCMP0212Status())) {
LogMessage("Real dep: " + dep);
if (!dep.empty()) {
LogMessage("Custom command real dep: " + dep);

View File

@@ -2095,7 +2095,8 @@ cmXCodeObject* cmGlobalXCodeGenerator::CreateRunScriptBuildPhase(
realDepends.reserve(ccg.GetDepends().size());
for (auto const& d : ccg.GetDepends()) {
std::string dep;
if (this->CurrentLocalGenerator->GetRealDependency(d, configName, dep)) {
if (this->CurrentLocalGenerator->GetRealDependency(
d, configName, dep, cc.GetCMP0212Status())) {
realDepends.emplace_back(std::move(dep));
}
}
@@ -2464,7 +2465,8 @@ void cmGlobalXCodeGenerator::CreateCustomRulesMakefile(
realDepends.reserve(ccg.GetDepends().size());
for (auto const& d : ccg.GetDepends()) {
std::string dep;
if (this->CurrentLocalGenerator->GetRealDependency(d, configName, dep)) {
if (this->CurrentLocalGenerator->GetRealDependency(
d, configName, dep, command.GetCMP0212Status())) {
realDepends.emplace_back(std::move(dep));
}
}

View File

@@ -2317,7 +2317,8 @@ cmGeneratorTarget* cmLocalGenerator::FindGeneratorTargetToUse(
bool cmLocalGenerator::GetRealDependency(std::string const& inName,
std::string const& config,
std::string& dep)
std::string& dep,
cmPolicies::PolicyStatus cmp0212)
{
// Older CMake code may specify the dependency using the target
// output file rather than the target name. Such code would have
@@ -2334,8 +2335,8 @@ bool cmLocalGenerator::GetRealDependency(std::string const& inName,
// Look for a CMake target with the given name.
cmGeneratorTarget* target = this->FindGeneratorTargetToUse(name);
if (!target && cmHasSuffix(name, ".exe"_s)) {
// If it doesn't exist, try to strip the `.exe` suffix.
if (!target && cmHasSuffix(name, ".exe"_s) && cmp0212 != cmPolicies::NEW) {
// If it doesn't exist, try to strip the `.exe` suffix per CMP0212.
std::string strippedName =
cmSystemTools::GetFilenameWithoutLastExtension(name);
if (cmGeneratorTarget* strippedTarget =

View File

@@ -310,7 +310,7 @@ public:
* used for dependencies of custom commands.
*/
bool GetRealDependency(std::string const& name, std::string const& config,
std::string& dep);
std::string& dep, cmPolicies::PolicyStatus cmp0212);
/** Called from command-line hook to clear dependencies. */
virtual void ClearDependencies(cmMakefile* /* mf */, bool /* verbose */) {}

View File

@@ -414,7 +414,8 @@ void cmLocalNinjaGenerator::AppendCustomCommandDeps(
{
for (std::string const& i : ccg.GetDepends()) {
std::string dep;
if (this->GetRealDependency(i, config, dep)) {
if (this->GetRealDependency(i, config, dep,
ccg.GetCC().GetCMP0212Status())) {
ninjaDeps.push_back(
this->GetGlobalNinjaGenerator()->ConvertToNinjaPath(dep));
}

View File

@@ -957,7 +957,8 @@ void cmLocalUnixMakefileGenerator3::AppendCustomDepend(
for (std::string const& d : ccg.GetDepends()) {
// Lookup the real name of the dependency in case it is a CMake target.
std::string dep;
if (this->GetRealDependency(d, this->GetConfigName(), dep)) {
if (this->GetRealDependency(d, this->GetConfigName(), dep,
ccg.GetCC().GetCMP0212Status())) {
depends.push_back(std::move(dep));
}
}

View File

@@ -1867,7 +1867,8 @@ void cmLocalVisualStudio7Generator::WriteCustomRule(
for (std::string const& d : ccg.GetDepends()) {
// Get the real name of the dependency in case it is a CMake target.
std::string dep;
if (this->GetRealDependency(d, config, dep)) {
if (this->GetRealDependency(d, config, dep,
command.GetCMP0212Status())) {
fout << this->ConvertToXMLOutputPath(dep) << ";";
}
}

View File

@@ -632,6 +632,9 @@ class cmMakefile;
3, 0, WARN) \
SELECT(POLICY, CMP0211, \
"A file may belong to at most one file set in a target.", 4, 4, 0, \
WARN) \
SELECT(POLICY, CMP0212, \
"add_custom_command DEPENDS does not strip .exe suffixes.", 4, 4, 0, \
WARN)
#define CM_SELECT_ID(F, A1, A2, A3, A4, A5, A6) F(A1)
@@ -692,7 +695,8 @@ class cmMakefile;
#define CM_FOR_EACH_CUSTOM_COMMAND_POLICY(F) \
F(CMP0116) \
F(CMP0147)
F(CMP0147) \
F(CMP0212)
/** \class cmPolicies
* \brief Handles changes in CMake behavior and policies

View File

@@ -1840,7 +1840,7 @@ void cmVisualStudio10TargetGenerator::WriteCustomRule(
for (std::string const& d : ccg.GetDepends()) {
std::string dep;
if (lg->GetRealDependency(d, c, dep)) {
if (lg->GetRealDependency(d, c, dep, command.GetCMP0212Status())) {
if (!unique_inputs.insert(dep).second) {
// already listed
continue;

View File

@@ -0,0 +1 @@
[^0]

View File

@@ -0,0 +1 @@
.*

View File

@@ -0,0 +1,2 @@
add_executable(foo main.c)
set_target_properties(foo PROPERTIES RUNTIME_OUTPUT_DIRECTORY bin)

View File

@@ -0,0 +1,4 @@
cmake_minimum_required(VERSION 4.3)
project(${RunCMake_TEST} C)
include(CMP0212-common-exe.cmake)
add_subdirectory(subdir)

View File

@@ -0,0 +1,12 @@
include(RunCMake)
function(run_cmake_case_cmp0212 case)
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${case}-build)
run_cmake(${case})
set(RunCMake_TEST_NO_CLEAN 1)
set(RunCMake_TEST_OUTPUT_MERGE 1)
run_cmake_command(${case}-build ${CMAKE_COMMAND} --build . --target ${ARGN})
endfunction()
run_cmake_case_cmp0212(CMP0212-OLD tgt1)
run_cmake_case_cmp0212(CMP0212-NEW tgt1 bar)

View File

@@ -0,0 +1,4 @@
int main()
{
return 0;
}

View File

@@ -0,0 +1,11 @@
cmake_policy(SET CMP0212 NEW)
include(CMP0212-common-custom.cmake)
# Define an executable with a different target name so that the above custom
# command finds the file-level dependency instead of the target-level
# dependency, and thus fails the build by compiling a bad C file.
add_executable(bar bad.c)
set_target_properties(bar PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "$<1:${CMAKE_CURRENT_BINARY_DIR}>"
RUNTIME_OUTPUT_NAME foo
)

View File

@@ -0,0 +1,2 @@
cmake_policy(SET CMP0212 OLD)
include(CMP0212-common-custom.cmake)

View File

@@ -0,0 +1,6 @@
add_custom_command(
OUTPUT cmd1
COMMAND ${CMAKE_COMMAND} -E touch "${CMAKE_CURRENT_BINARY_DIR}/cmd1"
DEPENDS foo.exe
)
add_custom_target(tgt1 DEPENDS cmd1)

View File

@@ -0,0 +1 @@
include(${RunCMake_TEST}.cmake)

View File

@@ -0,0 +1 @@
#error "This is an invalid C file."

View File

@@ -180,6 +180,9 @@ add_RunCMake_test(CMP0170)
add_RunCMake_test(CMP0171)
add_RunCMake_test(CMP0173)
add_RunCMake_test(CMP0187)
if(WIN32)
add_RunCMake_test(CMP0212)
endif()
if(CMAKE_C_COMPILER_ID STREQUAL "MSVC")
add_RunCMake_test(CMP0194 -DCMAKE_C_COMPILER_VERSION=${CMAKE_C_COMPILER_VERSION})