diff --git a/CMakeLists.txt b/CMakeLists.txt index 1b35e58..82eeb81 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,15 +3,21 @@ # Create a configure-time dependency on cmake.toml to improve IDE support configure_file(cmake.toml cmake.toml COPYONLY) +cmake_minimum_required(VERSION 2.8...3.8) + # Regenerate CMakeLists.txt file when necessary -include(cmkr.cmake OPTIONAL RESULT_VARIABLE CMKR_INCLUDE_RESULT) +set(CMKR_ROOT_PROJECT OFF) +if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) + set(CMKR_ROOT_PROJECT ON) -if(CMKR_INCLUDE_RESULT) - cmkr() -endif() + include(cmkr.cmake OPTIONAL RESULT_VARIABLE CMKR_INCLUDE_RESULT) + if(CMKR_INCLUDE_RESULT) + cmkr() + endif() -cmake_minimum_required(VERSION 2.8...3.8) +endif() +# Enable folder support set_property(GLOBAL PROPERTY USE_FOLDERS ON) # Hack to hide a warning during cmkr bootstrapping on Windows diff --git a/cmake/cmkr.cmake b/cmake/cmkr.cmake index 4bf9c40..f475653 100644 --- a/cmake/cmkr.cmake +++ b/cmake/cmkr.cmake @@ -1,20 +1,41 @@ include_guard() +# Change these defaults to point to your infrastructure if desired +set(CMKR_REPO "https://github.com/MoAlyousef/cmkr" CACHE STRING "cmkr git repository") +set(CMKR_TAG "archive_67ac9a43" CACHE STRING "cmkr git tag (this needs to be available forever)") +set(CMKR_EXECUTABLE "" CACHE FILEPATH "cmkr executable") +set(CMKR_SKIP_GENERATION OFF CACHE BOOL "skip automatic cmkr generation") + +# Disable cmkr if generation is disabled +if(CMKR_SKIP_GENERATION) + message(STATUS "[cmkr] Skipping automatic cmkr generation") + macro(cmkr) + endmacro() + return() +endif() + # Disable cmkr if no cmake.toml file is found -if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/cmake.toml) +if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake.toml") message(AUTHOR_WARNING "[cmkr] Not found: ${CMAKE_CURRENT_SOURCE_DIR}/cmake.toml") macro(cmkr) endmacro() return() endif() +# Convert a Windows native path to CMake path +if(CMKR_EXECUTABLE MATCHES "\\\\") + string(REPLACE "\\" "/" CMKR_EXECUTABLE_CMAKE "${CMKR_EXECUTABLE}") + set(CMKR_EXECUTABLE "${CMKR_EXECUTABLE_CMAKE}" CACHE FILEPATH "" FORCE) + unset(CMKR_EXECUTABLE_CMAKE) +endif() + # Helper macro to execute a process (COMMAND_ERROR_IS_FATAL ANY is 3.19 and higher) -macro(cmkr_exec) +function(cmkr_exec) execute_process(COMMAND ${ARGV} RESULT_VARIABLE CMKR_EXEC_RESULT) if(NOT CMKR_EXEC_RESULT EQUAL 0) message(FATAL_ERROR "cmkr_exec(${ARGV}) failed (exit code ${CMKR_EXEC_RESULT})") endif() -endmacro() +endfunction() # Windows-specific hack (CMAKE_EXECUTABLE_PREFIX is not set at the moment) if(WIN32) @@ -24,79 +45,103 @@ else() endif() # Use cached cmkr if found -if(DEFINED CACHE{CMKR_EXECUTABLE} AND EXISTS ${CMKR_EXECUTABLE}) +set(CMKR_CACHED_EXECUTABLE "${CMAKE_CURRENT_BINARY_DIR}/_cmkr/bin/${CMKR_EXECUTABLE_NAME}") +if(CMKR_EXECUTABLE AND EXISTS "${CMKR_EXECUTABLE}") message(VERBOSE "[cmkr] Found cmkr: '${CMKR_EXECUTABLE}'") +elseif(CMKR_EXECUTABLE AND NOT CMKR_EXECUTABLE STREQUAL CMKR_CACHED_EXECUTABLE) + message(FATAL_ERROR "[cmkr] '${CMKR_EXECUTABLE}' not found") else() - if(DEFINED CACHE{CMKR_EXECUTABLE}) - message(VERBOSE "[cmkr] '${CMKR_EXECUTABLE}' not found") + set(CMKR_EXECUTABLE "${CMKR_CACHED_EXECUTABLE}" CACHE FILEPATH "Full path to cmkr executable" FORCE) + message(VERBOSE "[cmkr] Bootstrapping '${CMKR_EXECUTABLE}'") + + message(STATUS "[cmkr] Fetching cmkr...") + set(CMKR_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/_cmkr") + if(EXISTS "${CMKR_DIRECTORY}") + cmkr_exec("${CMAKE_COMMAND}" -E rm -rf "${CMKR_DIRECTORY}") endif() - set(CMKR_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/_cmkr) - set(CMKR_EXECUTABLE ${CMAKE_CURRENT_BINARY_DIR}/_cmkr/bin/${CMKR_EXECUTABLE_NAME} CACHE INTERNAL "Full path to cmkr executable") + find_package(Git QUIET REQUIRED) + cmkr_exec("${GIT_EXECUTABLE}" + clone + --config advice.detachedHead=false + --branch ${CMKR_TAG} + --depth 1 + ${CMKR_REPO} + "${CMKR_DIRECTORY}" + ) + message(STATUS "[cmkr] Building cmkr...") + cmkr_exec("${CMAKE_COMMAND}" + "${CMKR_DIRECTORY}" + "-B${CMKR_DIRECTORY}/build" + "-DCMAKE_BUILD_TYPE=Release" + "-DCMAKE_INSTALL_PREFIX=${CMKR_DIRECTORY}" + ) + cmkr_exec("${CMAKE_COMMAND}" + --build "${CMKR_DIRECTORY}/build" + --config Release + --parallel + ) + cmkr_exec("${CMAKE_COMMAND}" + --install "${CMKR_DIRECTORY}/build" + --config Release + --prefix "${CMKR_DIRECTORY}" + --component cmkr + ) if(NOT EXISTS ${CMKR_EXECUTABLE}) - message(VERBOSE "[cmkr] Bootstrapping '${CMKR_EXECUTABLE}'") - message(STATUS "[cmkr] Fetching cmkr...") - if(EXISTS ${CMKR_DIRECTORY}) - cmkr_exec(${CMAKE_COMMAND} -E rm -rf ${CMKR_DIRECTORY}) - endif() - find_package(Git QUIET REQUIRED) - set(CMKR_REPO "https://github.com/moalyousef/cmkr") - cmkr_exec(${GIT_EXECUTABLE} clone ${CMKR_REPO} ${CMKR_DIRECTORY}) - cmkr_exec(${CMAKE_COMMAND} ${CMKR_DIRECTORY} -B${CMKR_DIRECTORY}/build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${CMKR_DIRECTORY}) - cmkr_exec(${CMAKE_COMMAND} --build ${CMKR_DIRECTORY}/build --config Release --parallel) - cmkr_exec(${CMAKE_COMMAND} --install ${CMKR_DIRECTORY}/build --config Release --prefix ${CMKR_DIRECTORY} --component cmkr) - if(NOT EXISTS ${CMKR_EXECUTABLE}) - message(FATAL_ERROR "[cmkr] Failed to bootstrap '${CMKR_EXECUTABLE}'") - endif() - cmkr_exec(${CMKR_EXECUTABLE} version) - message(STATUS "[cmkr] Bootstrapped ${CMKR_EXECUTABLE}") - else() - message(VERBOSE "[cmkr] Found cmkr: '${CMKR_EXECUTABLE}'") + message(FATAL_ERROR "[cmkr] Failed to bootstrap '${CMKR_EXECUTABLE}'") endif() + cmkr_exec("${CMKR_EXECUTABLE}" version) + message(STATUS "[cmkr] Bootstrapped ${CMKR_EXECUTABLE}") endif() -execute_process(COMMAND ${CMKR_EXECUTABLE} version +execute_process(COMMAND "${CMKR_EXECUTABLE}" version RESULT_VARIABLE CMKR_EXEC_RESULT ) if(NOT CMKR_EXEC_RESULT EQUAL 0) - unset(CMKR_EXECUTABLE CACHE) message(FATAL_ERROR "[cmkr] Failed to get version, try clearing the cache and rebuilding") endif() # This is the macro that contains black magic macro(cmkr) # When this macro is called from the generated file, fake some internal CMake variables - get_source_file_property(CMKR_CURRENT_LIST_FILE ${CMAKE_CURRENT_LIST_FILE} CMKR_CURRENT_LIST_FILE) + get_source_file_property(CMKR_CURRENT_LIST_FILE "${CMAKE_CURRENT_LIST_FILE}" CMKR_CURRENT_LIST_FILE) if(CMKR_CURRENT_LIST_FILE) - set(CMAKE_CURRENT_LIST_FILE ${CMKR_CURRENT_LIST_FILE}) - get_filename_component(CMAKE_CURRENT_LIST_DIR ${CMAKE_CURRENT_LIST_FILE} DIRECTORY) + set(CMAKE_CURRENT_LIST_FILE "${CMKR_CURRENT_LIST_FILE}") + get_filename_component(CMAKE_CURRENT_LIST_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) endif() # File-based include guard (include_guard is not documented to work) - get_source_file_property(CMKR_INCLUDE_GUARD ${CMAKE_CURRENT_LIST_FILE} CMKR_INCLUDE_GUARD) + get_source_file_property(CMKR_INCLUDE_GUARD "${CMAKE_CURRENT_LIST_FILE}" CMKR_INCLUDE_GUARD) if(NOT CMKR_INCLUDE_GUARD) - set_source_files_properties(${CMAKE_CURRENT_LIST_FILE} PROPERTIES CMKR_INCLUDE_GUARD TRUE) + set_source_files_properties("${CMAKE_CURRENT_LIST_FILE}" PROPERTIES CMKR_INCLUDE_GUARD TRUE) + + file(SHA256 "${CMAKE_CURRENT_LIST_FILE}" CMKR_LIST_FILE_SHA256_PRE) # Generate CMakeLists.txt - cmkr_exec(${CMKR_EXECUTABLE} gen - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + cmkr_exec("${CMKR_EXECUTABLE}" gen + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" ) - # Copy the now-generated CMakeLists.txt to CMakerLists.txt - # This is done because you cannot include() a file you are currently in - set(CMKR_TEMP_FILE ${CMAKE_CURRENT_SOURCE_DIR}/CMakerLists.txt) - configure_file(CMakeLists.txt ${CMKR_TEMP_FILE} COPYONLY) - - # Add the macro required for the hack at the start of the cmkr macro - set_source_files_properties(${CMKR_TEMP_FILE} PROPERTIES - CMKR_CURRENT_LIST_FILE ${CMAKE_CURRENT_LIST_FILE} - ) - - # 'Execute' the newly-generated CMakeLists.txt - include(${CMKR_TEMP_FILE}) - - # Delete the generated file - file(REMOVE ${CMKR_TEMP_FILE}) - - # Do not execute the rest of the original CMakeLists.txt - return() + file(SHA256 "${CMAKE_CURRENT_LIST_FILE}" CMKR_LIST_FILE_SHA256_POST) + + if(NOT CMKR_LIST_FILE_SHA256_PRE STREQUAL CMKR_LIST_FILE_SHA256_POST) + # Copy the now-generated CMakeLists.txt to CMakerLists.txt + # This is done because you cannot include() a file you are currently in + set(CMKR_TEMP_FILE "${CMAKE_CURRENT_SOURCE_DIR}/CMakerLists.txt") + configure_file(CMakeLists.txt "${CMKR_TEMP_FILE}" COPYONLY) + + # Add the macro required for the hack at the start of the cmkr macro + set_source_files_properties("${CMKR_TEMP_FILE}" PROPERTIES + CMKR_CURRENT_LIST_FILE "${CMAKE_CURRENT_LIST_FILE}" + ) + + # 'Execute' the newly-generated CMakeLists.txt + include("${CMKR_TEMP_FILE}") + + # Delete the generated file + file(REMOVE "${CMKR_TEMP_FILE}") + + # Do not execute the rest of the original CMakeLists.txt + return() + endif() + # Resume executing the unmodified CMakeLists.txt endif() endmacro() diff --git a/src/cmkrlib/gen.cpp b/src/cmkrlib/gen.cpp index 23b6c23..b2776cd 100644 --- a/src/cmkrlib/gen.cpp +++ b/src/cmkrlib/gen.cpp @@ -177,7 +177,7 @@ struct Command { return result; } - std::string indent(int n) { + static std::string indent(int n) { std::string result; for (int i = 0; i < n; i++) { result += '\t'; @@ -305,8 +305,8 @@ int generate_cmake(const char *path, bool root) { } return Command(ss, indent, command); }; - auto comment = [&ss](const std::string &comment) { - ss << "# " << comment << '\n'; + auto comment = [&ss, &indent](const std::string &comment) { + ss << Command::indent(indent) << "# " << comment << '\n'; return CommandEndl(ss); }; auto endl = [&ss]() { ss << '\n'; }; @@ -340,26 +340,36 @@ int generate_cmake(const char *path, bool root) { // TODO: add link with proper documentation comment("This file was generated automatically by cmkr.").endl(); - comment("Create a configure-time dependency on cmake.toml to improve IDE support"); - cmd("configure_file")("cmake.toml", "cmake.toml", "COPYONLY").endl(); - cmake::CMake cmake(path, false); if (root) { - comment("Regenerate CMakeLists.txt file when necessary"); - cmd("include")(cmake.cmkr_include, "OPTIONAL", "RESULT_VARIABLE", "CMKR_INCLUDE_RESULT").endl(); + cmd("cmake_minimum_required")("VERSION", cmake.cmake_version).endl(); + comment("Regenerate CMakeLists.txt automatically in the root project"); + cmd("set")("CMKR_ROOT_PROJECT", "OFF"); // clang-format off - cmd("if")("CMKR_INCLUDE_RESULT"); - cmd("cmkr")(); - cmd("endif")().endl(); - // clang-format on + cmd("if")("CMAKE_CURRENT_SOURCE_DIR", "STREQUAL", "CMAKE_SOURCE_DIR"); + cmd("set")("CMKR_ROOT_PROJECT", "ON").endl(); - cmd("cmake_minimum_required")("VERSION", cmake.cmake_version).endl(); + comment("Bootstrap cmkr"); + cmd("include")(cmake.cmkr_include, "OPTIONAL", "RESULT_VARIABLE", "CMKR_INCLUDE_RESULT"); + cmd("if")("CMKR_INCLUDE_RESULT"); + cmd("cmkr")(); + cmd("endif")().endl(); - cmd("set_property")("GLOBAL", "PROPERTY", "USE_FOLDERS", "ON").endl(); + comment("Enable folder support"); + cmd("set_property")("GLOBAL", "PROPERTY", "USE_FOLDERS", "ON"); + cmd("endif")().endl(); + // clang-format on } + // clang-format off + comment("Create a configure-time dependency on cmake.toml to improve IDE support"); + cmd("if")("CMKR_ROOT_PROJECT"); + cmd("configure_file")("cmake.toml", "cmake.toml", "COPYONLY"); + cmd("endif")().endl(); + // clang-format on + // TODO: remove support and replace with global compile-features if (!cmake.cppflags.empty()) { ss << "set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} \""; @@ -471,6 +481,7 @@ int generate_cmake(const char *path, bool root) { if (!cmake.subdirs.empty()) { for (const auto &dir : cmake.subdirs) { // clang-format off + comment(dir); cmd("set")("CMKR_CMAKE_FOLDER", "${CMAKE_FOLDER}"); cmd("if")("CMAKE_FOLDER"); cmd("set")("CMAKE_FOLDER", "${CMAKE_FOLDER}/" + dir);