GitExternal.cmake 14 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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 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 121 122 123 124 125 126 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
# 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.
#
# 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:
#  GIT_EXTERNAL_VERBOSE
#    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)
option(GIT_EXTERNAL_VERBOSE "Print git commands as they are executed" OFF)

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)
  if(GIT_EXTERNAL_VERBOSE OR GIT_EXTERNAL_LOCAL_VERBOSE)
    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)
  cmake_parse_arguments(GIT_EXTERNAL_LOCAL "VERBOSE;SHALLOW" "" "RESET" ${ARGN})
  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)
      message(FATAL_ERROR "${DIR} clone failed: ${error}\n")
    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
268
        if(NOT TARGET update-gitexternal-${GIT_EXTERNAL_NAME}) # not done
269 270 271 272 273 274 275
          # 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
276 277 278 279 280 281 282
          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)
283 284 285 286 287 288 289
          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
290
          set(GIT_EXTERNAL_TARGET update-gitexternal-${GIT_EXTERNAL_TARGET})
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
          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
307
  WORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}/${DIR}\")
308 309 310
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
311
  WORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}/${DIR}\")
312 313 314 315 316 317
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
318
          add_custom_target(update-gitexternal-${GIT_EXTERNAL_NAME}
319 320 321 322
            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
323 324
          add_dependencies(update-gitexternal
            update-gitexternal-${GIT_EXTERNAL_NAME})
325 326 327 328 329 330 331

          # 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
332
          add_custom_target(flatten-gitexternal-${GIT_EXTERNAL_NAME}
333 334 335 336 337 338
            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
339
            DEPENDS ${PROJECT_NAME}-make-branch
340
            WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${DIR}")
Pablo Toharia's avatar
Pablo Toharia committed
341 342 343 344
          add_dependencies(flatten-gitexternal
            flatten-gitexternal-${GIT_EXTERNAL_NAME})
          add_dependencies(${PROJECT_NAME}-flatten-gitexternal
            flatten-gitexternal-${GIT_EXTERNAL_NAME})
345

Pablo Toharia's avatar
Pablo Toharia committed
346
          foreach(_target flatten-gitexternal-${GIT_EXTERNAL_NAME} ${PROJECT_NAME}-flatten-gitexternal flatten-gitexternal update-gitexternal-${GIT_EXTERNAL_NAME} ${GIT_EXTERNAL_TARGET} update-gitexternal update)
347 348 349 350 351 352 353 354
            set_target_properties(${_target} PROPERTIES
              EXCLUDE_FROM_DEFAULT_BUILD ON FOLDER git)
          endforeach()
        endif()
      endif()
    endif()
  endforeach()
endif()