Ninja: Support embedding of CMake as subninja project

Add a `CMAKE_NINJA_OUTPUT_PATH_PREFIX` variable.  When it is set, CMake
generates a `build.ninja` file suitable for embedding into another ninja
project potentially generated by an alien generator.
This commit is contained in:
Nicolas Despres 2016-05-14 01:18:20 +02:00 committed by Brad King
parent 038e7716e5
commit 8a862a4d4b
28 changed files with 412 additions and 7 deletions

View File

@ -281,6 +281,7 @@ Variables that Control the Build
/variable/CMAKE_MAP_IMPORTED_CONFIG_CONFIG /variable/CMAKE_MAP_IMPORTED_CONFIG_CONFIG
/variable/CMAKE_MODULE_LINKER_FLAGS_CONFIG /variable/CMAKE_MODULE_LINKER_FLAGS_CONFIG
/variable/CMAKE_MODULE_LINKER_FLAGS /variable/CMAKE_MODULE_LINKER_FLAGS
/variable/CMAKE_NINJA_OUTPUT_PATH_PREFIX
/variable/CMAKE_NO_BUILTIN_CHRPATH /variable/CMAKE_NO_BUILTIN_CHRPATH
/variable/CMAKE_NO_SYSTEM_FROM_IMPORTED /variable/CMAKE_NO_SYSTEM_FROM_IMPORTED
/variable/CMAKE_OSX_ARCHITECTURES /variable/CMAKE_OSX_ARCHITECTURES

View File

@ -0,0 +1,6 @@
ninja-output-path-prefix
------------------------
* The :generator:`Ninja` generator learned to read a new
:variable:`CMAKE_NINJA_OUTPUT_PATH_PREFIX` variable to configure
the generated ``build.ninja`` file for use as a ``subninja``.

View File

@ -0,0 +1,27 @@
CMAKE_NINJA_OUTPUT_PATH_PREFIX
------------------------------
Set output files path prefix for the :generator:`Ninja` generator.
Every output files listed in the generated ``build.ninja`` will be
prefixed by the contents of this variable (a trailing slash is
appended if missing). This is useful when the generated ninja file is
meant to be embedded as a ``subninja`` file into a *super* ninja
project. For example, a ninja build file generated with a command
like::
cd top-build-dir/sub &&
cmake -G Ninja -DCMAKE_NINJA_OUTPUT_PATH_PREFIX=sub/ path/to/source
can be embedded in ``top-build-dir/build.ninja`` with a directive like
this::
subninja sub/build.ninja
The ``auto-regeneration`` rule in ``top-build-dir/build.ninja`` must have an
order-only dependency on ``sub/build.ninja``.
.. note::
When ``CMAKE_NINJA_OUTPUT_PATH_PREFIX`` is set, the project generated
by CMake cannot be used as a standalone project. No default targets
are specified.

View File

@ -379,4 +379,19 @@ std::reverse_iterator<Iter> cmMakeReverseIterator(Iter it)
return std::reverse_iterator<Iter>(it); return std::reverse_iterator<Iter>(it);
} }
inline bool cmHasSuffix(const std::string& str, const std::string& suffix)
{
if (str.size() < suffix.size()) {
return false;
}
return str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0;
}
inline void cmStripSuffixIfExists(std::string& str, const std::string& suffix)
{
if (cmHasSuffix(str, suffix)) {
str.resize(str.size() - suffix.size());
}
}
#endif #endif

View File

@ -488,6 +488,7 @@ void cmGlobalNinjaGenerator::Generate()
this->OpenBuildFileStream(); this->OpenBuildFileStream();
this->OpenRulesFileStream(); this->OpenRulesFileStream();
this->InitOutputPathPrefix();
this->TargetAll = this->NinjaOutputPath("all"); this->TargetAll = this->NinjaOutputPath("all");
this->CMakeCacheFile = this->NinjaOutputPath("CMakeCache.txt"); this->CMakeCacheFile = this->NinjaOutputPath("CMakeCache.txt");
@ -717,6 +718,23 @@ void cmGlobalNinjaGenerator::CloseRulesFileStream()
} }
} }
static void EnsureTrailingSlash(std::string& path)
{
if (path.empty()) {
return;
}
std::string::value_type last = path[path.size() - 1];
#ifdef _WIN32
if (last != '\\') {
path += '\\';
}
#else
if (last != '/') {
path += '/';
}
#endif
}
std::string cmGlobalNinjaGenerator::ConvertToNinjaPath(const std::string& path) std::string cmGlobalNinjaGenerator::ConvertToNinjaPath(const std::string& path)
{ {
cmLocalNinjaGenerator* ng = cmLocalNinjaGenerator* ng =
@ -1136,8 +1154,10 @@ void cmGlobalNinjaGenerator::WriteTargetAll(std::ostream& os)
this->WritePhonyBuild(os, "The main all target.", outputs, this->WritePhonyBuild(os, "The main all target.", outputs,
this->AllDependencies); this->AllDependencies);
cmGlobalNinjaGenerator::WriteDefault(os, outputs, if (!this->HasOutputPathPrefix()) {
"Make the all target the default."); cmGlobalNinjaGenerator::WriteDefault(os, outputs,
"Make the all target the default.");
}
} }
void cmGlobalNinjaGenerator::WriteTargetRebuildManifest(std::ostream& os) void cmGlobalNinjaGenerator::WriteTargetRebuildManifest(std::ostream& os)
@ -1251,7 +1271,28 @@ void cmGlobalNinjaGenerator::WriteTargetHelp(std::ostream& os)
/*variables=*/cmNinjaVars()); /*variables=*/cmNinjaVars());
} }
void cmGlobalNinjaGenerator::InitOutputPathPrefix()
{
this->OutputPathPrefix =
this->LocalGenerators[0]->GetMakefile()->GetSafeDefinition(
"CMAKE_NINJA_OUTPUT_PATH_PREFIX");
EnsureTrailingSlash(this->OutputPathPrefix);
}
std::string cmGlobalNinjaGenerator::NinjaOutputPath(std::string const& path) std::string cmGlobalNinjaGenerator::NinjaOutputPath(std::string const& path)
{ {
return path; if (!this->HasOutputPathPrefix() || cmSystemTools::FileIsFullPath(path)) {
return path;
}
return this->OutputPathPrefix + path;
}
void cmGlobalNinjaGenerator::StripNinjaOutputPathPrefixAsSuffix(
std::string& path)
{
if (path.empty()) {
return;
}
EnsureTrailingSlash(path);
cmStripSuffixIfExists(path, this->OutputPathPrefix);
} }

View File

@ -315,6 +315,8 @@ public:
bool SupportsConsolePool() const; bool SupportsConsolePool() const;
std::string NinjaOutputPath(std::string const& path); std::string NinjaOutputPath(std::string const& path);
bool HasOutputPathPrefix() const { return !this->OutputPathPrefix.empty(); }
void StripNinjaOutputPathPrefixAsSuffix(std::string& path);
protected: protected:
virtual void Generate(); virtual void Generate();
@ -401,6 +403,9 @@ private:
std::string NinjaVersion; std::string NinjaVersion;
private: private:
void InitOutputPathPrefix();
std::string OutputPathPrefix;
std::string TargetAll; std::string TargetAll;
std::string CMakeCacheFile; std::string CMakeCacheFile;
}; };

View File

@ -669,10 +669,12 @@ void cmNinjaTargetGenerator::EnsureDirectoryExists(
if (cmSystemTools::FileIsFullPath(path.c_str())) { if (cmSystemTools::FileIsFullPath(path.c_str())) {
cmSystemTools::MakeDirectory(path.c_str()); cmSystemTools::MakeDirectory(path.c_str());
} else { } else {
const std::string fullPath = std::string(this->GetGlobalGenerator() cmGlobalNinjaGenerator* gg = this->GetGlobalGenerator();
->GetCMakeInstance() std::string fullPath =
->GetHomeOutputDirectory()) + std::string(gg->GetCMakeInstance()->GetHomeOutputDirectory());
"/" + path; // Also ensures their is a trailing slash.
gg->StripNinjaOutputPathPrefixAsSuffix(fullPath);
fullPath += path;
cmSystemTools::MakeDirectory(fullPath.c_str()); cmSystemTools::MakeDirectory(fullPath.c_str());
} }
} }

View File

@ -0,0 +1,7 @@
add_custom_target(check_no_prefix_sub_dir ALL
COMMAND "${CMAKE_COMMAND}"
"-DNINJA_OUTPUT_PATH_PREFIX=${CMAKE_NINJA_OUTPUT_PATH_PREFIX}"
"-DCUR_BIN_DIR=${CMAKE_CURRENT_BINARY_DIR}"
-P "${CMAKE_CURRENT_SOURCE_DIR}/CheckNoPrefixSubDirScript.cmake"
VERBATIM
)

View File

@ -0,0 +1,8 @@
# Check that the prefix sub-directory is not repeated.
if(EXISTS "${CUR_BIN_DIR}/${NINJA_OUTPUT_PATH_PREFIX}")
message(FATAL_ERROR
"no sub directory named after the CMAKE_NINJA_OUTPUT_PATH_PREFIX "
"should be in the binary directory."
)
endif()

View File

@ -0,0 +1,23 @@
# Add rules to check the generated executable works.
set(hello_output "${CMAKE_CURRENT_BINARY_DIR}/hello.output")
add_custom_command(
OUTPUT "${hello_output}"
COMMAND "$<TARGET_FILE:hello>" > "${hello_output}"
DEPENDS hello
VERBATIM
)
if(NOT DEFINED HELLO_OUTPUT_STRING)
set(HELLO_OUTPUT_STRING "Hello world!\n")
endif()
set(hello_output_ref "${CMAKE_CURRENT_BINARY_DIR}/hello.output.ref")
file(WRITE "${hello_output_ref}" "${HELLO_OUTPUT_STRING}")
add_custom_target(check_output ALL
COMMAND "${CMAKE_COMMAND}" -E compare_files
"${hello_output}" "${hello_output_ref}"
DEPENDS "${hello_output}" "${hello_output_ref}"
VERBATIM
)

View File

@ -0,0 +1,13 @@
cmake_minimum_required(VERSION 3.5)
project(hello NONE)
add_custom_command(
OUTPUT hello.copy.c
COMMAND "${CMAKE_COMMAND}" -E copy
"${CMAKE_CURRENT_SOURCE_DIR}/hello.c"
hello.copy.c
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
)
add_custom_target(copy ALL DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/hello.copy.c")
include(CheckNoPrefixSubDir.cmake)

View File

@ -0,0 +1,5 @@
cmake_minimum_required(VERSION 3.5)
project(hello C)
add_executable(hello hello.c)
include(CheckOutput.cmake)
include(CheckNoPrefixSubDir.cmake)

View File

@ -32,3 +32,148 @@ function(run_SubDir)
run_cmake_command(SubDir-build ${CMAKE_COMMAND} --build . --target ${SubDir_all}) run_cmake_command(SubDir-build ${CMAKE_COMMAND} --build . --target ${SubDir_all})
endfunction() endfunction()
run_SubDir() run_SubDir()
function(run_ninja dir)
execute_process(
COMMAND "${RunCMake_MAKE_PROGRAM}"
WORKING_DIRECTORY "${dir}"
OUTPUT_VARIABLE ninja_stdout
ERROR_VARIABLE ninja_stderr
RESULT_VARIABLE ninja_result
)
if(NOT ninja_result EQUAL 0)
message(STATUS "
============ beginning of ninja's stdout ============
${ninja_stdout}
=============== end of ninja's stdout ===============
")
message(STATUS "
============ beginning of ninja's stderr ============
${ninja_stderr}
=============== end of ninja's stderr ===============
")
message(FATAL_ERROR
"top ninja build failed exited with status ${ninja_result}")
endif()
endfunction(run_ninja)
function(sleep delay)
execute_process(
COMMAND ${CMAKE_COMMAND} -E sleep ${delay}
RESULT_VARIABLE result
)
if(NOT result EQUAL 0)
message(FATAL_ERROR "failed to sleep for ${delay} second.")
endif()
endfunction(sleep)
function(touch path)
execute_process(
COMMAND ${CMAKE_COMMAND} -E touch ${path}
RESULT_VARIABLE result
)
if(NOT result EQUAL 0)
message(FATAL_ERROR "failed to touch main ${path} file.")
endif()
endfunction(touch)
macro(ninja_escape_path path out)
string(REPLACE "\$ " "\$\$" "${out}" "${path}")
string(REPLACE " " "\$ " "${out}" "${${out}}")
string(REPLACE ":" "\$:" "${out}" "${${out}}")
endmacro(ninja_escape_path)
macro(shell_escape string out)
string(REPLACE "\"" "\\\"" "${out}" "${string}")
endmacro(shell_escape)
function(run_sub_cmake test ninja_output_path_prefix)
set(top_build_dir "${RunCMake_BINARY_DIR}/${test}-build/")
file(REMOVE_RECURSE "${top_build_dir}")
file(MAKE_DIRECTORY "${top_build_dir}")
ninja_escape_path("${ninja_output_path_prefix}"
escaped_ninja_output_path_prefix)
# Generate top build ninja file.
set(top_build_ninja "${top_build_dir}/build.ninja")
shell_escape("${top_build_ninja}" escaped_top_build_ninja)
set(build_ninja_dep "${top_build_dir}/build_ninja_dep")
ninja_escape_path("${build_ninja_dep}" escaped_build_ninja_dep)
shell_escape("${CMAKE_COMMAND}" escaped_CMAKE_COMMAND)
file(WRITE "${build_ninja_dep}" "fake dependency of top build.ninja file\n")
if(WIN32)
set(cmd_prefix "cmd.exe /C \"")
set(cmd_suffix "\"")
else()
set(cmd_prefix "")
set(cmd_suffix "")
endif()
file(WRITE "${top_build_ninja}" "\
subninja ${escaped_ninja_output_path_prefix}/build.ninja
default ${escaped_ninja_output_path_prefix}/all
# Sleep for 1 second before to regenerate to make sure the timestamp of
# the top build.ninja will be strictly greater than the timestamp of the
# sub/build.ninja file. We assume the system as 1 sec timestamp resolution.
rule RERUN
command = ${cmd_prefix}\"${escaped_CMAKE_COMMAND}\" -E sleep 1 && \"${escaped_CMAKE_COMMAND}\" -E touch \"${escaped_top_build_ninja}\"${cmd_suffix}
description = Testing regeneration
generator = 1
build build.ninja: RERUN ${escaped_build_ninja_dep} || ${escaped_ninja_output_path_prefix}/build.ninja
pool = console
")
# Run sub cmake project.
set(RunCMake_TEST_OPTIONS "-DCMAKE_NINJA_OUTPUT_PATH_PREFIX=${ninja_output_path_prefix}")
set(RunCMake_TEST_BINARY_DIR "${top_build_dir}/${ninja_output_path_prefix}")
run_cmake(${test})
# Check there is no 'default' statement in Ninja file generated by CMake.
set(sub_build_ninja "${RunCMake_TEST_BINARY_DIR}/build.ninja")
file(READ "${sub_build_ninja}" sub_build_ninja_file)
if(sub_build_ninja_file MATCHES "\ndefault [^\n][^\n]*all\n")
message(FATAL_ERROR
"unexpected 'default' statement found in '${sub_build_ninja}'")
endif()
# Run ninja from the top build directory.
run_ninja("${top_build_dir}")
# Test regeneration rules run in order.
set(main_cmakelists "${RunCMake_SOURCE_DIR}/CMakeLists.txt")
sleep(1) # Assume the system as 1 sec timestamp resolution.
touch("${main_cmakelists}")
touch("${build_ninja_dep}")
run_ninja("${top_build_dir}")
file(TIMESTAMP "${main_cmakelists}" mtime_main_cmakelists UTC)
file(TIMESTAMP "${sub_build_ninja}" mtime_sub_build_ninja UTC)
file(TIMESTAMP "${top_build_ninja}" mtime_top_build_ninja UTC)
# Check sub build.ninja is regenerated.
if(mtime_main_cmakelists STRGREATER mtime_sub_build_ninja)
message(FATAL_ERROR
"sub build.ninja not regenerated:
CMakeLists.txt = ${mtime_main_cmakelists}
sub/build.ninja = ${mtime_sub_build_ninja}")
endif()
# Check top build.ninja is regenerated after sub build.ninja.
if(NOT mtime_top_build_ninja STRGREATER mtime_sub_build_ninja)
message(FATAL_ERROR
"top build.ninja not regenerated strictly after sub build.ninja:
sub/build.ninja = ${mtime_sub_build_ninja}
build.ninja = ${mtime_top_build_ninja}")
endif()
endfunction(run_sub_cmake)
foreach(ninja_output_path_prefix "sub space" "sub")
run_sub_cmake(Executable "${ninja_output_path_prefix}")
run_sub_cmake(StaticLib "${ninja_output_path_prefix}")
run_sub_cmake(SharedLib "${ninja_output_path_prefix}")
run_sub_cmake(TwoLibs "${ninja_output_path_prefix}")
run_sub_cmake(SubDirPrefix "${ninja_output_path_prefix}")
run_sub_cmake(CustomCommandWorkingDirectory "${ninja_output_path_prefix}")
endforeach(ninja_output_path_prefix)

View File

@ -0,0 +1,8 @@
cmake_minimum_required(VERSION 3.5)
project(hello C)
add_library(greeting SHARED greeting.c)
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
add_executable(hello hello_with_greeting.c)
target_link_libraries(hello greeting)
include(CheckOutput.cmake)
include(CheckNoPrefixSubDir.cmake)

View File

@ -0,0 +1,9 @@
cmake_minimum_required(VERSION 3.5)
project(hello C)
add_definitions(-DGREETING_STATIC)
add_library(greeting STATIC greeting.c)
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
add_executable(hello hello_with_greeting.c)
target_link_libraries(hello greeting)
include(CheckOutput.cmake)
include(CheckNoPrefixSubDir.cmake)

View File

@ -0,0 +1,8 @@
cmake_minimum_required(VERSION 3.5)
project(hello C)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/bin")
add_subdirectory(SubDirPrefix)
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
add_executable(hello hello_sub_greeting.c)
target_link_libraries(hello greeting)
include(CheckOutput.cmake)

View File

@ -0,0 +1,2 @@
add_library(greeting SHARED greeting.c)
include_directories(${CMAKE_CURRENT_SOURCE_DIR})

View File

@ -0,0 +1,9 @@
#include <stdio.h>
#if defined(_WIN32) && !defined(GREETING_STATIC)
__declspec(dllexport)
#endif
void greeting(void)
{
printf("Hello world!\n");
}

View File

@ -0,0 +1,4 @@
#if defined(_WIN32) && !defined(GREETING_STATIC)
__declspec(dllimport)
#endif
void greeting(void);

View File

@ -0,0 +1,17 @@
cmake_minimum_required(VERSION 3.5)
project(hello C)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib-static")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
add_library(greeting SHARED greeting.c)
add_library(greeting2 STATIC greeting2.c)
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
add_executable(hello hello_with_two_greetings.c)
target_link_libraries(hello greeting greeting2)
set(HELLO_OUTPUT_STRING "Hello world!\nHello world 2!\n")
include(CheckOutput.cmake)
include(CheckNoPrefixSubDir.cmake)

View File

@ -0,0 +1,9 @@
#include <stdio.h>
#if defined(_WIN32) && !defined(GREETING_STATIC)
__declspec(dllexport)
#endif
void greeting(void)
{
printf("Hello world!\n");
}

View File

@ -0,0 +1,4 @@
#if defined(_WIN32) && !defined(GREETING_STATIC)
__declspec(dllimport)
#endif
void greeting(void);

View File

@ -0,0 +1,6 @@
#include <stdio.h>
void greeting2(void)
{
printf("Hello world 2!\n");
}

View File

@ -0,0 +1 @@
void greeting2(void);

View File

@ -0,0 +1,7 @@
#include <stdio.h>
int main(void)
{
printf("Hello world!\n");
return 0;
}

View File

@ -0,0 +1,7 @@
#include <SubDirPrefix/greeting.h>
int main(void)
{
greeting();
return 0;
}

View File

@ -0,0 +1,7 @@
#include <greeting.h>
int main(void)
{
greeting();
return 0;
}

View File

@ -0,0 +1,9 @@
#include <greeting.h>
#include <greeting2.h>
int main(void)
{
greeting();
greeting2();
return 0;
}