Просмотр исходного кода

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.
Nicolas Despres 9 лет назад
Родитель
Сommit
8a862a4d4b

+ 1 - 0
Help/manual/cmake-variables.7.rst

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

+ 6 - 0
Help/release/dev/ninja-output-path-prefix.rst

@@ -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``.

+ 27 - 0
Help/variable/CMAKE_NINJA_OUTPUT_PATH_PREFIX.rst

@@ -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.

+ 15 - 0
Source/cmAlgorithms.h

@@ -379,4 +379,19 @@ std::reverse_iterator<Iter> cmMakeReverseIterator(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

+ 44 - 3
Source/cmGlobalNinjaGenerator.cxx

@@ -488,6 +488,7 @@ void cmGlobalNinjaGenerator::Generate()
   this->OpenBuildFileStream();
   this->OpenRulesFileStream();
 
+  this->InitOutputPathPrefix();
   this->TargetAll = this->NinjaOutputPath("all");
   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)
 {
   cmLocalNinjaGenerator* ng =
@@ -1136,8 +1154,10 @@ void cmGlobalNinjaGenerator::WriteTargetAll(std::ostream& os)
   this->WritePhonyBuild(os, "The main all target.", outputs,
                         this->AllDependencies);
 
-  cmGlobalNinjaGenerator::WriteDefault(os, outputs,
-                                       "Make the all target the default.");
+  if (!this->HasOutputPathPrefix()) {
+    cmGlobalNinjaGenerator::WriteDefault(os, outputs,
+                                         "Make the all target the default.");
+  }
 }
 
 void cmGlobalNinjaGenerator::WriteTargetRebuildManifest(std::ostream& os)
@@ -1251,7 +1271,28 @@ void cmGlobalNinjaGenerator::WriteTargetHelp(std::ostream& os)
              /*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)
 {
-  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);
 }

+ 5 - 0
Source/cmGlobalNinjaGenerator.h

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

+ 6 - 4
Source/cmNinjaTargetGenerator.cxx

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

+ 7 - 0
Tests/RunCMake/Ninja/CheckNoPrefixSubDir.cmake

@@ -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
+  )

+ 8 - 0
Tests/RunCMake/Ninja/CheckNoPrefixSubDirScript.cmake

@@ -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()

+ 23 - 0
Tests/RunCMake/Ninja/CheckOutput.cmake

@@ -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
+  )

+ 13 - 0
Tests/RunCMake/Ninja/CustomCommandWorkingDirectory.cmake

@@ -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)

+ 5 - 0
Tests/RunCMake/Ninja/Executable.cmake

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

+ 145 - 0
Tests/RunCMake/Ninja/RunCMakeTest.cmake

@@ -32,3 +32,148 @@ function(run_SubDir)
   run_cmake_command(SubDir-build ${CMAKE_COMMAND} --build . --target ${SubDir_all})
 endfunction()
 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)

+ 8 - 0
Tests/RunCMake/Ninja/SharedLib.cmake

@@ -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)

+ 9 - 0
Tests/RunCMake/Ninja/StaticLib.cmake

@@ -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)

+ 8 - 0
Tests/RunCMake/Ninja/SubDirPrefix.cmake

@@ -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)

+ 2 - 0
Tests/RunCMake/Ninja/SubDirPrefix/CMakeLists.txt

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

+ 9 - 0
Tests/RunCMake/Ninja/SubDirPrefix/greeting.c

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

+ 4 - 0
Tests/RunCMake/Ninja/SubDirPrefix/greeting.h

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

+ 17 - 0
Tests/RunCMake/Ninja/TwoLibs.cmake

@@ -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)

+ 9 - 0
Tests/RunCMake/Ninja/greeting.c

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

+ 4 - 0
Tests/RunCMake/Ninja/greeting.h

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

+ 6 - 0
Tests/RunCMake/Ninja/greeting2.c

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

+ 1 - 0
Tests/RunCMake/Ninja/greeting2.h

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

+ 7 - 0
Tests/RunCMake/Ninja/hello.c

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

+ 7 - 0
Tests/RunCMake/Ninja/hello_sub_greeting.c

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

+ 7 - 0
Tests/RunCMake/Ninja/hello_with_greeting.c

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

+ 9 - 0
Tests/RunCMake/Ninja/hello_with_two_greetings.c

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