From 503bdb9777ff44469aaa577568bd7cdbbc153f26 Mon Sep 17 00:00:00 2001 From: Daksh Mamodiya Date: Wed, 24 Jun 2026 16:38:16 +0200 Subject: [PATCH] Makefile: Generate test_prep targets for CMAKE_TEST_BUILD_DEPENDS Extend the test_prep/ and test_prep/all build targets, generated from add_test(BUILD_DEPENDS) when CMAKE_TEST_BUILD_DEPENDS is enabled, to the Makefile generators. Previously only the Ninja and FASTBuild generators provided them. Resolve each test's dependencies in a shared layer so all command-line generators consume one result. cmTestGenerator now filters target dependencies to build-system targets and records, for each file dependency, the single target that produces it as a primary custom-command output (cmGlobalGenerator::FindOutputOwningTarget). The Makefile generators emit phony rules in CMakeFiles/Makefile2 depending on .dir/all, with top-level forwarding rules. Sharing the resolution also stops Ninja and FASTBuild from emitting a dead test_prep edge for a non-buildable (e.g. INTERFACE) BUILD_DEPENDS target. On the Makefile generators a file dependency that no single target produces, and a test whose name contains ':', cannot be expressed as a recursive-make rule and are reported with a warning and skipped. Fixes: #27879 --- Help/command/add_test.rst | 14 +- Help/generator/Unix Makefiles.rst | 20 +++ .../dev/test-prep-makefile-generators.rst | 9 + Help/variable/CMAKE_TEST_BUILD_DEPENDS.rst | 13 +- Source/cmGlobalFastbuildGenerator.cxx | 5 +- Source/cmGlobalGenerator.cxx | 41 +++++ Source/cmGlobalGenerator.h | 12 ++ Source/cmGlobalNinjaGenerator.cxx | 8 +- Source/cmGlobalUnixMakefileGenerator3.cxx | 166 ++++++++++++++++++ Source/cmGlobalUnixMakefileGenerator3.h | 21 +++ Source/cmTestGenerator.cxx | 35 +++- Source/cmTestGenerator.h | 15 +- Tests/RunCMake/add_test/RunCMakeTest.cmake | 53 ++++-- .../TestDependency-ON-byproduct-stderr.txt | 9 + .../TestDependency-ON-byproduct.cmake | 20 +++ .../TestDependency-ON-colon-name-stderr.txt | 5 + .../TestDependency-ON-colon-name.cmake | 20 +++ .../TestDependency-ON-interface.cmake | 19 ++ .../TestDependency-ON-orphan-stderr.txt | 9 + .../add_test/TestDependency-ON-orphan.cmake | 20 +++ .../TestDependency-check-targets.cmake | 14 +- 21 files changed, 496 insertions(+), 32 deletions(-) create mode 100644 Help/release/dev/test-prep-makefile-generators.rst create mode 100644 Tests/RunCMake/add_test/TestDependency-ON-byproduct-stderr.txt create mode 100644 Tests/RunCMake/add_test/TestDependency-ON-byproduct.cmake create mode 100644 Tests/RunCMake/add_test/TestDependency-ON-colon-name-stderr.txt create mode 100644 Tests/RunCMake/add_test/TestDependency-ON-colon-name.cmake create mode 100644 Tests/RunCMake/add_test/TestDependency-ON-interface.cmake create mode 100644 Tests/RunCMake/add_test/TestDependency-ON-orphan-stderr.txt create mode 100644 Tests/RunCMake/add_test/TestDependency-ON-orphan.cmake diff --git a/Help/command/add_test.rst b/Help/command/add_test.rst index 66450a07a7..f5dc26f6b1 100644 --- a/Help/command/add_test.rst +++ b/Help/command/add_test.rst @@ -82,7 +82,12 @@ directory the test is created in. .. versionchanged:: 4.5 - :generator:`FASTBuild` gained support for ``test_prep/`` targets. + :generator:`FASTBuild` and the :ref:`Makefile Generators` gained support + for ``test_prep/`` targets. With the Makefile generators a + ``BUILD_DEPENDS`` file is built by building the target whose build + produces it; a file that is not the unique output of a single target is + reported with a warning, and a test whose name contains a ``:`` character + is excluded. The command may be specified using :manual:`generator expressions `. @@ -101,9 +106,10 @@ directory the test is created in. Specify a list of targets or files that must be built before the test can run. Each dependency is added to the ``test_prep/`` build target - described above when :variable:`CMAKE_TEST_BUILD_DEPENDS` is enabled with - the :ref:`Ninja Generators` or :generator:`FASTBuild`. The test name must be - a valid target name in order to list build dependencies with this keyword. + described above when :variable:`CMAKE_TEST_BUILD_DEPENDS` is enabled with the + :ref:`Ninja Generators`, :generator:`FASTBuild`, or :ref:`Makefile + Generators`. The test name must be a valid target name in order to list + build dependencies with this keyword. ``COMMAND_EXPAND_LISTS`` .. versionadded:: 3.16 diff --git a/Help/generator/Unix Makefiles.rst b/Help/generator/Unix Makefiles.rst index dfe4ecbd9e..03b577a1cb 100644 --- a/Help/generator/Unix Makefiles.rst +++ b/Help/generator/Unix Makefiles.rst @@ -29,3 +29,23 @@ be created, containing the following targets: ``package`` Runs the package step in the subdirectory, if any. + +.. versionadded:: 4.5 + + When the :variable:`CMAKE_TEST_BUILD_DEPENDS` variable is enabled, the + top-level makefile additionally provides the following targets: + + ``test_prep/`` + Builds all known build dependencies for the test named ```` + added by :command:`add_test`, including the executable target invoked by + the test, targets referenced by generator expressions in the test command, + and explicit ``BUILD_DEPENDS`` entries. A ``BUILD_DEPENDS`` file is built + by building the target whose build produces it. + + Tests whose names are not valid target names, and tests whose names + contain a ``:`` character, are excluded. If multiple tests in different + directories share the same name, their dependencies are merged into one + ``test_prep/`` target. + + ``test_prep/all`` + Depends on every generated ``test_prep/`` target. diff --git a/Help/release/dev/test-prep-makefile-generators.rst b/Help/release/dev/test-prep-makefile-generators.rst new file mode 100644 index 0000000000..3bafbaf9f7 --- /dev/null +++ b/Help/release/dev/test-prep-makefile-generators.rst @@ -0,0 +1,9 @@ +test-prep-makefile-generators +----------------------------- + +* The :generator:`FASTBuild` generator and the :ref:`Makefile Generators` + now generate ``test_prep/`` and ``test_prep/all`` convenience + build targets when the :variable:`CMAKE_TEST_BUILD_DEPENDS` variable is + enabled, matching the behavior of the :ref:`Ninja Generators`. These + targets build the dependencies of tests added by :command:`add_test`, + including those listed with the ``BUILD_DEPENDS`` keyword. diff --git a/Help/variable/CMAKE_TEST_BUILD_DEPENDS.rst b/Help/variable/CMAKE_TEST_BUILD_DEPENDS.rst index 8c48676733..e3a2e96724 100644 --- a/Help/variable/CMAKE_TEST_BUILD_DEPENDS.rst +++ b/Help/variable/CMAKE_TEST_BUILD_DEPENDS.rst @@ -5,11 +5,12 @@ CMAKE_TEST_BUILD_DEPENDS .. versionchanged:: 4.5 - :generator:`FASTBuild` gained support for ``test_prep/`` targets. + :generator:`FASTBuild` and the :ref:`Makefile Generators` gained support + for ``test_prep/`` targets. Enable ``test_prep/`` build targets for tests added by -:command:`add_test` when using the :ref:`Ninja Generators` or -:generator:`FASTBuild`. +:command:`add_test` when using the :ref:`Ninja Generators`, +:generator:`FASTBuild`, or :ref:`Makefile Generators`. When this variable is set to a true value, CMake generates a ``test_prep/`` target for each eligible test and a ``test_prep/all`` @@ -20,3 +21,9 @@ targets referenced by test command generator expressions, and explicit Tests whose names are not valid target names are excluded from this behavior. If multiple tests in different directories share the same name, their dependencies are merged into a single ``test_prep/`` target. + +With the :ref:`Makefile Generators`, a ``BUILD_DEPENDS`` file is built by +building the target whose build produces it. A generated file that is not the +unique output of a single target is reported with a warning, and a test whose +name contains a ``:`` character is excluded because it cannot be expressed as +a Makefile target. diff --git a/Source/cmGlobalFastbuildGenerator.cxx b/Source/cmGlobalFastbuildGenerator.cxx index 26a118646d..369ad8abc7 100644 --- a/Source/cmGlobalFastbuildGenerator.cxx +++ b/Source/cmGlobalFastbuildGenerator.cxx @@ -1614,8 +1614,9 @@ void cmGlobalFastbuildGenerator::WriteTestPrepTargets() for (cmGeneratorTarget* depTarget : testDeps.Targets) { testPrepTarget.Dependencies.emplace(depTarget->GetName()); } - for (std::string const& depFile : testDeps.Files) { - testPrepTarget.Dependencies.emplace(depFile); + for (cmTestGenerator::BuildDependencies::FileDependency const& depFile : + testDeps.Files) { + testPrepTarget.Dependencies.emplace(depFile.Path); } } } diff --git a/Source/cmGlobalGenerator.cxx b/Source/cmGlobalGenerator.cxx index bddde54e48..3e6e47606f 100644 --- a/Source/cmGlobalGenerator.cxx +++ b/Source/cmGlobalGenerator.cxx @@ -2232,10 +2232,51 @@ void cmGlobalGenerator::CreateGeneratorTargets(TargetTypes targetTypes) } } +void cmGlobalGenerator::ComputeOutputOwnerIndex() +{ + this->OutputOwnerIndexComputed = true; + for (auto const& lg : this->LocalGenerators) { + for (auto const& gt : lg->GetGeneratorTargets()) { + if (!gt->IsInBuildSystem()) { + continue; + } + for (cmGeneratorTarget::AllConfigSource const& acs : + gt->GetAllConfigSources( + cmGeneratorTarget::SourceKindCustomCommand)) { + cmCustomCommand const* cc = acs.Source->GetCustomCommand(); + if (!cc) { + continue; + } + for (std::string const& out : cc->GetOutputs()) { + this->OutputOwnerIndex[cmSystemTools::CollapseFullPath(out)] + .push_back(gt.get()); + } + } + } + } +} + +cmGeneratorTarget* cmGlobalGenerator::FindOutputOwningTarget( + std::string const& output) +{ + if (!this->OutputOwnerIndexComputed) { + this->ComputeOutputOwnerIndex(); + } + auto it = + this->OutputOwnerIndex.find(cmSystemTools::CollapseFullPath(output)); + if (it != this->OutputOwnerIndex.end() && it->second.size() == 1) { + return it->second.front(); + } + return nullptr; +} + void cmGlobalGenerator::ClearGeneratorMembers() { this->BuildExportSets.clear(); + this->OutputOwnerIndex.clear(); + this->OutputOwnerIndexComputed = false; + this->Makefiles.clear(); this->LocalGenerators.clear(); diff --git a/Source/cmGlobalGenerator.h b/Source/cmGlobalGenerator.h index 7f941e3be1..39aad5d618 100644 --- a/Source/cmGlobalGenerator.h +++ b/Source/cmGlobalGenerator.h @@ -313,6 +313,11 @@ public: std::vector GetLocalGeneratorTargetsInOrder( cmLocalGenerator* lg) const; + // Find the single build-system target that produces the given path as a + // primary custom-command output, or nullptr if there is none or more than + // one. Used to resolve file-level test build dependencies. + cmGeneratorTarget* FindOutputOwningTarget(std::string const& output); + cmMakefile* GetCurrentMakefile() const { return this->CurrentConfigureMakefile; @@ -978,6 +983,13 @@ private: using TargetDependMap = std::map; TargetDependMap TargetDependencies; + // Map from a custom-command primary output (collapsed full path) to the + // build-system target(s) that produce it. Built lazily on first use and + // cleared with the other generator members. + std::map> OutputOwnerIndex; + bool OutputOwnerIndexComputed = false; + void ComputeOutputOwnerIndex(); + friend class cmake; void CreateGeneratorTargets( TargetTypes targetTypes, cmMakefile* mf, cmLocalGenerator* lg, diff --git a/Source/cmGlobalNinjaGenerator.cxx b/Source/cmGlobalNinjaGenerator.cxx index bed1634f2b..285ba7f25d 100644 --- a/Source/cmGlobalNinjaGenerator.cxx +++ b/Source/cmGlobalNinjaGenerator.cxx @@ -1326,9 +1326,11 @@ void cmGlobalNinjaGenerator::WriteTestPrepTargets() this->AppendTargetOutputs(depTarget, testPrepTarget.ExplicitDeps, config, DependOnTargetArtifact); } - std::transform(testDeps.Files.begin(), testDeps.Files.end(), - std::back_inserter(testPrepTarget.ExplicitDeps), - this->MapToNinjaPath()); + for (cmTestGenerator::BuildDependencies::FileDependency const& file : + testDeps.Files) { + testPrepTarget.ExplicitDeps.push_back( + this->ConvertToNinjaPath(file.Path)); + } } } diff --git a/Source/cmGlobalUnixMakefileGenerator3.cxx b/Source/cmGlobalUnixMakefileGenerator3.cxx index 58ae2478ae..a65cc1fe5e 100644 --- a/Source/cmGlobalUnixMakefileGenerator3.cxx +++ b/Source/cmGlobalUnixMakefileGenerator3.cxx @@ -18,6 +18,7 @@ #include "cmLocalUnixMakefileGenerator3.h" #include "cmMakefile.h" #include "cmMakefileTargetGenerator.h" +#include "cmMessageType.h" #include "cmOutputConverter.h" #include "cmState.h" #include "cmStateTypes.h" @@ -25,6 +26,8 @@ #include "cmSystemTools.h" #include "cmTarget.h" #include "cmTargetDepend.h" +#include "cmTest.h" +#include "cmTestGenerator.h" #include "cmValue.h" #include "cmake.h" @@ -109,6 +112,10 @@ void cmGlobalUnixMakefileGenerator3::Generate() this->ClangTidyExportFixesDirs.clear(); this->ClangTidyExportFixesFiles.clear(); + // Compute the "test_prep/" targets before generating the local makefiles + // so their convenience rules can be written into the top-level Makefile. + this->ComputeTestPrepTargets(); + // first do superclass method this->cmGlobalGenerator::Generate(); @@ -233,6 +240,9 @@ void cmGlobalUnixMakefileGenerator3::WriteMainMakefile2() cm::static_reference_cast(localGen)); } + // Write the internal test_prep/ rules. + this->WriteTestPrepRules(makefileStream, rootLG); + // Write special bottom targets rootLG.WriteSpecialTargetsBottom(makefileStream); } @@ -648,6 +658,9 @@ void cmGlobalUnixMakefileGenerator3::WriteConvenienceRules( } } } + + // Forward the test_prep/ convenience targets to CMakeFiles/Makefile2. + this->WriteTestPrepConvenienceRules(ruleFileStream); } void cmGlobalUnixMakefileGenerator3::WriteConvenienceRules2( @@ -932,6 +945,159 @@ void cmGlobalUnixMakefileGenerator3::AppendCodegenTargetDepends( } } +void cmGlobalUnixMakefileGenerator3::ComputeTestPrepTargets() +{ + this->TestPrepTargets.clear(); + this->TestPrepEnabled = false; + if (this->Makefiles.empty() || + !this->Makefiles.front()->IsOn("CMAKE_TEST_BUILD_DEPENDS")) { + return; + } + this->TestPrepEnabled = true; + + // Map a generator target to its ".dir/all" recursive rule. + auto targetAllRule = [](cmGeneratorTarget* gt) -> std::string { + auto* lg3 = + static_cast(gt->GetLocalGenerator()); + return cmStrCat(lg3->GetRelativeTargetDirectory(gt), "/all"); + }; + + // Collect the dependencies of each test, merging tests that share a name + // across directories (as the Ninja generator does). + for (auto const& lg : this->LocalGenerators) { + for (auto const& tester : lg->GetMakefile()->GetTestGenerators()) { + cmTestGenerator::BuildDependencies deps; + if (!tester->GetBuildDependencies(lg.get(), deps)) { + continue; + } + cmTest* test = tester->GetTest(); + std::string const& testName = test->GetName(); + + // A Makefile target name cannot contain ':'. Such a name is valid for + // add_test() (e.g. a namespaced name) and works with the Ninja + // generator, but cannot be expressed as a Makefile rule. + if (testName.find(':') != std::string::npos) { + test->GetMakefile()->IssueMessage( + MessageType::WARNING, + cmStrCat("Test \"", testName, + "\" has a name containing ':', which cannot be used as a " + "Makefile build target. No \"test_prep/\" target will be " + "generated for it. Use the Ninja generator or rename the " + "test to build its dependencies with a \"test_prep/\" " + "target."), + test->GetBacktrace()); + continue; + } + + std::vector& rules = + this->TestPrepTargets[cmStrCat("test_prep/", testName)]; + + // Target dependencies are filtered to build-system targets by + // GetBuildDependencies. + for (cmGeneratorTarget* dep : deps.Targets) { + rules.push_back(targetAllRule(dep)); + } + + for (cmTestGenerator::BuildDependencies::FileDependency const& file : + deps.Files) { + if (file.Owner) { + // The file is the primary output of one build-system target; build + // that target to produce the file. + rules.push_back(targetAllRule(file.Owner)); + } else if (file.Generated) { + // The file is generated but cannot be attributed to a single owning + // target (e.g. a byproduct or an ambiguous/shared output), so the + // recursive Makefile graph cannot build it from a top-level rule. + test->GetMakefile()->IssueMessage( + MessageType::WARNING, + cmStrCat("Test \"", testName, "\" BUILD_DEPENDS file\n ", + file.Path, + "\nis generated but is not the unique output of a build " + "target, so the \"test_prep/", + testName, + "\" target cannot build it with this generator. Depend " + "on the target that produces it (for example one created " + "with add_custom_target) instead."), + test->GetBacktrace()); + } + // Otherwise the file is not generated by the build (e.g. a source + // file that already exists) and needs no build rule. + } + } + } + + // Sort and de-duplicate each rule list (as the Ninja generator does). + for (auto& entry : this->TestPrepTargets) { + std::vector& rules = entry.second; + std::sort(rules.begin(), rules.end()); + rules.erase(std::unique(rules.begin(), rules.end()), rules.end()); + } +} + +void cmGlobalUnixMakefileGenerator3::WriteTestPrepRules( + std::ostream& makefileStream, cmLocalUnixMakefileGenerator3& rootLG) +{ + if (!this->TestPrepEnabled) { + return; + } + + rootLG.WriteDivider(makefileStream); + makefileStream << "# Targets to build the dependencies of tests.\n\n"; + + std::vector no_commands; + std::vector allDeps; + for (auto const& entry : this->TestPrepTargets) { + std::vector depends = entry.second; + if (depends.empty() && !this->EmptyRuleHackDepends.empty()) { + depends.push_back(this->EmptyRuleHackDepends); + } + rootLG.WriteMakeRule(makefileStream, "Build the dependencies of a test.", + entry.first, depends, no_commands, true); + allDeps.push_back(entry.first); + } + + if (allDeps.empty() && !this->EmptyRuleHackDepends.empty()) { + allDeps.push_back(this->EmptyRuleHackDepends); + } + rootLG.WriteMakeRule(makefileStream, "Build the dependencies of all tests.", + "test_prep/all", allDeps, no_commands, true); +} + +void cmGlobalUnixMakefileGenerator3::WriteTestPrepConvenienceRules( + std::ostream& ruleFileStream) +{ + if (!this->TestPrepEnabled) { + return; + } + + auto& lg = cm::static_reference_cast( + this->LocalGenerators[0]); + + bool regenerate = !this->GlobalSettingIsOn("CMAKE_SUPPRESS_REGENERATION"); + std::string const makefile2 = "CMakeFiles/Makefile2"; + + std::vector depends; + std::vector commands; + auto writeForward = [&](std::string const& prepName) { + depends.clear(); + if (regenerate) { + depends.emplace_back("cmake_check_build_system"); + } + commands.clear(); + commands.push_back(lg.GetRecursiveMakeCall(makefile2, prepName)); + lg.WriteMakeRule(ruleFileStream, "Build the dependencies of a test.", + prepName, depends, commands, true); + }; + + lg.WriteDivider(ruleFileStream); + ruleFileStream << "# Convenience rules to build test dependencies.\n\n"; + + for (auto const& entry : this->TestPrepTargets) { + writeForward(entry.first); + } + writeForward("test_prep/all"); +} + void cmGlobalUnixMakefileGenerator3::WriteHelpRule( std::ostream& ruleFileStream, cmLocalUnixMakefileGenerator3* lg) { diff --git a/Source/cmGlobalUnixMakefileGenerator3.h b/Source/cmGlobalUnixMakefileGenerator3.h index 5dfdbd0a30..df48c384cf 100644 --- a/Source/cmGlobalUnixMakefileGenerator3.h +++ b/Source/cmGlobalUnixMakefileGenerator3.h @@ -226,6 +226,21 @@ protected: void AppendCodegenTargetDepends(std::vector& depends, cmGeneratorTarget* target); + // Compute the "test_prep/" build targets requested via the + // CMAKE_TEST_BUILD_DEPENDS variable. Each test's dependencies are + // resolved to the build-system targets that produce them so that the + // recursive Makefile graph can build them. + void ComputeTestPrepTargets(); + + // Write the internal "test_prep/" and "test_prep/all" rules into + // CMakeFiles/Makefile2. + void WriteTestPrepRules(std::ostream& makefileStream, + cmLocalUnixMakefileGenerator3& rootLG); + + // Write the top-level "test_prep/" convenience rules that forward + // into CMakeFiles/Makefile2. + void WriteTestPrepConvenienceRules(std::ostream& ruleFileStream); + // Target name hooks for superclass. char const* GetAllTargetName() const override { return "all"; } char const* GetInstallTargetName() const override { return "install"; } @@ -298,4 +313,10 @@ private: cmStateSnapshot::StrictWeakOrder> DirectoryTargetsMap; void InitializeProgressMarks() override; + + // Ordered map of "test_prep/" -> the ".dir/all" recursive + // rules that must be built to prepare the named test. Populated by + // ComputeTestPrepTargets when CMAKE_TEST_BUILD_DEPENDS is enabled. + std::map> TestPrepTargets; + bool TestPrepEnabled = false; }; diff --git a/Source/cmTestGenerator.cxx b/Source/cmTestGenerator.cxx index 5255023347..d884cbf879 100644 --- a/Source/cmTestGenerator.cxx +++ b/Source/cmTestGenerator.cxx @@ -64,6 +64,27 @@ std::string TestName(cmTest* test) return name; } +// Whether a path is produced by the build (a custom-command output or +// byproduct) rather than a pre-existing file. The output-to-source map +// records every generated path regardless of which target, if any, builds it. +bool fileIsGenerated(cmGlobalGenerator* gg, std::string const& file) +{ + std::string const collapsed = cmSystemTools::CollapseFullPath(file); + for (auto const& lg : gg->GetLocalGenerators()) { + cmSourcesWithOutput so = lg->GetSourcesWithOutput(collapsed); + if (so.Source || so.Target) { + return true; + } + if (file != collapsed) { + so = lg->GetSourcesWithOutput(file); + if (so.Source || so.Target) { + return true; + } + } + } + return false; +} + } // End: anonymous namespace cmTestGenerator::cmTestGenerator( @@ -135,7 +156,12 @@ bool cmTestGenerator::GetBuildDependencies(cmLocalGenerator* lg, } cmGeneratorTarget* depTarget = lg->FindGeneratorTargetToUse(depName); if (!depTarget) { - info.Files.push_back(depName); + cmGlobalGenerator* gg = lg->GetGlobalGenerator(); + BuildDependencies::FileDependency file; + file.Path = depName; + file.Owner = gg->FindOutputOwningTarget(depName); + file.Generated = fileIsGenerated(gg, depName); + info.Files.push_back(std::move(file)); continue; } if (depTarget->IsImported()) { @@ -149,8 +175,11 @@ bool cmTestGenerator::GetBuildDependencies(cmLocalGenerator* lg, dependencies.insert(depTarget); } - info.Targets.insert(info.Targets.end(), dependencies.begin(), - dependencies.end()); + for (cmGeneratorTarget* gt : dependencies) { + if (gt->IsInBuildSystem()) { + info.Targets.push_back(gt); + } + } return true; } diff --git a/Source/cmTestGenerator.h b/Source/cmTestGenerator.h index fe65a54e1a..278db2eed0 100644 --- a/Source/cmTestGenerator.h +++ b/Source/cmTestGenerator.h @@ -26,8 +26,21 @@ class cmTestGenerator : public cmScriptGenerator public: struct BuildDependencies { + // A file dependency together with how the build produces it. + struct FileDependency + { + std::string Path; + // The single build-system target that produces Path as a primary + // custom-command output, or nullptr if there is no such unique target. + cmGeneratorTarget* Owner = nullptr; + // Whether Path is produced by the build at all (output or byproduct). + bool Generated = false; + }; + // Build-system targets the test depends on (filtered to targets that are + // part of the build system). std::vector Targets; - std::vector Files; + // BUILD_DEPENDS entries that did not name a target. + std::vector Files; }; cmTestGenerator(cmTest* test, diff --git a/Tests/RunCMake/add_test/RunCMakeTest.cmake b/Tests/RunCMake/add_test/RunCMakeTest.cmake index 79ac4593a9..7aa406f866 100644 --- a/Tests/RunCMake/add_test/RunCMakeTest.cmake +++ b/Tests/RunCMake/add_test/RunCMakeTest.cmake @@ -71,24 +71,57 @@ function(run_testdependency_case CASE_NAME EXPECT_PRESENT) unset(RunCMake_TEST_NO_CLEAN) endfunction() -if(RunCMake_GENERATOR MATCHES "Ninja|FASTBuild") +if(RunCMake_GENERATOR MATCHES "Ninja|FASTBuild|Makefiles") block() + if(RunCMake_GENERATOR_IS_MULTI_CONFIG) + set(TestDependency_BUILD_CONFIG_ARG --config Debug) + else() + set(TestDependency_BUILD_CONFIG_ARG) + endif() + run_testdependency_case(DEFAULT FALSE) run_testdependency_case(OFF FALSE) run_cmake(TestDependency-ON-invalid-test-name) + block() + # A header-only INTERFACE library dependency is filtered out by the + # generator-independent dependency resolution, so building the + # test_prep target must not fail on a missing rule. + set(RunCMake_TEST_BINARY_DIR + ${RunCMake_BINARY_DIR}/TestDependency-ON-interface-build) + run_cmake(TestDependency-ON-interface) + set(RunCMake_TEST_NO_CLEAN 1) + set(RunCMake_TEST_OUTPUT_MERGE 1) + run_cmake_command(TestDependency-ON-interface-build + ${CMAKE_COMMAND} --build . ${TestDependency_BUILD_CONFIG_ARG} + --target test_prep/InterfaceTest) + endblock() + + if(RunCMake_GENERATOR MATCHES Makefiles) + # Diagnostics specific to the Makefile generators. + block() + # A ':' in a test name cannot be expressed as a Makefile target. + set(RunCMake_TEST_BINARY_DIR + ${RunCMake_BINARY_DIR}/TestDependency-ON-colon-name-build) + run_cmake(TestDependency-ON-colon-name) + endblock() + block() + # A byproduct file cannot be built through a single owning target. + set(RunCMake_TEST_BINARY_DIR + ${RunCMake_BINARY_DIR}/TestDependency-ON-byproduct-build) + run_cmake(TestDependency-ON-byproduct) + endblock() + block() + # A custom-command output owned by no target cannot be built. + set(RunCMake_TEST_BINARY_DIR + ${RunCMake_BINARY_DIR}/TestDependency-ON-orphan-build) + run_cmake(TestDependency-ON-orphan) + endblock() + endif() + set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/TestDependency-ON-build) run_testdependency_case(ON TRUE) - if(RunCMake_GENERATOR_IS_MULTI_CONFIG) - set(TestDependency_CONFIG Debug) - else() - set(TestDependency_CONFIG "") - endif() - set(TestDependency_BUILD_CONFIG_ARG) - if(TestDependency_CONFIG) - set(TestDependency_BUILD_CONFIG_ARG --config ${TestDependency_CONFIG}) - endif() set(RunCMake_TEST_OUTPUT_MERGE 1) set(RunCMake_TEST_NO_CLEAN 1) run_cmake_command(TestDependency-ON-all diff --git a/Tests/RunCMake/add_test/TestDependency-ON-byproduct-stderr.txt b/Tests/RunCMake/add_test/TestDependency-ON-byproduct-stderr.txt new file mode 100644 index 0000000000..aea5b5f3e6 --- /dev/null +++ b/Tests/RunCMake/add_test/TestDependency-ON-byproduct-stderr.txt @@ -0,0 +1,9 @@ +CMake Warning at TestDependency-ON-byproduct.cmake:[0-9]+ \(add_test\): + Test "ByproductTest" BUILD_DEPENDS file + + .*/TestDependencyByproduct-built.txt + + is generated but is not the unique output of a build target, so the + "test_prep/ByproductTest" target cannot build it with this generator. + Depend on the target that produces it \(for example one created with + add_custom_target\) instead. diff --git a/Tests/RunCMake/add_test/TestDependency-ON-byproduct.cmake b/Tests/RunCMake/add_test/TestDependency-ON-byproduct.cmake new file mode 100644 index 0000000000..c578d4c510 --- /dev/null +++ b/Tests/RunCMake/add_test/TestDependency-ON-byproduct.cmake @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 4.3) +set(CMAKE_TEST_BUILD_DEPENDS ON) + +project(TestDependencyByproduct C) + +enable_testing() + +add_executable(TestDependencyByproductExe main.c) +add_custom_command(TARGET TestDependencyByproductExe POST_BUILD + COMMAND + "${CMAKE_COMMAND}" -E touch + "${CMAKE_CURRENT_BINARY_DIR}/TestDependencyByproduct-built.txt" + BYPRODUCTS + "${CMAKE_CURRENT_BINARY_DIR}/TestDependencyByproduct-built.txt") + +add_test(NAME ByproductTest + COMMAND + "${CMAKE_COMMAND}" -E true + BUILD_DEPENDS + "${CMAKE_CURRENT_BINARY_DIR}/TestDependencyByproduct-built.txt") diff --git a/Tests/RunCMake/add_test/TestDependency-ON-colon-name-stderr.txt b/Tests/RunCMake/add_test/TestDependency-ON-colon-name-stderr.txt new file mode 100644 index 0000000000..56cfd609d0 --- /dev/null +++ b/Tests/RunCMake/add_test/TestDependency-ON-colon-name-stderr.txt @@ -0,0 +1,5 @@ +CMake Warning at TestDependency-ON-colon-name.cmake:[0-9]+ \(add_test\): + Test "Foo::Bar" has a name containing ':', which cannot be used as a + Makefile build target. No "test_prep/" target will be generated for it. + Use the Ninja generator or rename the test to build its dependencies with a + "test_prep/" target. diff --git a/Tests/RunCMake/add_test/TestDependency-ON-colon-name.cmake b/Tests/RunCMake/add_test/TestDependency-ON-colon-name.cmake new file mode 100644 index 0000000000..28b1bbe93f --- /dev/null +++ b/Tests/RunCMake/add_test/TestDependency-ON-colon-name.cmake @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 4.3) +set(CMAKE_TEST_BUILD_DEPENDS ON) + +project(TestDependencyColonName C) + +enable_testing() + +add_custom_target(TestDependencyPrereq + COMMAND + "${CMAKE_COMMAND}" -E touch + "${CMAKE_CURRENT_BINARY_DIR}/TestDependencyPrereq-built.txt" + BYPRODUCTS + "${CMAKE_CURRENT_BINARY_DIR}/TestDependencyPrereq-built.txt" + VERBATIM) + +add_test(NAME "Foo::Bar" + COMMAND + "${CMAKE_COMMAND}" -E true + BUILD_DEPENDS + TestDependencyPrereq) diff --git a/Tests/RunCMake/add_test/TestDependency-ON-interface.cmake b/Tests/RunCMake/add_test/TestDependency-ON-interface.cmake new file mode 100644 index 0000000000..48ccc0abcc --- /dev/null +++ b/Tests/RunCMake/add_test/TestDependency-ON-interface.cmake @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 4.3) +set(CMAKE_TEST_BUILD_DEPENDS ON) + +project(TestDependencyInterface C) + +enable_testing() + +add_executable(TestDependencyInterfaceExe main.c) + +# Header-only INTERFACE library is not part of the build system and must be +# filtered out of the test_prep dependencies so that no dead +# "TestDependencyIface.dir/all" prerequisite is generated. +add_library(TestDependencyIface INTERFACE) + +add_test(NAME InterfaceTest + COMMAND + TestDependencyInterfaceExe + BUILD_DEPENDS + TestDependencyIface) diff --git a/Tests/RunCMake/add_test/TestDependency-ON-orphan-stderr.txt b/Tests/RunCMake/add_test/TestDependency-ON-orphan-stderr.txt new file mode 100644 index 0000000000..d014eb6870 --- /dev/null +++ b/Tests/RunCMake/add_test/TestDependency-ON-orphan-stderr.txt @@ -0,0 +1,9 @@ +CMake Warning at TestDependency-ON-orphan.cmake:[0-9]+ \(add_test\): + Test "OrphanTest" BUILD_DEPENDS file + + .*/TestDependencyOrphan-built.txt + + is generated but is not the unique output of a build target, so the + "test_prep/OrphanTest" target cannot build it with this generator. Depend + on the target that produces it \(for example one created with + add_custom_target\) instead. diff --git a/Tests/RunCMake/add_test/TestDependency-ON-orphan.cmake b/Tests/RunCMake/add_test/TestDependency-ON-orphan.cmake new file mode 100644 index 0000000000..cd52e12ebb --- /dev/null +++ b/Tests/RunCMake/add_test/TestDependency-ON-orphan.cmake @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 4.3) +set(CMAKE_TEST_BUILD_DEPENDS ON) + +project(TestDependencyOrphan C) + +enable_testing() + +# A custom-command output that no target builds. +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/TestDependencyOrphan-built.txt" + COMMAND + "${CMAKE_COMMAND}" -E touch + "${CMAKE_CURRENT_BINARY_DIR}/TestDependencyOrphan-built.txt" + VERBATIM) + +add_test(NAME OrphanTest + COMMAND + "${CMAKE_COMMAND}" -E true + BUILD_DEPENDS + "${CMAKE_CURRENT_BINARY_DIR}/TestDependencyOrphan-built.txt") diff --git a/Tests/RunCMake/add_test/TestDependency-check-targets.cmake b/Tests/RunCMake/add_test/TestDependency-check-targets.cmake index a1518ff1a8..b0c2ae615a 100644 --- a/Tests/RunCMake/add_test/TestDependency-check-targets.cmake +++ b/Tests/RunCMake/add_test/TestDependency-check-targets.cmake @@ -6,14 +6,16 @@ if(NOT DEFINED expect_present) message(FATAL_ERROR "expect_present not set") endif() -file(GLOB ninja_files LIST_DIRECTORIES false +file(GLOB build_files LIST_DIRECTORIES false "${RunCMake_TEST_BINARY_DIR}/*.ninja" "${RunCMake_TEST_BINARY_DIR}/*.ninja.in" - "${RunCMake_TEST_BINARY_DIR}/fbuild.bff") + "${RunCMake_TEST_BINARY_DIR}/fbuild.bff" + "${RunCMake_TEST_BINARY_DIR}/Makefile" + "${RunCMake_TEST_BINARY_DIR}/CMakeFiles/Makefile2") set(found FALSE) -foreach(ninja_file IN LISTS ninja_files) - file(READ "${ninja_file}" content) +foreach(build_file IN LISTS build_files) + file(READ "${build_file}" content) if(content MATCHES "test_prep.all|test_prep.TargetBuildTest") set(found TRUE) break() @@ -21,9 +23,9 @@ foreach(ninja_file IN LISTS ninja_files) endforeach() if(expect_present AND NOT found) - message(FATAL_ERROR "Expected test_prep targets to be present in ninja files.") + message(FATAL_ERROR "Expected test_prep targets to be present in build files.") endif() if(NOT expect_present AND found) - message(FATAL_ERROR "Expected test_prep targets to be absent from ninja files.") + message(FATAL_ERROR "Expected test_prep targets to be absent from build files.") endif()