diff --git a/cmake/modules/CustomCommandWithDepFile.cmake b/cmake/modules/CustomCommandWithDepFile.cmake new file mode 100644 index 0000000000..3a2e7309cc --- /dev/null +++ b/cmake/modules/CustomCommandWithDepFile.cmake @@ -0,0 +1,12 @@ +# Allow to easily create a custom command that uses dep file. +# depfile are a ninja only feature and a hard fail using other generators. + +function(add_custom_command_with_depfile) + cmake_parse_arguments("" "" "DEPFILE" "" ${ARGN}) + + if(_DEPFILE AND "${CMAKE_GENERATOR}" MATCHES "Ninja") + set(_dep_file_arg DEPFILE "${_DEPFILE}") + endif() + + add_custom_command(${_UNPARSED_ARGUMENTS} ${_dep_file_arg}) +endfunction() diff --git a/cmake/modules/NativeExecutable.cmake b/cmake/modules/NativeExecutable.cmake index fc4ef6ff2f..f4756d305f 100644 --- a/cmake/modules/NativeExecutable.cmake +++ b/cmake/modules/NativeExecutable.cmake @@ -1,117 +1,132 @@ # Allow to easily build native executable. # Useful for cross compilation. if(NOT DEFINED __IS_NATIVE_BUILD) # Check if we are in a native build or not. set(__IS_NATIVE_BUILD 0 CACHE INTERNAL "Indicate if this is a native build") endif() if(__IS_NATIVE_BUILD AND CMAKE_CROSSCOMPILING) message(FATAL_ERROR "A native build cannot be cross compiled") endif() # It is imperative that NATIVE_BUILD_DIR be in the cache. set(NATIVE_BUILD_DIR "${CMAKE_BINARY_DIR}/native" CACHE PATH "The path of the native build directory" FORCE) +# Only ninja support depfiles and this is a hard error with other generators +# so we need a nice wrapper to handle this mess. +include(CustomCommandWithDepFile) + function(add_native_executable NAME) if(__IS_NATIVE_BUILD) add_executable(${NAME} EXCLUDE_FROM_ALL ${ARGN}) # Multi-configuration generators (VS, Xcode) append a per-configuration # subdirectory to the specified directory unless the # `RUNTIME_OUTPUT_DIRECTORY` property is defined using a generator # expression. # Since we don't care about the build configuration for native # executables, we can simply drop this subdirectory. # Doing so ensure that the path to the binary can always be retrieved. set_target_properties(${NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/$<0:>" ) else() set(NATIVE_TARGET "${NAME}") file(RELATIVE_PATH RELATIVE_PATH "${CMAKE_BINARY_DIR}" "${CMAKE_CURRENT_BINARY_DIR}") if(RELATIVE_PATH) string(PREPEND NATIVE_TARGET "${RELATIVE_PATH}/") endif() + if("${CMAKE_GENERATOR}" MATCHES "Ninja") + set(TARGET "${NATIVE_TARGET}") + else() + set(TARGET "${NAME}") + endif() + set(NATIVE_BINARY "${NATIVE_BUILD_DIR}/${NATIVE_TARGET}") + set(NATIVE_LINK "${CMAKE_CURRENT_BINARY_DIR}/native-${NAME}") + + configure_file( + "${CMAKE_SOURCE_DIR}/cmake/templates/NativeBuildRunner.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/build_native_${NAME}.sh" + ) # We create a symlink because cmake craps itself if the imported # executable has the same name as the executable itself. # https://cmake.org/pipermail/cmake/2019-May/069480.html - add_custom_command( - OUTPUT "native-${NAME}" + add_custom_command_with_depfile( + OUTPUT "${NATIVE_LINK}" COMMENT "Building native ${NATIVE_TARGET}" - COMMAND ${CMAKE_COMMAND} --build "${NATIVE_BUILD_DIR}" --target "${NAME}" - COMMAND ${CMAKE_COMMAND} -E create_symlink "${NATIVE_BINARY}" "native-${NAME}" - DEPENDS native-cmake-build ${ARGN} + COMMAND "${CMAKE_CURRENT_BINARY_DIR}/build_native_${NAME}.sh" + DEPENDS + native-cmake-build + "${CMAKE_CURRENT_BINARY_DIR}/build_native_${NAME}.sh" + ${ARGN} + DEPFILE "${NATIVE_LINK}.d" VERBATIM USES_TERMINAL ) add_executable(${NAME} IMPORTED GLOBAL) set_target_properties(${NAME} PROPERTIES IMPORTED_LOCATION "${NATIVE_BINARY}") # This obviously cannot depend on a file for some mysterious reasons only # the cmake gods are aware of, so we need a phony custom target. - add_custom_target("build-native-${NAME}" DEPENDS "native-${NAME}") + add_custom_target("build-native-${NAME}" DEPENDS "${NATIVE_LINK}") add_dependencies(${NAME} "build-native-${NAME}") endif() endfunction(add_native_executable) function(native_target_include_directories) if(__IS_NATIVE_BUILD) target_include_directories(${ARGN}) endif() endfunction(native_target_include_directories) function(native_add_cmake_flags) set_property(GLOBAL APPEND PROPERTY _NATIVE_BUILD_CMAKE_FLAGS ${ARGN}) endfunction(native_add_cmake_flags) # Internal machinery function(_gen_native_cmake_target) message(STATUS "Configuring native build in ${NATIVE_BUILD_DIR}") get_property(ARGSLIST GLOBAL PROPERTY _NATIVE_BUILD_CMAKE_FLAGS) list(SORT ARGSLIST) list(REMOVE_DUPLICATES ARGSLIST) list(JOIN ARGSLIST " " ARGS) file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/config") configure_file( "${CMAKE_SOURCE_DIR}/cmake/templates/NativeCmakeRunner.cmake.in" "${CMAKE_BINARY_DIR}/config/run_native_cmake.sh" ) endfunction(_gen_native_cmake_target) function(_gen_native_cmake_hook VAR ACCESS) # When CMAKE_CURRENT_LIST_DIR is set to empty, we execute everything. if("${VAR}" STREQUAL "CMAKE_CURRENT_LIST_DIR" AND "${CMAKE_CURRENT_LIST_DIR}" STREQUAL "" AND "${ACCESS}" STREQUAL "MODIFIED_ACCESS") _gen_native_cmake_target() endif() endfunction(_gen_native_cmake_hook) if(NOT __IS_NATIVE_BUILD AND NOT TARGET native-cmake-build) # Set a hook to execute when everything is set. variable_watch(CMAKE_CURRENT_LIST_DIR _gen_native_cmake_hook) - if("${CMAKE_GENERATOR}" MATCHES "Ninja") - set(cmake_cache_dep_file DEPFILE "${NATIVE_BUILD_DIR}/CMakeFiles/CMakeCache.txt.d") - endif() - - add_custom_command( + add_custom_command_with_depfile( OUTPUT "${NATIVE_BUILD_DIR}/CMakeCache.txt" COMMENT "Preparing native build..." COMMAND "${CMAKE_BINARY_DIR}/config/run_native_cmake.sh" DEPENDS "${CMAKE_BINARY_DIR}/config/run_native_cmake.sh" WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" - ${cmake_cache_dep_file} + DEPFILE "${NATIVE_BUILD_DIR}/CMakeFiles/CMakeCache.txt.d" VERBATIM USES_TERMINAL ) add_custom_target(native-cmake-build DEPENDS "${NATIVE_BUILD_DIR}/CMakeCache.txt") # Add the native directory to the list of file to cleanup. set_property(DIRECTORY "${CMAKE_SOURCE_DIR}" APPEND PROPERTY ADDITIONAL_CLEAN_FILES "${NATIVE_BUILD_DIR}") endif() diff --git a/cmake/templates/NativeBuildRunner.cmake.in b/cmake/templates/NativeBuildRunner.cmake.in new file mode 100755 index 0000000000..e78702f2a2 --- /dev/null +++ b/cmake/templates/NativeBuildRunner.cmake.in @@ -0,0 +1,14 @@ +#!/bin/sh + +cd "${NATIVE_BUILD_DIR}" +"${CMAKE_COMMAND}" --build "${NATIVE_BUILD_DIR}" --target "${TARGET}" +"${CMAKE_COMMAND}" -E create_symlink "${NATIVE_BINARY}" "${NATIVE_LINK}" + +# Ok let's generate a depfile if we can. +if test "x${CMAKE_GENERATOR}" = "xNinja"; then + "${CMAKE_SOURCE_DIR}/cmake/utils/gen-ninja-deps.py" \ + --build-dir "${NATIVE_BUILD_DIR}" \ + --base-dir "${CMAKE_BINARY_DIR}" \ + --ninja "${CMAKE_MAKE_PROGRAM}" \ + "${RELATIVE_PATH}/native-${NAME}" "${TARGET}" > "${NATIVE_LINK}.d" +fi diff --git a/cmake/templates/NativeCmakeRunner.cmake.in b/cmake/templates/NativeCmakeRunner.cmake.in index 5108ee51f7..ed267b6f19 100755 --- a/cmake/templates/NativeCmakeRunner.cmake.in +++ b/cmake/templates/NativeCmakeRunner.cmake.in @@ -1,19 +1,19 @@ #!/bin/sh "${CMAKE_COMMAND}" -G"${CMAKE_GENERATOR}" \ -S "${CMAKE_SOURCE_DIR}" \ -B "${NATIVE_BUILD_DIR}" \ -D__IS_NATIVE_BUILD=1 \ -DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM} \ ${ARGS} # Ok let's generate a depfile if we can. if test "x${CMAKE_GENERATOR}" = "xNinja"; then - "${CMAKE_SOURCE_DIR}/cmake/utils/gen-ninja-deps.py" \ - --build-dir "${NATIVE_BUILD_DIR}" \ - --base-dir "${CMAKE_BINARY_DIR}" \ - --ninja "${CMAKE_MAKE_PROGRAM}" \ - native/CMakeCache.txt build.ninja \ - --extra-deps build.ninja \ - > "${NATIVE_BUILD_DIR}/CMakeFiles/CMakeCache.txt.d" + "${CMAKE_SOURCE_DIR}/cmake/utils/gen-ninja-deps.py" \ + --build-dir "${NATIVE_BUILD_DIR}" \ + --base-dir "${CMAKE_BINARY_DIR}" \ + --ninja "${CMAKE_MAKE_PROGRAM}" \ + native/CMakeCache.txt build.ninja \ + --extra-deps build.ninja \ + > "${NATIVE_BUILD_DIR}/CMakeFiles/CMakeCache.txt.d" fi diff --git a/cmake/utils/gen-ninja-deps.py b/cmake/utils/gen-ninja-deps.py index cc21185e30..57674206ed 100755 --- a/cmake/utils/gen-ninja-deps.py +++ b/cmake/utils/gen-ninja-deps.py @@ -1,181 +1,181 @@ #!/usr/bin/env python3 import subprocess import os import argparse parser = argparse.ArgumentParser(description='Produce a dep file from ninja.') parser.add_argument( '--build-dir', help='The build directory.', required=True) parser.add_argument( '--base-dir', help='The directory for which dependencies are rewriten.', required=True) parser.add_argument('--ninja', help='The ninja executable to use.') parser.add_argument( 'base_target', help="The target from the base's perspective.") parser.add_argument( 'targets', nargs='+', help='The target for which dependencies are extracted.') parser.add_argument( '--extra-deps', nargs='+', help='Extra dependencies.') args = parser.parse_args() build_dir = os.path.abspath(args.build_dir) base_dir = os.path.abspath(args.base_dir) ninja = args.ninja base_target = args.base_target targets = args.targets extra_deps = args.extra_deps # Make sure we operate in the right folder. os.chdir(build_dir) if ninja is None: ninja = subprocess.check_output(['command', '-v', 'ninja'])[:-1] # Construct the set of all targets all_targets = set() doto_targets = set() for t in subprocess.check_output([ninja, '-t', 'targets', 'all']).splitlines(): t, r = t.split(b':') all_targets.add(t) if r[:13] == b' C_COMPILER__' or r[:15] == b' CXX_COMPILER__': doto_targets.add(t) def parse_ninja_query(query): deps = dict() lines = query.splitlines() while len(lines): line = lines.pop(0) if line[0] == ord(' '): continue # We have a new target target = line.split(b':')[0] assert lines.pop(0)[:8] == b' input:' inputs = set() while True: i = lines.pop(0) if i[:4] != b' ': break ''' ninja has 3 types of input: 1. Explicit dependencies, no prefix; 2. Implicit dependencies, | prefix. 3. Order only dependencies, || prefix. Order only dependency do not require the target to be rebuilt and so we ignore them. ''' i = i[4:] if i[0] == ord('|'): if i[1] == ord('|'): # We reached the order only dependencies. break i = i[2:] inputs.add(i) deps[target] = inputs return deps def extract_deps(workset): # Recursively extract the dependencies of the target. deps = dict() while len(workset) > 0: query = subprocess.check_output([ninja, '-t', 'query'] + list(workset)) target_deps = parse_ninja_query(query) deps.update(target_deps) workset = set() for d in target_deps.values(): workset.update(t for t in d if t in all_targets and t not in deps) # Extract build time dependencies. bt_targets = [t for t in deps if t in doto_targets] if len(bt_targets) == 0: return deps ndeps = subprocess.check_output( [ninja, '-t', 'deps'] + bt_targets, stderr=subprocess.DEVNULL) lines = ndeps.splitlines() while len(lines) > 0: line = lines.pop(0) t, m = line.split(b':') if m == b' deps not found': continue inputs = set() while True: i = lines.pop(0) if i == b'': break assert i[:4] == b' ' inputs.add(i[4:]) deps[t] = inputs return deps base_dir = base_dir.encode() def rebase_deps(deps): rebased = dict() cache = dict() def rebase(path): if path in cache: return cache[path] abspath = os.path.abspath(path) newpath = path if path == abspath else os.path.relpath( abspath, base_dir) cache[path] = newpath return newpath for t, s in deps.items(): rebased[rebase(t)] = set(rebase(d) for d in s) return rebased deps = extract_deps(set(targets)) deps = rebase_deps(deps) def dump(deps): for t, d in deps.items(): if len(d) == 0: continue str = t.decode() + ": \\\n " str += " \\\n ".join(sorted(map((lambda x: x.decode()), d))) print(str) # Collapse everything under the base target. -basedeps = set(d.encode() for d in extra_deps) +basedeps = set() if extra_deps is None else set(d.encode() for d in extra_deps) for d in deps.values(): basedeps.update(d) base_target = base_target.encode() basedeps.discard(base_target) dump({base_target: basedeps})