git-sync-deps 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. #!/usr/bin/env python
  2. # Copyright 2014 Google Inc.
  3. #
  4. # Redistribution and use in source and binary forms, with or without
  5. # modification, are permitted provided that the following conditions are
  6. # met:
  7. #
  8. # * Redistributions of source code must retain the above copyright
  9. # notice, this list of conditions and the following disclaimer.
  10. # * Redistributions in binary form must reproduce the above
  11. # copyright notice, this list of conditions and the following disclaimer
  12. # in the documentation and/or other materials provided with the
  13. # distribution.
  14. # * Neither the name of Google Inc. nor the names of its
  15. # contributors may be used to endorse or promote products derived from
  16. # this software without specific prior written permission.
  17. #
  18. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  19. # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  20. # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  21. # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  22. # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  23. # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  24. # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  25. # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  26. # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  27. # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  28. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  29. """Parse a DEPS file and git checkout all of the dependencies.
  30. Args:
  31. An optional list of deps_os values.
  32. Environment Variables:
  33. GIT_EXECUTABLE: path to "git" binary; if unset, will look for one of
  34. ['git', 'git.exe', 'git.bat'] in your default path.
  35. GIT_SYNC_DEPS_PATH: file to get the dependency list from; if unset,
  36. will use the file ../DEPS relative to this script's directory.
  37. GIT_SYNC_DEPS_QUIET: if set to non-empty string, suppress messages.
  38. Git Config:
  39. To disable syncing of a single repository:
  40. cd path/to/repository
  41. git config sync-deps.disable true
  42. To re-enable sync:
  43. cd path/to/repository
  44. git config --unset sync-deps.disable
  45. """
  46. import os
  47. import re
  48. import subprocess
  49. import sys
  50. import threading
  51. from builtins import bytes
  52. def git_executable():
  53. """Find the git executable.
  54. Returns:
  55. A string suitable for passing to subprocess functions, or None.
  56. """
  57. envgit = os.environ.get('GIT_EXECUTABLE')
  58. searchlist = ['git', 'git.exe', 'git.bat']
  59. if envgit:
  60. searchlist.insert(0, envgit)
  61. with open(os.devnull, 'w') as devnull:
  62. for git in searchlist:
  63. try:
  64. subprocess.call([git, '--version'], stdout=devnull)
  65. except (OSError,):
  66. continue
  67. return git
  68. return None
  69. DEFAULT_DEPS_PATH = os.path.normpath(
  70. os.path.join(os.path.dirname(__file__), os.pardir, 'DEPS'))
  71. def usage(deps_file_path = None):
  72. sys.stderr.write(
  73. 'Usage: run to grab dependencies, with optional platform support:\n')
  74. sys.stderr.write(' %s %s' % (sys.executable, __file__))
  75. if deps_file_path:
  76. parsed_deps = parse_file_to_dict(deps_file_path)
  77. if 'deps_os' in parsed_deps:
  78. for deps_os in parsed_deps['deps_os']:
  79. sys.stderr.write(' [%s]' % deps_os)
  80. sys.stderr.write('\n\n')
  81. sys.stderr.write(__doc__)
  82. def git_repository_sync_is_disabled(git, directory):
  83. try:
  84. disable = subprocess.check_output(
  85. [git, 'config', 'sync-deps.disable'], cwd=directory)
  86. return disable.lower().strip() in ['true', '1', 'yes', 'on']
  87. except subprocess.CalledProcessError:
  88. return False
  89. def is_git_toplevel(git, directory):
  90. """Return true iff the directory is the top level of a Git repository.
  91. Args:
  92. git (string) the git executable
  93. directory (string) the path into which the repository
  94. is expected to be checked out.
  95. """
  96. try:
  97. toplevel = subprocess.check_output(
  98. [git, 'rev-parse', '--show-toplevel'], cwd=directory).strip()
  99. return os.path.realpath(bytes(directory, 'utf8')) == os.path.realpath(toplevel)
  100. except subprocess.CalledProcessError:
  101. return False
  102. def status(directory, checkoutable):
  103. def truncate(s, length):
  104. return s if len(s) <= length else s[:(length - 3)] + '...'
  105. dlen = 36
  106. directory = truncate(directory, dlen)
  107. checkoutable = truncate(checkoutable, 40)
  108. sys.stdout.write('%-*s @ %s\n' % (dlen, directory, checkoutable))
  109. def git_checkout_to_directory(git, repo, checkoutable, directory, verbose):
  110. """Checkout (and clone if needed) a Git repository.
  111. Args:
  112. git (string) the git executable
  113. repo (string) the location of the repository, suitable
  114. for passing to `git clone`.
  115. checkoutable (string) a tag, branch, or commit, suitable for
  116. passing to `git checkout`
  117. directory (string) the path into which the repository
  118. should be checked out.
  119. verbose (boolean)
  120. Raises an exception if any calls to git fail.
  121. """
  122. if not os.path.isdir(directory):
  123. subprocess.check_call(
  124. [git, 'clone', '--quiet', repo, directory])
  125. if not is_git_toplevel(git, directory):
  126. # if the directory exists, but isn't a git repo, you will modify
  127. # the parent repostory, which isn't what you want.
  128. sys.stdout.write('%s\n IS NOT TOP-LEVEL GIT DIRECTORY.\n' % directory)
  129. return
  130. # Check to see if this repo is disabled. Quick return.
  131. if git_repository_sync_is_disabled(git, directory):
  132. sys.stdout.write('%s\n SYNC IS DISABLED.\n' % directory)
  133. return
  134. with open(os.devnull, 'w') as devnull:
  135. # If this fails, we will fetch before trying again. Don't spam user
  136. # with error infomation.
  137. if 0 == subprocess.call([git, 'checkout', '--quiet', checkoutable],
  138. cwd=directory, stderr=devnull):
  139. # if this succeeds, skip slow `git fetch`.
  140. if verbose:
  141. status(directory, checkoutable) # Success.
  142. return
  143. # If the repo has changed, always force use of the correct repo.
  144. # If origin already points to repo, this is a quick no-op.
  145. subprocess.check_call(
  146. [git, 'remote', 'set-url', 'origin', repo], cwd=directory)
  147. subprocess.check_call([git, 'fetch', '--quiet'], cwd=directory)
  148. subprocess.check_call([git, 'checkout', '--quiet', checkoutable], cwd=directory)
  149. if verbose:
  150. status(directory, checkoutable) # Success.
  151. def parse_file_to_dict(path):
  152. dictionary = {}
  153. contents = open(path).read()
  154. # Need to convert Var() to vars[], so that the DEPS is actually Python. Var()
  155. # comes from Autoroller using gclient which has a slightly different DEPS
  156. # format.
  157. contents = re.sub(r"Var\((.*?)\)", r"vars[\1]", contents)
  158. exec(contents, dictionary)
  159. return dictionary
  160. def git_sync_deps(deps_file_path, command_line_os_requests, verbose):
  161. """Grab dependencies, with optional platform support.
  162. Args:
  163. deps_file_path (string) Path to the DEPS file.
  164. command_line_os_requests (list of strings) Can be empty list.
  165. List of strings that should each be a key in the deps_os
  166. dictionary in the DEPS file.
  167. Raises git Exceptions.
  168. """
  169. git = git_executable()
  170. assert git
  171. deps_file_directory = os.path.dirname(deps_file_path)
  172. deps_file = parse_file_to_dict(deps_file_path)
  173. dependencies = deps_file['deps'].copy()
  174. os_specific_dependencies = deps_file.get('deps_os', dict())
  175. if 'all' in command_line_os_requests:
  176. for value in list(os_specific_dependencies.values()):
  177. dependencies.update(value)
  178. else:
  179. for os_name in command_line_os_requests:
  180. # Add OS-specific dependencies
  181. if os_name in os_specific_dependencies:
  182. dependencies.update(os_specific_dependencies[os_name])
  183. for directory in dependencies:
  184. for other_dir in dependencies:
  185. if directory.startswith(other_dir + '/'):
  186. raise Exception('%r is parent of %r' % (other_dir, directory))
  187. list_of_arg_lists = []
  188. for directory in sorted(dependencies):
  189. if '@' in dependencies[directory]:
  190. repo, checkoutable = dependencies[directory].split('@', 1)
  191. else:
  192. raise Exception("please specify commit or tag")
  193. relative_directory = os.path.join(deps_file_directory, directory)
  194. list_of_arg_lists.append(
  195. (git, repo, checkoutable, relative_directory, verbose))
  196. multithread(git_checkout_to_directory, list_of_arg_lists)
  197. for directory in deps_file.get('recursedeps', []):
  198. recursive_path = os.path.join(deps_file_directory, directory, 'DEPS')
  199. git_sync_deps(recursive_path, command_line_os_requests, verbose)
  200. def multithread(function, list_of_arg_lists):
  201. # for args in list_of_arg_lists:
  202. # function(*args)
  203. # return
  204. threads = []
  205. for args in list_of_arg_lists:
  206. thread = threading.Thread(None, function, None, args)
  207. thread.start()
  208. threads.append(thread)
  209. for thread in threads:
  210. thread.join()
  211. def main(argv):
  212. deps_file_path = os.environ.get('GIT_SYNC_DEPS_PATH', DEFAULT_DEPS_PATH)
  213. verbose = not bool(os.environ.get('GIT_SYNC_DEPS_QUIET', False))
  214. if '--help' in argv or '-h' in argv:
  215. usage(deps_file_path)
  216. return 1
  217. git_sync_deps(deps_file_path, argv, verbose)
  218. # subprocess.check_call(
  219. # [sys.executable,
  220. # os.path.join(os.path.dirname(deps_file_path), 'bin', 'fetch-gn')])
  221. return 0
  222. if __name__ == '__main__':
  223. exit(main(sys.argv[1:]))