Home | History | Annotate | Download | only in tools
      1 #!/usr/bin/python
      2 # Copyright 2014 Google Inc.
      3 #
      4 # Use of this source code is governed by a BSD-style license that can be
      5 # found in the LICENSE file.
      6 
      7 
      8 """Parse a DEPS file and git checkout all of the dependencies.
      9 
     10 Args:
     11   An optional list of deps_os values.
     12 
     13 Environment Variables:
     14   GIT_EXECUTABLE: path to "git" binary; if unset, will look for one of
     15   ['git', 'git.exe', 'git.bat'] in your default path.
     16 
     17   GIT_SYNC_DEPS_PATH: file to get the dependency list from; if unset,
     18   will use the file ../DEPS relative to this script's directory.
     19 
     20   GIT_SYNC_DEPS_QUIET: if set to non-empty string, suppress messages.
     21 
     22 Git Config:
     23   To disable syncing of a single repository:
     24       cd path/to/repository
     25       git config sync-deps.disable true
     26 
     27   To re-enable sync:
     28       cd path/to/repository
     29       git config --unset sync-deps.disable
     30 """
     31 
     32 
     33 import os
     34 import subprocess
     35 import sys
     36 import threading
     37 
     38 
     39 def git_executable():
     40   """Find the git executable.
     41 
     42   Returns:
     43       A string suitable for passing to subprocess functions, or None.
     44   """
     45   envgit = os.environ.get('GIT_EXECUTABLE')
     46   searchlist = ['git', 'git.exe', 'git.bat']
     47   if envgit:
     48     searchlist.insert(0, envgit)
     49   with open(os.devnull, 'w') as devnull:
     50     for git in searchlist:
     51       try:
     52         subprocess.call([git, '--version'], stdout=devnull)
     53       except (OSError,):
     54         continue
     55       return git
     56   return None
     57 
     58 
     59 DEFAULT_DEPS_PATH = os.path.normpath(
     60   os.path.join(os.path.dirname(__file__), os.pardir, 'DEPS'))
     61 
     62 
     63 def usage(deps_file_path = None):
     64   sys.stderr.write(
     65     'Usage: run to grab dependencies, with optional platform support:\n')
     66   sys.stderr.write('  python %s' % __file__)
     67   if deps_file_path:
     68     for deps_os in parse_file_to_dict(deps_file_path)['deps_os']:
     69       sys.stderr.write(' [%s]' % deps_os)
     70   else:
     71     sys.stderr.write(' [DEPS_OS...]')
     72   sys.stderr.write('\n\n')
     73   sys.stderr.write(__doc__)
     74 
     75 
     76 def git_repository_sync_is_disabled(git, directory):
     77   try:
     78     disable = subprocess.check_output(
     79       [git, 'config', 'sync-deps.disable'], cwd=directory)
     80     return disable.lower().strip() in ['true', '1', 'yes', 'on']
     81   except subprocess.CalledProcessError:
     82     return False
     83 
     84 
     85 def is_git_toplevel(git, directory):
     86   """Return true iff the directory is the top level of a Git repository.
     87 
     88   Args:
     89     git (string) the git executable
     90 
     91     directory (string) the path into which the repository
     92               is expected to be checked out.
     93   """
     94   try:
     95     toplevel = subprocess.check_output(
     96       [git, 'rev-parse', '--show-toplevel'], cwd=directory).strip()
     97     return os.path.realpath(directory) == os.path.realpath(toplevel)
     98   except subprocess.CalledProcessError:
     99     return False
    100 
    101 
    102 def git_checkout_to_directory(git, repo, checkoutable, directory, verbose):
    103   """Checkout (and clone if needed) a Git repository.
    104 
    105   Args:
    106     git (string) the git executable
    107 
    108     repo (string) the location of the repository, suitable
    109          for passing to `git clone`.
    110 
    111     checkoutable (string) a tag, branch, or commit, suitable for
    112                  passing to `git checkout`
    113 
    114     directory (string) the path into which the repository
    115               should be checked out.
    116 
    117     verbose (boolean)
    118 
    119   Raises an exception if any calls to git fail.
    120   """
    121   if not os.path.isdir(directory):
    122     subprocess.check_call(
    123       [git, 'clone', '--quiet', repo, directory])
    124 
    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 
    131   # Check to see if this repo is disabled.  Quick return.
    132   if git_repository_sync_is_disabled(git, directory):
    133     sys.stdout.write('%s\n  SYNC IS DISABLED.\n' % directory)
    134     return
    135 
    136   if 0 == subprocess.call(
    137       [git, 'checkout', '--quiet', checkoutable], cwd=directory):
    138     # if this succeeds, skip slow `git fetch`.
    139     if verbose:
    140       sys.stdout.write('%s\n  @ %s\n' % (directory, checkoutable))
    141     return
    142 
    143   subprocess.check_call([git, 'fetch', '--quiet'], cwd=directory)
    144 
    145   if 0 != subprocess.call(
    146       [git, 'checkout', '--quiet', checkoutable], cwd=directory):
    147       subprocess.check_call(
    148           [git, 'remote', 'set-url', 'origin', repo], cwd=directory)
    149       subprocess.check_call([git, 'fetch', '--quiet'], cwd=directory)
    150       subprocess.check_call([git, 'checkout', '--quiet'], cwd=directory)
    151 
    152   if verbose:
    153     sys.stdout.write('%s\n  @ %s\n' % (directory, checkoutable))  # Success.
    154 
    155 
    156 def parse_file_to_dict(path):
    157   dictionary = {}
    158   execfile(path, dictionary)
    159   return dictionary
    160 
    161 
    162 def git_sync_deps(deps_file_path, command_line_os_requests, verbose):
    163   """Grab dependencies, with optional platform support.
    164 
    165   Args:
    166     deps_file_path (string) Path to the DEPS file.
    167 
    168     command_line_os_requests (list of strings) Can be empty list.
    169         List of strings that should each be a key in the deps_os
    170         dictionary in the DEPS file.
    171 
    172   Raises git Exceptions.
    173   """
    174   git = git_executable()
    175   assert git
    176 
    177   deps_file_directory = os.path.dirname(deps_file_path)
    178   deps_file = parse_file_to_dict(deps_file_path)
    179   dependencies = deps_file['deps'].copy()
    180   os_specific_dependencies = deps_file.get('deps_os', [])
    181   for os_name in command_line_os_requests:
    182     # Add OS-specific dependencies
    183     if os_name in os_specific_dependencies:
    184       dependencies.update(os_specific_dependencies[os_name])
    185   list_of_arg_lists = []
    186   for directory in dependencies:
    187     if '@' in dependencies[directory]:
    188       repo, checkoutable = dependencies[directory].split('@', 1)
    189     else:
    190       raise Exception("please specify commit or tag")
    191 
    192     relative_directory = os.path.join(deps_file_directory, directory)
    193 
    194     list_of_arg_lists.append(
    195       (git, repo, checkoutable, relative_directory, verbose))
    196 
    197   multithread(git_checkout_to_directory, list_of_arg_lists)
    198 
    199   for directory in deps_file.get('recursedeps', []):
    200     recursive_path = os.path.join(deps_file_directory, directory, 'DEPS')
    201     git_sync_deps(recursive_path, command_line_os_requests, verbose)
    202 
    203 
    204 def multithread(function, list_of_arg_lists):
    205   # for args in list_of_arg_lists:
    206   #   function(*args)
    207   # return
    208   threads = []
    209   for args in list_of_arg_lists:
    210     thread = threading.Thread(None, function, None, args)
    211     thread.start()
    212     threads.append(thread)
    213   for thread in threads:
    214     thread.join()
    215 
    216 
    217 def main(argv):
    218   deps_file_path = os.environ.get('GIT_SYNC_DEPS_PATH', DEFAULT_DEPS_PATH)
    219   verbose = not bool(os.environ.get('GIT_SYNC_DEPS_QUIET', False))
    220 
    221   if '--help' in argv or '-h' in argv:
    222     usage(deps_file_path)
    223     return 1
    224 
    225   git_sync_deps(deps_file_path, argv, verbose)
    226   return 0
    227 
    228 
    229 if __name__ == '__main__':
    230   exit(main(sys.argv[1:]))
    231