Makefile: Add install_prep targets for minimal builds

Extend the `install_prep/<component>` and `install_prep/all` targets to
the Makefile generators.

Some differences compared to the implementation for Ninja:
* A Makefile rule name cannot contain `:`, so components whose names
  contain one get no preparation target.
* A top-level rule cannot build an individual custom command output, so
  a generated file to be installed is prepared by building the target
  that owns its custom command.
* When a target must be relinked before installation, its `preinstall`
  rule is built after its `all` rule.
* Where the make tool matches target names case-insensitively,
  components whose names differ only in case share a preparation
  target, as done for `test_prep/`.

Co-authored-by: Tyler Yankee <tyler.yankee@kitware.com>
Issue: #27643
This commit is contained in:
Taylor SasserandTyler Yankee authored and Tyler Yankee committed 2026-10-12 12:23:47 -04:00
1 parent 0ea2d28b54
commit 62e4f1577d
14 files changed
+407 -12

No files matched your search

+17
View File
@@ -30,6 +30,23 @@ be created, containing the following targets:
``package``
Runs the package step in the subdirectory, if any.
.. versionadded:: 4.5
If the project has install rules, the top-level makefile additionally
provides the following targets, unless the
:variable:`CMAKE_SKIP_INSTALL_PREP_TARGETS` or
:variable:`CMAKE_SKIP_INSTALL_RULES` variable is enabled:
``install_prep/<component>``
Builds all known build dependencies for the install component named
``<component>`` added by :command:`install`. Components whose names cannot
be represented safely are excluded. See :ref:`Install Preparation Targets`
and :ref:`Target Names`.
``install_prep/all``
Builds all known build dependencies needed for an installation that does
not select a component. See :ref:`Install Preparation Targets`.
.. versionadded:: 4.5
When the :variable:`CMAKE_TEST_BUILD_DEPENDS` variable is enabled, the
+15 -6
View File
@@ -75,7 +75,8 @@ Some convenience targets derive their names from other names in the project:
and :variable:`CMAKE_TEST_BUILD_DEPENDS`.
* The ``install_prep/<component>`` targets use install component names.
A component name may contain the characters allowed in target names above,
plus ``:`` and ``/``. See also :ref:`Install Preparation Targets`.
plus ``:`` and ``/``. The :ref:`Makefile Generators` do not allow ``:``.
See also :ref:`Install Preparation Targets`.
.. _`Executables`:
@@ -1781,8 +1782,9 @@ target properties with a limited set of names:
Preparation Targets
===================
The :ref:`Ninja Generators` provide convenience targets that build only the
prerequisites of a later step, such as running tests or installing.
The :ref:`Ninja Generators` and :ref:`Makefile Generators` provide convenience
targets that build only the prerequisites of a later step, such as running
tests or installing.
.. _`Install Preparation Targets`:
@@ -1852,11 +1854,17 @@ and custom targets. How such a file is prepared depends on how it is produced:
* A file that is the output of a custom command attached to exactly one
target is prepared by running that custom command. The target which
installs the file (or the file set to which the file belongs), if any, is
not built.
not built. The :ref:`Makefile Generators` cannot depend on individual
files, so they build the target owning the custom command instead.
* Any other generated file, such as a byproduct or the output of a custom
command attached to more than one target, is prepared by depending on the
file itself, which runs whatever produces it.
file itself, which runs whatever produces it. The
:ref:`Makefile Generators` cannot do so, so they warn. If a target
installs the file (or the file set to which the file belongs), they build
that target instead, which must therefore depend on the producer, e.g.,
through :command:`add_dependencies`. Otherwise, they do not prepare the
file.
For example:
@@ -1869,7 +1877,8 @@ For example:
set_property(TARGET A PROPERTY PUBLIC_HEADER
"${CMAKE_CURRENT_BINARY_DIR}/generated.h")
# Preparation runs gen, the producer of the byproduct.
# Preparation runs gen, or with some generators builds B instead, so B
# must depend on the producer of its byproduct.
add_custom_target(gen COMMAND ...
BYPRODUCTS "${CMAKE_CURRENT_BINARY_DIR}/byproduct.h")
add_library(B b.c)
+22
View File
@@ -3616,6 +3616,28 @@ void cmGlobalGenerator::CheckInstallPrepTargets()
}
}
bool cmGlobalGenerator::CheckInstallPrepGeneratedFile(
cmGeneratorTarget const* target, std::string const& file,
std::set<std::string>& diagnosed)
{
if (this->FindOutputOwningTarget(file)) {
return true;
}
if (diagnosed.insert(file).second) {
target->GetLocalGenerator()->IssueDiagnostic(
cmDiagnostics::CMD_AUTHOR,
cmStrCat("Target \"", target->GetName(), "\" installs file\n ", file,
"\nwhich is generated but is not the unique output of a build "
"target, so \"install_prep/\" targets cannot build it with "
"this generator. They build target \"",
target->GetName(),
"\" instead, which must depend on the target that produces "
"the file (for example via add_dependencies)."),
target->GetBacktrace());
}
return false;
}
bool cmGlobalGenerator::AreInstallPrepTargetsEnabled() const
{
return !this->Makefiles.empty() && this->InstallTargetEnabled &&
+8
View File
@@ -174,6 +174,14 @@ public:
// Generators that write install preparation targets call this once.
void CheckInstallPrepTargets();
// Whether a generated file to be prepared for installation is the unique
// output of a target. Generators that cannot depend on a file with no such
// owner build its installing target instead, and call this to diagnose it
// once per file.
bool CheckInstallPrepGeneratedFile(cmGeneratorTarget const* target,
std::string const& file,
std::set<std::string>& diagnosed);
bool AreInstallPrepTargetsEnabled() const;
/** Check whether the given name matches the current generator. */
+172 -4
View File
@@ -96,6 +96,14 @@ void cmGlobalUnixMakefileGenerator3::ComputeTargetObjectDirectory(
gt->ObjectDirectory = dir;
}
bool cmGlobalUnixMakefileGenerator3::IsInstallPrepComponentRepresentable(
std::string const& component) const
{
// A Makefile rule name cannot contain ':'.
return component.find(':') == std::string::npos &&
this->cmGlobalGenerator::IsInstallPrepComponentRepresentable(component);
}
bool cmGlobalUnixMakefileGenerator3::CanEscapeOctothorpe() const
{
// Make tools that use UNIX-style '/' paths also support '\' escaping.
@@ -118,9 +126,10 @@ void cmGlobalUnixMakefileGenerator3::Generate()
this->TestPrepTargets.clear();
this->TestPrepEnabled = false;
// Compute the "test_prep/" targets before generating the local makefiles
// so they can be emitted in a single pass.
// Compute the "test_prep/" and "install_prep/" targets before generating the
// local makefiles so they can be emitted in a single pass.
this->ComputeTestPrepTargets();
this->ComputeInstallPrepTargets();
// first do superclass method
this->cmGlobalGenerator::Generate();
@@ -246,8 +255,9 @@ void cmGlobalUnixMakefileGenerator3::WriteMainMakefile2()
cm::static_reference_cast<cmLocalUnixMakefileGenerator3>(localGen));
}
// Write the internal test_prep/ rules.
// Write the internal test_prep/ and install_prep/ rules.
this->WriteTestPrepRules(makefileStream, rootLG);
this->WriteInstallPrepRules(makefileStream, rootLG);
// Write special bottom targets
rootLG.WriteSpecialTargetsBottom(makefileStream);
@@ -726,8 +736,10 @@ void cmGlobalUnixMakefileGenerator3::WriteConvenienceRules(
}
}
// Forward the test_prep/ convenience targets to CMakeFiles/Makefile2.
// Forward the test_prep/ and install_prep/ convenience targets to
// CMakeFiles/Makefile2.
this->WriteTestPrepConvenienceRules(ruleFileStream);
this->WriteInstallPrepConvenienceRules(ruleFileStream);
}
void cmGlobalUnixMakefileGenerator3::WriteConvenienceRules2(
@@ -1231,6 +1243,162 @@ void cmGlobalUnixMakefileGenerator3::WriteTestPrepConvenienceRules(
"test_prep/all", depends, commands, true);
}
void cmGlobalUnixMakefileGenerator3::ComputeInstallPrepTargets()
{
this->InstallPrepComponentTargets.clear();
this->InstallPrepAllTargets.clear();
this->InstallPrepOrderedTargets.clear();
this->InstallPrepEnabled = false;
if (!this->AreInstallPrepTargetsEnabled()) {
return;
}
this->InstallPrepEnabled = true;
this->CheckInstallPrepTargets();
auto const& rootLG =
cm::static_reference_cast<cmLocalUnixMakefileGenerator3>(
this->LocalGenerators.front());
std::string const config = rootLG.GetConfigName();
InstallPrepTargets const prep = this->GetInstallPrepTargets(config);
std::set<std::string> unownedGeneratedFiles;
auto appendTargetRules =
[this, &config, &unownedGeneratedFiles](
std::vector<std::string>& rules, cmGeneratorTarget const* gt,
InstallPrepTargets::TargetRequirements requirements) {
auto* lg3 =
static_cast<cmLocalUnixMakefileGenerator3*>(gt->GetLocalGenerator());
std::string const targetDirectory = lg3->GetRelativeTargetDirectory(gt);
bool const needsRelink = gt->NeedRelinkBeforeInstall(config);
if (requirements.InstallArtifact && needsRelink) {
std::string const orderedRule =
cmStrCat(targetDirectory, "/install_prep");
this->InstallPrepOrderedTargets[orderedRule] = {
cmStrCat(targetDirectory, "/all"),
cmStrCat(targetDirectory, "/preinstall")
};
rules.push_back(orderedRule);
return;
}
// A top-level rule cannot build an individual custom command output, so
// build the whole target that owns it. A generated file that is not
// the unique output of a target is attributed to its installing target.
for (std::string const& file : requirements.GeneratedFiles) {
this->CheckInstallPrepGeneratedFile(gt, file, unownedGeneratedFiles);
}
if (requirements.Normal ||
(!requirements.GeneratedFiles.empty() && gt->IsInBuildSystem())) {
rules.push_back(cmStrCat(targetDirectory, "/all"));
}
if (requirements.InstallArtifact) {
rules.push_back(
cmStrCat(targetDirectory, needsRelink ? "/preinstall" : "/all"));
}
};
for (auto const& target : prep.All) {
appendTargetRules(this->InstallPrepAllTargets, target.first,
target.second);
}
for (auto const& entry : prep.Components) {
std::string const name = cmStrCat("install_prep/", entry.first);
// Merge targets whose names differ only in case where the make tool does
// not distinguish them, as for "test_prep/" targets. Each of them then
// prepares a superset of what its installation needs. Components are
// visited in order, so the first spelling names the target.
std::string const key = this->TargetNamesAreCaseInsensitive()
? cmSystemTools::LowerCase(name)
: name;
std::vector<std::string>* rules = &this->InstallPrepAllTargets;
if (key != "install_prep/all") {
InstallPrepTarget& prepTarget = this->InstallPrepComponentTargets[key];
if (prepTarget.Name.empty()) {
prepTarget.Name = name;
}
rules = &prepTarget.Rules;
}
for (auto const& target : entry.second) {
appendTargetRules(*rules, target.first, target.second);
}
}
auto sortAndRemoveDuplicates = [](std::vector<std::string>& rules) {
std::sort(rules.begin(), rules.end());
rules.erase(std::unique(rules.begin(), rules.end()), rules.end());
};
sortAndRemoveDuplicates(this->InstallPrepAllTargets);
for (auto& entry : this->InstallPrepComponentTargets) {
sortAndRemoveDuplicates(entry.second.Rules);
}
}
void cmGlobalUnixMakefileGenerator3::WriteInstallPrepRules(
std::ostream& makefileStream, cmLocalUnixMakefileGenerator3& rootLG)
{
if (!this->InstallPrepEnabled) {
return;
}
std::vector<std::string> noCommands;
std::string const makefile2 = "CMakeFiles/Makefile2";
for (auto const& ordered : this->InstallPrepOrderedTargets) {
std::vector<std::string> depends{ ordered.second.NormalRule };
std::vector<std::string> commands{ rootLG.GetRecursiveMakeCall(
makefile2, ordered.second.PreinstallRule) };
rootLG.WriteMakeRule(makefileStream, nullptr, ordered.first, depends,
commands, true);
}
for (auto const& entry : this->InstallPrepComponentTargets) {
std::vector<std::string> depends = entry.second.Rules;
if (depends.empty() && !this->EmptyRuleHackDepends.empty()) {
depends.push_back(this->EmptyRuleHackDepends);
}
rootLG.WriteMakeRule(makefileStream, nullptr, entry.second.Name, depends,
noCommands, true);
}
std::vector<std::string> allDepends = this->InstallPrepAllTargets;
if (allDepends.empty() && !this->EmptyRuleHackDepends.empty()) {
allDepends.push_back(this->EmptyRuleHackDepends);
}
rootLG.WriteMakeRule(makefileStream, nullptr, "install_prep/all", allDepends,
noCommands, true);
}
void cmGlobalUnixMakefileGenerator3::WriteInstallPrepConvenienceRules(
std::ostream& ruleFileStream)
{
if (!this->InstallPrepEnabled) {
return;
}
auto& lg = cm::static_reference_cast<cmLocalUnixMakefileGenerator3>(
this->LocalGenerators[0]);
bool regenerate = !this->GlobalSettingIsOn("CMAKE_SUPPRESS_REGENERATION");
std::string const makefile2 = "CMakeFiles/Makefile2";
std::vector<std::string> depends;
std::vector<std::string> 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, nullptr, prepName, depends, commands,
true);
};
for (auto const& entry : this->InstallPrepComponentTargets) {
writeForward(entry.second.Name);
}
writeForward("install_prep/all");
}
void cmGlobalUnixMakefileGenerator3::WriteHelpRule(
std::ostream& ruleFileStream, cmLocalUnixMakefileGenerator3* lg)
{
+40
View File
@@ -201,6 +201,9 @@ public:
bool IsIPOSupported() const override { return true; }
bool IsInstallPrepComponentRepresentable(
std::string const& component) const override;
bool SupportsShortObjectNames() const override;
void ComputeTargetObjectDirectory(cmGeneratorTarget* gt) const override;
@@ -248,6 +251,20 @@ protected:
// into CMakeFiles/Makefile2.
void WriteTestPrepConvenienceRules(std::ostream& ruleFileStream);
// Compute the "install_prep/<component>" build targets. Each installation
// component's dependencies are resolved to the build-system targets that
// produce them so that the recursive Makefile graph can build them.
void ComputeInstallPrepTargets();
// Write the internal "install_prep/<component>" and "install_prep/all" rules
// into CMakeFiles/Makefile2.
void WriteInstallPrepRules(std::ostream& makefileStream,
cmLocalUnixMakefileGenerator3& rootLG);
// Write the top-level "install_prep/<component>" convenience rules that
// forward into CMakeFiles/Makefile2.
void WriteInstallPrepConvenienceRules(std::ostream& ruleFileStream);
// Target name hooks for superclass.
char const* GetAllTargetName() const override { return "all"; }
char const* GetInstallTargetName() const override { return "install"; }
@@ -347,4 +364,27 @@ private:
TestPrepTarget& GetTestPrepTarget(std::string const& name);
void AddDirectoryTestPrepTargets(cmLocalGenerator* lg);
bool TestPrepEnabled = false;
// An "install_prep/<component>" target and the "<target>.dir/all"
// recursive rules that must be built to prepare the named installation
// component.
struct InstallPrepTarget
{
std::string Name;
std::vector<std::string> Rules;
};
// Ordered map of "install_prep/" target name -> its rules. Populated by
// ComputeInstallPrepTargets.
std::map<std::string, InstallPrepTarget> InstallPrepComponentTargets;
std::vector<std::string> InstallPrepAllTargets;
struct InstallPrepOrderedTarget
{
std::string NormalRule;
std::string PreinstallRule;
};
std::map<std::string, InstallPrepOrderedTarget> InstallPrepOrderedTargets;
bool InstallPrepEnabled = false;
};
+1 -1
View File
@@ -292,7 +292,7 @@ if(CMAKE_GENERATOR MATCHES "Ninja")
-DRunCMake_GENERATOR_IS_MULTI_CONFIG=${_isMultiConfig})
endif()
add_RunCMake_test(InstallParallel)
if(CMAKE_GENERATOR MATCHES "Ninja")
if(CMAKE_GENERATOR MATCHES "Make|Ninja")
add_RunCMake_test(InstallPrep)
endif()
add_RunCMake_test(CTest)
@@ -0,0 +1,45 @@
^CMake Warning \(author\) at ComponentNames\.cmake:[0-9]+ \(install\):
The install component "Colon:Name" has build dependencies, but its name
cannot be used in a build target name with this generator\. No
"install_prep/" target will be generated for it\. Rename the component to
build its dependencies with an "install_prep/" target\.
Call Stack \(most recent call first\):
CMakeLists\.txt:[0-9]+ \(include\)
This warning is for project developers\. Use -Wno-author to suppress it\.
+
CMake Warning \(author\) at ComponentNames\.cmake:[0-9]+ \(install\):
The install component "Unsafe/\.\./Name" has build dependencies, but its name
cannot be used in a build target name with this generator\. No
"install_prep/" target will be generated for it\. Rename the component to
build its dependencies with an "install_prep/" target\.
Call Stack \(most recent call first\):
CMakeLists\.txt:[0-9]+ \(include\)
This warning is for project developers\. Use -Wno-author to suppress it\.
+
CMake Warning \(author\) at ComponentNames\.cmake:[0-9]+ \(install\):
The install component "Empty//Segment" has build dependencies, but its name
cannot be used in a build target name with this generator\. No
"install_prep/" target will be generated for it\. Rename the component to
build its dependencies with an "install_prep/" target\.
Call Stack \(most recent call first\):
CMakeLists\.txt:[0-9]+ \(include\)
This warning is for project developers\. Use -Wno-author to suppress it\.
+
CMake Warning \(author\) at ComponentNames\.cmake:[0-9]+ \(install\):
The install component "\./Dot" has build dependencies, but its name cannot
be used in a build target name with this generator\. No "install_prep/"
target will be generated for it\. Rename the component to build its
dependencies with an "install_prep/" target\.
Call Stack \(most recent call first\):
CMakeLists\.txt:[0-9]+ \(include\)
This warning is for project developers\. Use -Wno-author to suppress it\.
+
CMake Warning \(author\) at ComponentNames\.cmake:[0-9]+ \(install\):
An install rule in the component named "all" uses EXCLUDE_FROM_ALL and has
build dependencies, but no install_prep/ target builds them:
install_prep/all omits EXCLUDE_FROM_ALL rules, and the "all" component has
no preparation target of its own\. Rename the component or remove
EXCLUDE_FROM_ALL\.
Call Stack \(most recent call first\):
CMakeLists\.txt:[0-9]+ \(include\)
This warning is for project developers\. Use -Wno-author to suppress it\.$
@@ -0,0 +1,13 @@
^CMake Warning \(author\) at GeneratedByDependency\.cmake:[0-9]+ \(add_library\):
Target "generated_headers" installs file
[^
]*/GeneratedByDependency-build/generated\.h
which is generated but is not the unique output of a build target, so
"install_prep/" targets cannot build it with this generator\. They build
target "generated_headers" instead, which must depend on the target that
produces the file \(for example via add_dependencies\)\.
Call Stack \(most recent call first\):
CMakeLists\.txt:[0-9]+ \(include\)
This warning is for project developers\. Use -Wno-author to suppress it\.$
@@ -2,7 +2,14 @@ enable_language(C)
# Generators that can depend on individual files prepare a generated file by
# running only the custom command that produces it.
set(generated_target_source "${CMAKE_CURRENT_LIST_DIR}/neverbuild.c")
#
# The Makefile generators build the whole target instead, so its sources must
# compile there.
if(CMAKE_GENERATOR MATCHES "Make")
set(generated_target_source "${CMAKE_CURRENT_LIST_DIR}/lib.c")
else()
set(generated_target_source "${CMAKE_CURRENT_LIST_DIR}/neverbuild.c")
endif()
# Add a custom command that generates the named file in the current binary
# directory and store its full path in the given variable.
@@ -0,0 +1,11 @@
^CMake Warning \(author\) at GeneratedFilesByproduct\.cmake:[0-9]+ \(install\):
An install rule installs or depends on file
[^
]*/GeneratedFilesByproduct-build/byproduct\.txt
which is generated but is not the unique output of a build target, so
"install_prep/" targets cannot build it with this generator\..*
Call Stack \(most recent call first\):
CMakeLists\.txt:[0-9]+ \(include\)
This warning is for project developers\. Use -Wno-author to suppress it\.$
@@ -0,0 +1,8 @@
check_installed([[^bin;bin/relink$]])
foreach(program IN ITEMS "${RunCMake_TEST_BINARY_DIR}/relink" "${CMAKE_INSTALL_PREFIX}/bin/relink")
execute_process(COMMAND "${program}" RESULT_VARIABLE result)
if(NOT result EQUAL 0)
set(RunCMake_TEST_FAILED "Relinked executable ${program} failed: ${result}")
endif()
endforeach()
@@ -0,0 +1,17 @@
enable_language(C)
# Use different dummy RPATHs and disable binary RPATH editing to force the
# Makefile generator's relink-before-install path.
set(CMAKE_NO_BUILTIN_CHRPATH 1)
add_executable(relink EXCLUDE_FROM_ALL runtime_main.c)
set_target_properties(relink PROPERTIES
BUILD_RPATH foo
INSTALL_RPATH bar
)
install(TARGETS relink
RUNTIME DESTINATION bin
COMPONENT Relink
)
add_library(unit_tests STATIC neverbuild.c)
@@ -61,7 +61,17 @@ macro(configure_install_prep test config)
if(NOT RunCMake_GENERATOR_IS_MULTI_CONFIG)
list(APPEND RunCMake_TEST_OPTIONS -DCMAKE_BUILD_TYPE=${config})
endif()
# Some generators issue additional diagnostics.
set(stderr_variant "")
if(RunCMake_GENERATOR MATCHES "Make")
set(stderr_variant make)
endif()
if(stderr_variant AND
EXISTS "${RunCMake_SOURCE_DIR}/${test}-stderr-${stderr_variant}.txt")
set(RunCMake-stderr-file ${test}-stderr-${stderr_variant}.txt)
endif()
run_cmake(${test})
unset(RunCMake-stderr-file)
set(RunCMake_TEST_NO_CLEAN TRUE)
endmacro()
@@ -130,8 +140,14 @@ block()
configure_install_prep(ComponentNames Debug)
run_cmake_command(ComponentNames-all-build ${CMAKE_COMMAND}
--build . --config Debug --target install_prep/all)
if(RunCMake_GENERATOR MATCHES "Make")
set(RunCMake_TEST_EXPECT_RESULT "[^0]")
set(RunCMake_TEST_OUTPUT_MERGE TRUE)
endif()
run_cmake_command(ComponentNames-colon-build ${CMAKE_COMMAND}
--build . --config Debug --target install_prep/Colon:Name)
unset(RunCMake_TEST_EXPECT_RESULT)
unset(RunCMake_TEST_OUTPUT_MERGE)
run_missing_install_prep(ComponentNames-dotdot install_prep/Unsafe/../Name)
run_missing_install_prep(ComponentNames-empty-segment
install_prep/Empty//Segment)
@@ -181,3 +197,17 @@ endif()
if(CMAKE_HOST_SYSTEM_NAME MATCHES "^(Linux|Darwin)$")
run_install_prep(Namelink install_prep/Namelink Namelink)
endif()
if(RunCMake_GENERATOR MATCHES "Make" AND
CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
block()
configure_install_prep(MakefilePreinstall Debug)
run_cmake_command(MakefilePreinstall-prep ${CMAKE_COMMAND}
--build . --config Debug --target install_prep/Relink)
run_cmake_command(MakefilePreinstall-run
${RunCMake_TEST_BINARY_DIR}/relink)
set(RunCMake-check-file MakefilePreinstall-install-check.cmake)
run_cmake_command(MakefilePreinstall-install ${CMAKE_COMMAND}
--install . --config Debug --component Relink)
endblock()
endif()