GitExternal.cmake 14.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
# Configures an external git repository
#
# Usage:
#  * Automatically reads, parses and updates a .gitexternals file if it only
#    contains lines in the form "# <directory> <giturl> <gittag>".
#    This function parses the file for this pattern and then calls
#    git_external on each found entry. Additionally it provides an
#    update target to bump the tag to the master revision by
#    recreating .gitexternals.
#  * Provides function
#      git_external(<directory> <giturl> <gittag> [VERBOSE,SHALLOW]
#        [RESET <files>])
#    which will check out directory in CMAKE_SOURCE_DIR (if relative)
#    or in the given absolute path using the given repository and tag
#    (commit-ish).
#
# Options which can be supplied to the function:
#  VERBOSE, when present, this option tells the function to output
#    information about what operations are being performed by git on
#    the repo.
#  SHALLOW, when present, causes a shallow clone of depth 1 to be made
#    of the specified repo. This may save considerable memory/bandwidth
#    when only a specific branch of a repo is required and the full history
#    is not required. Note that the SHALLOW option will only work for a branch
#    or tag and cannot be used for an arbitrary SHA.
26 27 28
#  OPTIONAL, when present, this option makes this operation optional.
#    The function will output a warning and return if the repo could not be
#    cloned.
29 30 31 32 33 34 35
#
# Targets:
#  * <directory>-rebase: fetches latest updates and rebases the given external
#    git repository onto it.
#  * rebase: Rebases all git externals, including sub projects
#
# Options (global) which control behaviour:
36
#  COMMON_GIT_EXTERNAL_VERBOSE
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
#    This is a global option which has the same effect as the VERBOSE option,
#    with the difference that output information will be produced for all
#    external repos when set.
#  GIT_EXTERNAL_TAG
#    If set, git external tags referring to a SHA1 (not a branch) will be
#    overwritten by this value.
#
# CMake or environment variables:
#  GITHUB_USER
#    If set, a remote called 'user' is set up for github repositories, pointing
#    to git@github.com:<user>/<project>. Also, this remote is used by default
#    for 'git push'.


if(NOT GIT_FOUND)
  find_package(Git QUIET)
endif()
if(NOT GIT_EXECUTABLE)
  return()
endif()

include(CMakeParseArguments)
59
option(COMMON_GIT_EXTERNAL_VERBOSE "Print git commands as they are executed" OFF)
60 61 62 63 64 65 66

if(NOT GITHUB_USER AND DEFINED ENV{GITHUB_USER})
  set(GITHUB_USER $ENV{GITHUB_USER} CACHE STRING
    "Github user name used to setup remote for 'user' forks")
endif()

macro(GIT_EXTERNAL_MESSAGE msg)
67
  if(COMMON_GIT_EXTERNAL_VERBOSE)
68 69 70 71 72 73 74 75 76 77 78 79
    message(STATUS "${NAME}: ${msg}")
  endif()
endmacro()

# utility function for printing a list with custom separator
function(JOIN VALUES GLUE OUTPUT)
  string (REGEX REPLACE "([^\\]|^);" "\\1${GLUE}" _TMP_STR "${VALUES}")
  string (REGEX REPLACE "[\\](.)" "\\1" _TMP_STR "${_TMP_STR}") #fixes escaping
  set (${OUTPUT} "${_TMP_STR}" PARENT_SCOPE)
endfunction()

function(GIT_EXTERNAL DIR REPO tag)
80
  cmake_parse_arguments(GIT_EXTERNAL_LOCAL "VERBOSE;SHALLOW;OPTIONAL" "" "RESET" ${ARGN})
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
  set(TAG ${tag})
  if(GIT_EXTERNAL_TAG AND "${tag}" MATCHES "^[0-9a-f]+$")
    set(TAG ${GIT_EXTERNAL_TAG})
  endif()

  # check if we had a previous external of the same name
  string(REGEX REPLACE "[:/]" "_" TARGET "${DIR}")
  get_property(OLD_TAG GLOBAL PROPERTY ${TARGET}_GITEXTERNAL_TAG)
  if(OLD_TAG)
    if(NOT OLD_TAG STREQUAL TAG)
      string(REPLACE "${CMAKE_SOURCE_DIR}/" "" PWD
        "${CMAKE_CURRENT_SOURCE_DIR}")
      git_external_message("${DIR}: already configured with ${OLD_TAG}, ignoring requested ${TAG} in ${PWD}")
      return()
    endif()
  else()
    set_property(GLOBAL PROPERTY ${TARGET}_GITEXTERNAL_TAG ${TAG})
  endif()

  if(NOT IS_ABSOLUTE "${DIR}")
    set(DIR "${CMAKE_SOURCE_DIR}/${DIR}")
  endif()
  get_filename_component(NAME "${DIR}" NAME)
  get_filename_component(GIT_EXTERNAL_DIR "${DIR}/.." ABSOLUTE)

  if(NOT EXISTS "${DIR}")
    # clone
    set(_clone_options --recursive)
    if(GIT_EXTERNAL_LOCAL_SHALLOW)
      list(APPEND _clone_options --depth 1 --branch ${TAG})
    else()
      set(_msg_tag "[${TAG}]")
    endif()
    JOIN("${_clone_options}" " " _msg_text)
    message(STATUS "git clone ${_msg_text} ${REPO} ${DIR} ${_msg_tag}")
    execute_process(
      COMMAND "${GIT_EXECUTABLE}" clone ${_clone_options} ${REPO} ${DIR}
      RESULT_VARIABLE nok ERROR_VARIABLE error
      WORKING_DIRECTORY "${GIT_EXTERNAL_DIR}")
    if(nok)
121 122 123 124 125 126
      if(GIT_EXTERNAL_LOCAL_OPTIONAL)
        message(STATUS "${DIR} clone failed: ${error}\n")
        return()
      else()
        message(FATAL_ERROR "${DIR} clone failed: ${error}\n")
      endif()
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
    endif()

    # checkout requested tag
    if(NOT GIT_EXTERNAL_LOCAL_SHALLOW)
      execute_process(
        COMMAND "${GIT_EXECUTABLE}" checkout -q "${TAG}"
        RESULT_VARIABLE nok ERROR_VARIABLE error
        WORKING_DIRECTORY "${DIR}")
      if(nok)
        message(FATAL_ERROR "git checkout ${TAG} in ${DIR} failed: ${error}\n")
      endif()
    endif()

    # checkout requested tag
    execute_process(
      COMMAND "${GIT_EXECUTABLE}" checkout -q "${TAG}"
      RESULT_VARIABLE nok ERROR_VARIABLE error
      WORKING_DIRECTORY "${DIR}")
    if(nok)
      message(FATAL_ERROR "git checkout ${TAG} in ${DIR} failed: ${error}\n")
    endif()
  endif()

  # set up "user" remote for github forks and make it default for 'git push'
  if(GITHUB_USER AND REPO MATCHES ".*github.com.*")
    string(REGEX REPLACE ".*(github.com)[\\/:]().*(\\/.*)" "git@\\1:\\2${GITHUB_USER}\\3"
      GIT_EXTERNAL_USER_REPO ${REPO})
    execute_process(
      COMMAND "${GIT_EXECUTABLE}" remote add user ${GIT_EXTERNAL_USER_REPO}
      OUTPUT_QUIET ERROR_QUIET WORKING_DIRECTORY "${DIR}")
    execute_process(
      COMMAND "${GIT_EXECUTABLE}" config remote.pushdefault user
      OUTPUT_QUIET ERROR_QUIET WORKING_DIRECTORY "${DIR}")
  endif()

  if(COMMON_SOURCE_DIR)
    file(RELATIVE_PATH __dir ${COMMON_SOURCE_DIR} ${DIR})
  else()
    file(RELATIVE_PATH __dir ${CMAKE_SOURCE_DIR} ${DIR})
  endif()
  string(REGEX REPLACE "[:/\\.]" "-" __target "${__dir}")
  if(TARGET ${__target}-rebase)
    return()
  endif()

  set(__rebase_cmake "${CMAKE_CURRENT_BINARY_DIR}/${__target}-rebase.cmake")
  file(WRITE ${__rebase_cmake}
    "if(NOT IS_DIRECTORY \"${DIR}/.git\")\n"
    "  message(FATAL_ERROR \"Can't update git external ${__dir}: Not a git repository\")\n"
    "endif()\n"
    # check if we are already on the requested tag (nothing to do)
    "execute_process(COMMAND \"${GIT_EXECUTABLE}\" rev-parse --short HEAD\n"
    "  OUTPUT_VARIABLE currentref OUTPUT_STRIP_TRAILING_WHITESPACE\n"
    "  WORKING_DIRECTORY \"${DIR}\")\n"
    "if(currentref STREQUAL ${TAG}) # nothing to do\n"
    "  return()\n"
    "endif()\n"
    "\n"
    # reset generated files
    "foreach(GIT_EXTERNAL_RESET_FILE ${GIT_EXTERNAL_RESET})\n"
    "  execute_process(\n"
    "    COMMAND \"${GIT_EXECUTABLE}\" reset -q \"\${GIT_EXTERNAL_RESET_FILE}\"\n"
    "    ERROR_QUIET OUTPUT_QUIET\n"
    "    WORKING_DIRECTORY \"${DIR}\")\n"
    "  execute_process(\n"
    "    COMMAND \"${GIT_EXECUTABLE}\" checkout -q -- \"${GIT_EXTERNAL_RESET_FILE}\"\n"
    "    ERROR_QUIET OUTPUT_QUIET\n"
    "    WORKING_DIRECTORY \"${DIR}\")\n"
    "endforeach()\n"
    "\n"
    # fetch updates
    "execute_process(COMMAND \"${GIT_EXECUTABLE}\" fetch origin -q\n"
    "  RESULT_VARIABLE nok ERROR_VARIABLE error\n"
    "  WORKING_DIRECTORY \"${DIR}\")\n"
    "if(nok)\n"
    "  message(FATAL_ERROR \"Fetch for ${__dir} failed:\n   \${error}\")\n"
    "endif()\n"
    "\n"
  )
  if("${TAG}" MATCHES "^[0-9a-f]+$")
    # requested TAG is a SHA1, just switch to it
    file(APPEND ${__rebase_cmake}
      # checkout requested tag
      "execute_process(\n"
      "  COMMAND \"${GIT_EXECUTABLE}\" checkout -q \"${TAG}\"\n"
      "  RESULT_VARIABLE nok ERROR_VARIABLE error\n"
      "  WORKING_DIRECTORY \"${DIR}\")\n"
      "if(nok)\n"
      "  message(FATAL_ERROR \"git checkout ${TAG} in ${__dir} failed: ${error}\n\")\n"
      "endif()\n"
    )
  else()
    # requested TAG is a branch
    file(APPEND ${__rebase_cmake}
      # switch to requested branch
      "execute_process(\n"
      "  COMMAND \"${GIT_EXECUTABLE}\" checkout -q \"${TAG}\"\n"
      "  OUTPUT_QUIET ERROR_QUIET WORKING_DIRECTORY \"${DIR}\")\n"
      # try to rebase it
      "execute_process(COMMAND \"${GIT_EXECUTABLE}\" rebase FETCH_HEAD\n"
      "  RESULT_VARIABLE nok ERROR_VARIABLE error OUTPUT_VARIABLE output\n"
      "  WORKING_DIRECTORY \"${DIR}\")\n"
      "if(nok)\n"
      "  execute_process(COMMAND \"${GIT_EXECUTABLE}\" rebase --abort\n"
      "    WORKING_DIRECTORY \"${DIR}\" ERROR_QUIET OUTPUT_QUIET)\n"
      "  message(FATAL_ERROR \"Rebase ${__dir} failed:\n\${output}\${error}\")\n"
      "endif()\n"
    )
  endif()

  if(NOT GIT_EXTERNAL_SCRIPT_MODE)
    add_custom_target(${__target}-rebase
      COMMAND ${CMAKE_COMMAND} -P ${__rebase_cmake}
      COMMENT "Rebasing ${__dir} [${TAG}]")
    set_target_properties(${__target}-rebase PROPERTIES
      EXCLUDE_FROM_DEFAULT_BUILD ON FOLDER git)
    if(NOT TARGET rebase)
      add_custom_target(rebase)
      set_target_properties(rebase PROPERTIES EXCLUDE_FROM_DEFAULT_BUILD ON)
    endif()
    add_dependencies(rebase ${__target}-rebase)
  endif()
endfunction()

set(GIT_EXTERNALS ${GIT_EXTERNALS_FILE})
if(NOT GIT_EXTERNALS)
  set(GIT_EXTERNALS "${CMAKE_CURRENT_SOURCE_DIR}/.gitexternals")
endif()

if(EXISTS ${GIT_EXTERNALS} AND NOT GIT_EXTERNAL_SCRIPT_MODE)
  include(${GIT_EXTERNALS})
  file(READ ${GIT_EXTERNALS} GIT_EXTERNAL_FILE)
  string(REGEX REPLACE "\n" ";" GIT_EXTERNAL_FILE "${GIT_EXTERNAL_FILE}")
  foreach(LINE ${GIT_EXTERNAL_FILE})
    if(NOT LINE MATCHES "^#.*$")
      message(FATAL_ERROR "${GIT_EXTERNALS} contains non-comment line: ${LINE}")
    endif()
    string(REGEX REPLACE "^#[ ]*(.+[ ]+.+[ ]+.+)$" "\\1" DATA ${LINE})
    if(NOT LINE STREQUAL DATA)
      string(REGEX REPLACE "[ ]+" ";" DATA "${DATA}")
      list(LENGTH DATA DATA_LENGTH)
      if(DATA_LENGTH EQUAL 3)
        list(GET DATA 0 DIR)
        list(GET DATA 1 REPO)
        list(GET DATA 2 TAG)

        # Create a unique, flat name
        string(REPLACE "/" "-" GIT_EXTERNAL_NAME ${DIR}_${PROJECT_NAME})

Pablo Toharia's avatar
Pablo Toharia committed
276
        if(NOT TARGET update-gitexternal-${GIT_EXTERNAL_NAME}) # not done
277 278 279 280 281 282 283
          # pull in identified external
          git_external(${DIR} ${REPO} ${TAG})

          # Create update script and target to bump external spec
          if(NOT TARGET update)
            add_custom_target(update)
          endif()
Pablo Toharia's avatar
Pablo Toharia committed
284 285 286 287 288 289 290
          if(NOT TARGET update-gitexternal)
            add_custom_target(update-gitexternal)
            add_custom_target(flatten-gitexternal)
            add_dependencies(update update-gitexternal)
          endif()
          if(NOT TARGET ${PROJECT_NAME}-flatten-gitexternal)
            add_custom_target(${PROJECT_NAME}-flatten-gitexternal)
291 292 293 294 295 296 297
          endif()

          # Create a unique, flat name
          file(RELATIVE_PATH GIT_EXTERNALS_BASE ${CMAKE_SOURCE_DIR}
            ${GIT_EXTERNALS})
          string(REPLACE "/" "_" GIT_EXTERNAL_TARGET ${GIT_EXTERNALS_BASE})

Pablo Toharia's avatar
Pablo Toharia committed
298
          set(GIT_EXTERNAL_TARGET update-gitexternal-${GIT_EXTERNAL_TARGET})
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
          if(NOT TARGET ${GIT_EXTERNAL_TARGET})
            set(GIT_EXTERNAL_SCRIPT
              "${CMAKE_CURRENT_BINARY_DIR}/${GIT_EXTERNAL_TARGET}.cmake")
            file(WRITE "${GIT_EXTERNAL_SCRIPT}"
              "file(WRITE ${GIT_EXTERNALS} \"# -*- mode: cmake -*-\n\")\n")
            add_custom_target(${GIT_EXTERNAL_TARGET}
              COMMAND "${CMAKE_COMMAND}" -DGIT_EXTERNAL_SCRIPT_MODE=1 -P ${GIT_EXTERNAL_SCRIPT}
              COMMENT "Recreate ${GIT_EXTERNALS_BASE}"
              WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
          endif()

          set(GIT_EXTERNAL_SCRIPT
            "${CMAKE_CURRENT_BINARY_DIR}/gitupdate${GIT_EXTERNAL_NAME}.cmake")
          file(WRITE "${GIT_EXTERNAL_SCRIPT}" "
include(\"${CMAKE_CURRENT_LIST_DIR}/GitExternal.cmake\")
execute_process(COMMAND \"${GIT_EXECUTABLE}\" fetch origin -q
Pablo Toharia's avatar
Pablo Toharia committed
315
  WORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}/${DIR}\")
316 317 318
execute_process(
  COMMAND \"${GIT_EXECUTABLE}\" show-ref --hash=7 refs/remotes/origin/master
  OUTPUT_VARIABLE newref OUTPUT_STRIP_TRAILING_WHITESPACE
Pablo Toharia's avatar
Pablo Toharia committed
319
  WORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}/${DIR}\")
320 321 322 323 324 325
if(newref)
  file(APPEND ${GIT_EXTERNALS} \"# ${DIR} ${REPO} \${newref}\\n\")
  git_external(${DIR} ${REPO} \${newref})
else()
  file(APPEND ${GIT_EXTERNALS} \"# ${DIR} ${REPO} ${TAG}\n\")
endif()")
Pablo Toharia's avatar
Pablo Toharia committed
326
          add_custom_target(update-gitexternal-${GIT_EXTERNAL_NAME}
327 328 329 330
            COMMAND "${CMAKE_COMMAND}" -DGIT_EXTERNAL_SCRIPT_MODE=1 -P ${GIT_EXTERNAL_SCRIPT}
            COMMENT "Update ${REPO} in ${GIT_EXTERNALS_BASE}"
            DEPENDS ${GIT_EXTERNAL_TARGET}
            WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
Pablo Toharia's avatar
Pablo Toharia committed
331 332
          add_dependencies(update-gitexternal
            update-gitexternal-${GIT_EXTERNAL_NAME})
333 334 335 336 337 338 339

          # Flattens a git external repository into its parent repo:
          # * Clean any changes from external
          # * Unlink external from git: Remove external/.git and .gitexternals
          # * Add external directory to parent
          # * Commit with flattened repo and tag info
          # - Depend on release branch checked out
Pablo Toharia's avatar
Pablo Toharia committed
340
          add_custom_target(flatten-gitexternal-${GIT_EXTERNAL_NAME}
341 342 343 344 345 346
            COMMAND "${GIT_EXECUTABLE}" clean -dfx
            COMMAND "${CMAKE_COMMAND}" -E remove_directory .git
            COMMAND "${CMAKE_COMMAND}" -E remove -f "${CMAKE_CURRENT_SOURCE_DIR}/.gitexternals"
            COMMAND "${GIT_EXECUTABLE}" add -f .
            COMMAND "${GIT_EXECUTABLE}" commit -m "Flatten ${REPO} into ${DIR} at ${TAG}" . "${CMAKE_CURRENT_SOURCE_DIR}/.gitexternals"
            COMMENT "Flatten ${REPO} into ${DIR}"
Pablo Toharia's avatar
Pablo Toharia committed
347
            DEPENDS ${PROJECT_NAME}-make-branch
348
            WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${DIR}")
Pablo Toharia's avatar
Pablo Toharia committed
349 350 351 352
          add_dependencies(flatten-gitexternal
            flatten-gitexternal-${GIT_EXTERNAL_NAME})
          add_dependencies(${PROJECT_NAME}-flatten-gitexternal
            flatten-gitexternal-${GIT_EXTERNAL_NAME})
353

Pablo Toharia's avatar
Pablo Toharia committed
354
          foreach(_target flatten-gitexternal-${GIT_EXTERNAL_NAME} ${PROJECT_NAME}-flatten-gitexternal flatten-gitexternal update-gitexternal-${GIT_EXTERNAL_NAME} ${GIT_EXTERNAL_TARGET} update-gitexternal update)
355 356 357 358 359 360 361 362
            set_target_properties(${_target} PROPERTIES
              EXCLUDE_FROM_DEFAULT_BUILD ON FOLDER git)
          endforeach()
        endif()
      endif()
    endif()
  endforeach()
endif()