Ninja: Add option for parallel install

Adds the global property ``INSTALL_PARALLEL`` to enable a parallel install
target for Ninja.

Fixes: #25459
This commit is contained in:
Martin Duffy 2024-05-22 16:08:54 -04:00
parent daeb8fffa2
commit 0e5250e63c
20 changed files with 135 additions and 4 deletions

View File

@ -24,6 +24,13 @@ Builtin Targets
The ``CMAKE_STRIP`` variable will contain the platform's ``strip`` utility, which
removes symbols information from generated binaries.
``install/parallel``
.. versionadded:: 3.30
Created only if the :prop_gbl:`INSTALL_PARALLEL` global property is ``ON``.
Runs the install step for each subdirectory independently and in parallel.
For each subdirectory ``sub/dir`` of the project, additional targets
are generated:

View File

@ -39,6 +39,7 @@ Properties of Global Scope
/prop_gbl/GENERATOR_IS_MULTI_CONFIG
/prop_gbl/GLOBAL_DEPENDS_DEBUG_MODE
/prop_gbl/GLOBAL_DEPENDS_NO_CYCLES
/prop_gbl/INSTALL_PARALLEL
/prop_gbl/IN_TRY_COMPILE
/prop_gbl/JOB_POOLS
/prop_gbl/PACKAGES_FOUND

View File

@ -0,0 +1,23 @@
INSTALL_PARALLEL
----------------
.. versionadded:: 3.30
Enables parallel installation option for the Ninja generator.
When this property is ``ON``, ``install/local`` targets have the
console pool disabled, allowing them to run concurrently.
This property also provides the target ``install/parallel``, which has an
explicit dependency on the ``install/local`` target for each subdirectory,
recursing down the project.
Setting this property has no affect on the behavior of ``cmake --install``.
The install must be invoked by building the ``install/parallel`` target
directly.
Calls to :command:`install(CODE)` or :command:`install(SCRIPT)` might depend
on actions performed by an earlier :command:`install` command in a different
directory such as files installed or variable settings. If the project has
such order-dependent installation logic, parallel installation should be
not be enabled, in order to prevent possible race conditions.

View File

@ -3048,7 +3048,9 @@ void cmGlobalGenerator::AddGlobalTarget_Install(
if (const char* install_local = this->GetInstallLocalTargetName()) {
gti.Name = install_local;
gti.Message = "Installing only the local directory...";
gti.UsesTerminal = true;
gti.UsesTerminal =
!this->GetCMakeInstance()->GetState()->GetGlobalPropertyAsBool(
"INSTALL_PARALLEL");
gti.CommandLines.clear();
cmCustomCommandLine localCmdLine = singleLine;

View File

@ -1835,6 +1835,21 @@ void cmGlobalNinjaGenerator::WriteBuiltinTargets(std::ostream& os)
if (!this->DefaultFileConfig.empty()) {
this->WriteTargetDefault(*this->GetDefaultFileStream());
}
if (this->InstallTargetEnabled &&
this->GetCMakeInstance()->GetState()->GetGlobalPropertyAsBool(
"INSTALL_PARALLEL") &&
!this->Makefiles[0]->IsOn("CMAKE_SKIP_INSTALL_RULES")) {
cmNinjaBuild build("phony");
build.Comment = "Install every subdirectory in parallel";
build.Outputs.emplace_back(this->GetInstallParallelTargetName());
for (auto const& mf : this->Makefiles) {
build.ExplicitDeps.emplace_back(
this->ConvertToNinjaPath(cmStrCat(mf->GetCurrentBinaryDirectory(), "/",
this->GetInstallLocalTargetName())));
}
WriteBuild(os, build);
}
}
void cmGlobalNinjaGenerator::WriteTargetDefault(std::ostream& os)

View File

@ -218,6 +218,10 @@ public:
{
return "install/strip";
}
const char* GetInstallParallelTargetName() const
{
return "install/parallel";
}
const char* GetTestTargetName() const override { return "test"; }
const char* GetPackageTargetName() const override { return "package"; }
const char* GetPackageSourceTargetName() const override

View File

@ -723,10 +723,12 @@ void cmLocalGenerator::GenerateInstallRules()
" set(CMAKE_INSTALL_MANIFEST \"install_manifest.txt\")\n"
"endif()\n"
"\n"
"string(REPLACE \";\" \"\\n\" CMAKE_INSTALL_MANIFEST_CONTENT\n"
"if(NOT CMAKE_INSTALL_LOCAL_ONLY)\n"
" string(REPLACE \";\" \"\\n\" CMAKE_INSTALL_MANIFEST_CONTENT\n"
" \"${CMAKE_INSTALL_MANIFEST_FILES}\")\n"
"file(WRITE \"" << homedir << "/${CMAKE_INSTALL_MANIFEST}\"\n"
" \"${CMAKE_INSTALL_MANIFEST_CONTENT}\")\n";
" file(WRITE \"" << homedir << "/${CMAKE_INSTALL_MANIFEST}\"\n"
" \"${CMAKE_INSTALL_MANIFEST_CONTENT}\")\n"
"endif()\n";
/* clang-format on */
}
}

View File

@ -250,6 +250,7 @@ if(CMAKE_GENERATOR MATCHES "Ninja")
add_RunCMake_test(NinjaPrivateDeps
-DCMAKE_C_OUTPUT_EXTENSION=${CMAKE_C_OUTPUT_EXTENSION}
-DRunCMake_GENERATOR_IS_MULTI_CONFIG=${_isMultiConfig})
add_RunCMake_test(InstallParallel)
endif()
add_RunCMake_test(CTest)

View File

@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.29)
project(${RunCMake_TEST} NONE)
include(${RunCMake_TEST}.cmake)

View File

@ -0,0 +1,17 @@
include(RunCMake)
function(install_test test parallel install_target check_script)
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${test}-install)
set(RunCMake_TEST_OPTIONS -DINSTALL_PARALLEL=${parallel})
if (NOT RunCMake_GENERATOR_IS_MULTI_CONFIG)
list(APPEND RunCMake_TEST_OPTIONS -DCMAKE_BUILD_TYPE=Debug)
endif()
run_cmake(install)
set(RunCMake_TEST_NO_CLEAN 1)
run_cmake_command(${test}-install ${CMAKE_COMMAND} --build . --config Debug -t ${install_target})
set(RunCMake_TEST_COMMAND_WORKING_DIRECTORY ${RunCMake_SOURCE_DIR})
run_cmake_command(verify-parallel ${CMAKE_COMMAND} -P ${check_script} ${RunCMake_TEST_BINARY_DIR}/.ninja_log)
endfunction()
install_test(parallel 1 install/parallel check-parallel.cmake)
install_test(no-parallel 0 install check-single.cmake)

View File

@ -0,0 +1,15 @@
include(read-ninja-install.cmake)
foreach(line ${lines})
string(REPLACE "\t" ";" line ${line})
list(GET line 0 start)
list(GET line 1 end)
list(APPEND start_times ${start})
list(APPEND end_times ${end})
endforeach()
list(GET start_times 1 start_2)
list(GET end_times 0 end_1)
if (NOT start_2 LESS end_1)
message(FATAL_ERROR "Install is not parallel")
endif()

View File

@ -0,0 +1,5 @@
include(read-ninja-install.cmake)
list(LENGTH lines len)
if (NOT ${len} STREQUAL "1")
message(FATAL_ERROR "Expected single installation call")
endif()

View File

@ -0,0 +1,6 @@
install(CODE "message(installing:${CMAKE_CURRENT_SOURCE_DIR})")
if (INSTALL_PARALLEL)
set_property(GLOBAL PROPERTY INSTALL_PARALLEL ON)
endif()
add_subdirectory(subdir-1)
add_subdirectory(subdir-2)

View File

@ -0,0 +1,5 @@
installing:.*
installing:.*
installing:.*
installing:.*
installing:.*

View File

@ -0,0 +1,15 @@
\[1\/5\] Installing only the local directory...
\-\- Install configuration: \"Debug\"
installing:.*
\[2\/5\] Installing only the local directory...
\-\- Install configuration: \"Debug\"
installing:.*
\[3\/5\] Installing only the local directory...
\-\- Install configuration: \"Debug\"
installing:.*
\[4\/5\] Installing only the local directory...
\-\- Install configuration: \"Debug\"
installing:.*
\[5\/5\] Installing only the local directory...
\-\- Install configuration: \"Debug\"
installing:.*

View File

@ -0,0 +1,4 @@
set(ninja_log ${CMAKE_ARGV3})
file(STRINGS ${ninja_log} lines)
list(POP_FRONT lines)
list(FILTER lines INCLUDE REGEX ".*install.*util")

View File

@ -0,0 +1,3 @@
install(CODE "message(installing:${CMAKE_CURRENT_SOURCE_DIR})")
add_subdirectory(subdir-3)
add_subdirectory(subdir-4)

View File

@ -0,0 +1 @@
install(CODE "message(installing:${CMAKE_CURRENT_SOURCE_DIR})")

View File

@ -0,0 +1 @@
install(CODE "message(installing:${CMAKE_CURRENT_SOURCE_DIR})")

View File

@ -0,0 +1 @@
install(CODE "message(installing:${CMAKE_CURRENT_SOURCE_DIR})")