From 58ab03c031e24127c9acfee92ab79f297cddf0f8 Mon Sep 17 00:00:00 2001 From: Tyler Yankee Date: Mon, 9 Mar 2026 12:24:33 -0400 Subject: [PATCH 1/2] add_custom_command: Update dependency search heuristics Search for a target with the `.exe` suffix stripped from its name only if a target by the original name doesn't exist. Fixes: #27612 --- Source/cmLocalGenerator.cxx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Source/cmLocalGenerator.cxx b/Source/cmLocalGenerator.cxx index 1a4bbecb22..2b044b5ede 100644 --- a/Source/cmLocalGenerator.cxx +++ b/Source/cmLocalGenerator.cxx @@ -2331,12 +2331,21 @@ bool cmLocalGenerator::GetRealDependency(std::string const& inName, if (name.empty()) { return false; } - if (cmHasSuffix(name, ".exe"_s)) { - name = cmSystemTools::GetFilenameWithoutLastExtension(name); - } // Look for a CMake target with the given name. - if (cmGeneratorTarget* target = this->FindGeneratorTargetToUse(name)) { + cmGeneratorTarget* target = this->FindGeneratorTargetToUse(name); + if (!target && cmHasSuffix(name, ".exe"_s)) { + // If it doesn't exist, try to strip the `.exe` suffix. + std::string strippedName = + cmSystemTools::GetFilenameWithoutLastExtension(name); + if (cmGeneratorTarget* strippedTarget = + this->FindGeneratorTargetToUse(strippedName)) { + name = strippedName; + target = strippedTarget; + } + } + + if (target) { // make sure it is not just a coincidence that the target name // found is part of the inName if (cmSystemTools::FileIsFullPath(inName)) { From 858fdd426b030fb3336d6f19c3034fc781b9f337 Mon Sep 17 00:00:00 2001 From: Tyler Yankee Date: Mon, 9 Mar 2026 12:46:40 -0400 Subject: [PATCH 2/2] 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 --- Help/command/add_custom_command.rst | 6 ++++ Help/manual/cmake-policies.7.rst | 1 + Help/policy/CMP0212.rst | 31 +++++++++++++++++++ .../dev/add_custom_command-DEPENDS-exe.rst | 6 ++++ Source/cmFastbuildTargetGenerator.cxx | 3 +- Source/cmGlobalXCodeGenerator.cxx | 6 ++-- Source/cmLocalGenerator.cxx | 7 +++-- Source/cmLocalGenerator.h | 2 +- Source/cmLocalNinjaGenerator.cxx | 3 +- Source/cmLocalUnixMakefileGenerator3.cxx | 3 +- Source/cmLocalVisualStudio7Generator.cxx | 3 +- Source/cmPolicies.h | 6 +++- Source/cmVisualStudio10TargetGenerator.cxx | 2 +- .../CMP0212/CMP0212-NEW-build-result.txt | 1 + .../CMP0212/CMP0212-NEW-build-stdout.txt | 1 + .../RunCMake/CMP0212/CMP0212-common-exe.cmake | 2 ++ Tests/RunCMake/CMP0212/CMakeLists.txt | 4 +++ Tests/RunCMake/CMP0212/RunCMakeTest.cmake | 12 +++++++ Tests/RunCMake/CMP0212/main.c | 4 +++ .../RunCMake/CMP0212/subdir/CMP0212-NEW.cmake | 11 +++++++ .../RunCMake/CMP0212/subdir/CMP0212-OLD.cmake | 2 ++ .../subdir/CMP0212-common-custom.cmake | 6 ++++ Tests/RunCMake/CMP0212/subdir/CMakeLists.txt | 1 + Tests/RunCMake/CMP0212/subdir/bad.c | 1 + Tests/RunCMake/CMakeLists.txt | 3 ++ 25 files changed, 115 insertions(+), 12 deletions(-) create mode 100644 Help/policy/CMP0212.rst create mode 100644 Help/release/dev/add_custom_command-DEPENDS-exe.rst create mode 100644 Tests/RunCMake/CMP0212/CMP0212-NEW-build-result.txt create mode 100644 Tests/RunCMake/CMP0212/CMP0212-NEW-build-stdout.txt create mode 100644 Tests/RunCMake/CMP0212/CMP0212-common-exe.cmake create mode 100644 Tests/RunCMake/CMP0212/CMakeLists.txt create mode 100644 Tests/RunCMake/CMP0212/RunCMakeTest.cmake create mode 100644 Tests/RunCMake/CMP0212/main.c create mode 100644 Tests/RunCMake/CMP0212/subdir/CMP0212-NEW.cmake create mode 100644 Tests/RunCMake/CMP0212/subdir/CMP0212-OLD.cmake create mode 100644 Tests/RunCMake/CMP0212/subdir/CMP0212-common-custom.cmake create mode 100644 Tests/RunCMake/CMP0212/subdir/CMakeLists.txt create mode 100644 Tests/RunCMake/CMP0212/subdir/bad.c diff --git a/Help/command/add_custom_command.rst b/Help/command/add_custom_command.rst index cb18bcc855..4dc92b2d61 100644 --- a/Help/command/add_custom_command.rst +++ b/Help/command/add_custom_command.rst @@ -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. diff --git a/Help/manual/cmake-policies.7.rst b/Help/manual/cmake-policies.7.rst index 8188f93fa9..761963b751 100644 --- a/Help/manual/cmake-policies.7.rst +++ b/Help/manual/cmake-policies.7.rst @@ -100,6 +100,7 @@ Policies Introduced by CMake 4.4 .. toctree:: :maxdepth: 1 + CMP0212: add_custom_command DEPENDS does not strip .exe suffixes. CMP0211: A file may belong to at most one file set in a target. Policies Introduced by CMake 4.3 diff --git a/Help/policy/CMP0212.rst b/Help/policy/CMP0212.rst new file mode 100644 index 0000000000..81d7828f43 --- /dev/null +++ b/Help/policy/CMP0212.rst @@ -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 diff --git a/Help/release/dev/add_custom_command-DEPENDS-exe.rst b/Help/release/dev/add_custom_command-DEPENDS-exe.rst new file mode 100644 index 0000000000..02bb71b356 --- /dev/null +++ b/Help/release/dev/add_custom_command-DEPENDS-exe.rst @@ -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`. diff --git a/Source/cmFastbuildTargetGenerator.cxx b/Source/cmFastbuildTargetGenerator.cxx index 06f6ab9eca..2c008bb3c4 100644 --- a/Source/cmFastbuildTargetGenerator.cxx +++ b/Source/cmFastbuildTargetGenerator.cxx @@ -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); diff --git a/Source/cmGlobalXCodeGenerator.cxx b/Source/cmGlobalXCodeGenerator.cxx index 68614bbf4c..aaecf3a952 100644 --- a/Source/cmGlobalXCodeGenerator.cxx +++ b/Source/cmGlobalXCodeGenerator.cxx @@ -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)); } } diff --git a/Source/cmLocalGenerator.cxx b/Source/cmLocalGenerator.cxx index 2b044b5ede..258ce56477 100644 --- a/Source/cmLocalGenerator.cxx +++ b/Source/cmLocalGenerator.cxx @@ -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 = diff --git a/Source/cmLocalGenerator.h b/Source/cmLocalGenerator.h index 3196c73113..c7091578f7 100644 --- a/Source/cmLocalGenerator.h +++ b/Source/cmLocalGenerator.h @@ -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 */) {} diff --git a/Source/cmLocalNinjaGenerator.cxx b/Source/cmLocalNinjaGenerator.cxx index b6cc3a6c86..dc319af078 100644 --- a/Source/cmLocalNinjaGenerator.cxx +++ b/Source/cmLocalNinjaGenerator.cxx @@ -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)); } diff --git a/Source/cmLocalUnixMakefileGenerator3.cxx b/Source/cmLocalUnixMakefileGenerator3.cxx index 8658f5d10f..dca015b6df 100644 --- a/Source/cmLocalUnixMakefileGenerator3.cxx +++ b/Source/cmLocalUnixMakefileGenerator3.cxx @@ -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)); } } diff --git a/Source/cmLocalVisualStudio7Generator.cxx b/Source/cmLocalVisualStudio7Generator.cxx index 3d44ae8626..1619f84f4d 100644 --- a/Source/cmLocalVisualStudio7Generator.cxx +++ b/Source/cmLocalVisualStudio7Generator.cxx @@ -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) << ";"; } } diff --git a/Source/cmPolicies.h b/Source/cmPolicies.h index 3f54a1e1ac..5b0fc00f14 100644 --- a/Source/cmPolicies.h +++ b/Source/cmPolicies.h @@ -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 diff --git a/Source/cmVisualStudio10TargetGenerator.cxx b/Source/cmVisualStudio10TargetGenerator.cxx index c5992ee0f7..b56fd19207 100644 --- a/Source/cmVisualStudio10TargetGenerator.cxx +++ b/Source/cmVisualStudio10TargetGenerator.cxx @@ -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; diff --git a/Tests/RunCMake/CMP0212/CMP0212-NEW-build-result.txt b/Tests/RunCMake/CMP0212/CMP0212-NEW-build-result.txt new file mode 100644 index 0000000000..d197c913c2 --- /dev/null +++ b/Tests/RunCMake/CMP0212/CMP0212-NEW-build-result.txt @@ -0,0 +1 @@ +[^0] diff --git a/Tests/RunCMake/CMP0212/CMP0212-NEW-build-stdout.txt b/Tests/RunCMake/CMP0212/CMP0212-NEW-build-stdout.txt new file mode 100644 index 0000000000..8d98f9debd --- /dev/null +++ b/Tests/RunCMake/CMP0212/CMP0212-NEW-build-stdout.txt @@ -0,0 +1 @@ +.* diff --git a/Tests/RunCMake/CMP0212/CMP0212-common-exe.cmake b/Tests/RunCMake/CMP0212/CMP0212-common-exe.cmake new file mode 100644 index 0000000000..05eda9ff01 --- /dev/null +++ b/Tests/RunCMake/CMP0212/CMP0212-common-exe.cmake @@ -0,0 +1,2 @@ +add_executable(foo main.c) +set_target_properties(foo PROPERTIES RUNTIME_OUTPUT_DIRECTORY bin) diff --git a/Tests/RunCMake/CMP0212/CMakeLists.txt b/Tests/RunCMake/CMP0212/CMakeLists.txt new file mode 100644 index 0000000000..b18faf46dd --- /dev/null +++ b/Tests/RunCMake/CMP0212/CMakeLists.txt @@ -0,0 +1,4 @@ +cmake_minimum_required(VERSION 4.3) +project(${RunCMake_TEST} C) +include(CMP0212-common-exe.cmake) +add_subdirectory(subdir) diff --git a/Tests/RunCMake/CMP0212/RunCMakeTest.cmake b/Tests/RunCMake/CMP0212/RunCMakeTest.cmake new file mode 100644 index 0000000000..d4a0747aea --- /dev/null +++ b/Tests/RunCMake/CMP0212/RunCMakeTest.cmake @@ -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) diff --git a/Tests/RunCMake/CMP0212/main.c b/Tests/RunCMake/CMP0212/main.c new file mode 100644 index 0000000000..f8b643afbf --- /dev/null +++ b/Tests/RunCMake/CMP0212/main.c @@ -0,0 +1,4 @@ +int main() +{ + return 0; +} diff --git a/Tests/RunCMake/CMP0212/subdir/CMP0212-NEW.cmake b/Tests/RunCMake/CMP0212/subdir/CMP0212-NEW.cmake new file mode 100644 index 0000000000..9568a4a8b5 --- /dev/null +++ b/Tests/RunCMake/CMP0212/subdir/CMP0212-NEW.cmake @@ -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 +) diff --git a/Tests/RunCMake/CMP0212/subdir/CMP0212-OLD.cmake b/Tests/RunCMake/CMP0212/subdir/CMP0212-OLD.cmake new file mode 100644 index 0000000000..05b484967c --- /dev/null +++ b/Tests/RunCMake/CMP0212/subdir/CMP0212-OLD.cmake @@ -0,0 +1,2 @@ +cmake_policy(SET CMP0212 OLD) +include(CMP0212-common-custom.cmake) diff --git a/Tests/RunCMake/CMP0212/subdir/CMP0212-common-custom.cmake b/Tests/RunCMake/CMP0212/subdir/CMP0212-common-custom.cmake new file mode 100644 index 0000000000..d5f2c9e02b --- /dev/null +++ b/Tests/RunCMake/CMP0212/subdir/CMP0212-common-custom.cmake @@ -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) diff --git a/Tests/RunCMake/CMP0212/subdir/CMakeLists.txt b/Tests/RunCMake/CMP0212/subdir/CMakeLists.txt new file mode 100644 index 0000000000..a7cff2bd4d --- /dev/null +++ b/Tests/RunCMake/CMP0212/subdir/CMakeLists.txt @@ -0,0 +1 @@ +include(${RunCMake_TEST}.cmake) diff --git a/Tests/RunCMake/CMP0212/subdir/bad.c b/Tests/RunCMake/CMP0212/subdir/bad.c new file mode 100644 index 0000000000..a364cfd0d7 --- /dev/null +++ b/Tests/RunCMake/CMP0212/subdir/bad.c @@ -0,0 +1 @@ +#error "This is an invalid C file." diff --git a/Tests/RunCMake/CMakeLists.txt b/Tests/RunCMake/CMakeLists.txt index 93282796c6..d684cd3054 100644 --- a/Tests/RunCMake/CMakeLists.txt +++ b/Tests/RunCMake/CMakeLists.txt @@ -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})