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   subprocess.check_call([git, 'fetch', '--quiet'], cwd=directory)
    137 
    138   subprocess.check_call(
    139     [git, 'checkout', '--quiet', checkoutable], cwd=directory)
    140 
    141   if verbose:
    142     sys.stdout.write('%s\n  @ %s\n' % (directory, checkoutable))  # Success.
    143 
    144 
    145 def parse_file_to_dict(path):
    146   dictionary = {}
    147   execfile(path, dictionary)
    148   return dictionary
    149 
    150 
    151 def git_sync_deps(deps_file_path, command_line_os_requests, verbose):
    152   """Grab dependencies, with optional platform support.
    153 
    154   Args:
    155     deps_file_path (string) Path to the DEPS file.
    156 
    157     command_line_os_requests (list of strings) Can be empty list.
    158         List of strings that should each be a key in the deps_os
    159         dictionary in the DEPS file.
    160 
    161   Raises git Exceptions.
    162   """
    163   git = git_executable()
    164   assert git
    165 
    166   deps_file_directory = os.path.dirname(deps_file_path)
    167   deps_file = parse_file_to_dict(deps_file_path)
    168   dependencies = deps_file['deps'].copy()
    169   os_specific_dependencies = deps_file.get('deps_os', [])
    170   for os_name in command_line_os_requests:
    171     # Add OS-specific dependencies
    172     if os_name in os_specific_dependencies:
    173       dependencies.update(os_specific_dependencies[os_name])
    174   list_of_arg_lists = []
    175   for directory in dependencies:
    176     if '@' in dependencies[directory]:
    177       repo, checkoutable = dependencies[directory].split('@', 1)
    178     else:
    179       repo, checkoutable = dependencies[directory], 'origin/master'
    180 
    181     relative_directory = os.path.join(deps_file_directory, directory)
    182 
    183     list_of_arg_lists.append(
    184       (git, repo, checkoutable, relative_directory, verbose))
    185 
    186   multithread(git_checkout_to_directory, list_of_arg_lists)
    187 
    188   for directory in deps_file.get('recursedeps', []):
    189     recursive_path = os.path.join(deps_file_directory, directory, 'DEPS')
    190     git_sync_deps(recursive_path, command_line_os_requests, verbose)
    191 
    192 
    193 def multithread(function, list_of_arg_lists):
    194   # for args in list_of_arg_lists:
    195   #   function(*args)
    196   # return
    197   threads = []
    198   for args in list_of_arg_lists:
    199     thread = threading.Thread(None, function, None, args)
    200     thread.start()
    201     threads.append(thread)
    202   for thread in threads:
    203     thread.join()
    204 
    205 
    206 def main(argv):
    207   deps_file_path = os.environ.get('GIT_SYNC_DEPS_PATH', DEFAULT_DEPS_PATH)
    208   verbose = not bool(os.environ.get('GIT_SYNC_DEPS_QUIET', False))
    209 
    210   if '--help' in argv or '-h' in argv:
    211     usage(deps_file_path)
    212     return 1
    213 
    214   git_sync_deps(deps_file_path, argv, verbose)
    215   return 0
    216 
    217 
    218 if __name__ == '__main__':
    219   exit(main(sys.argv[1:]))
    220