1#!/usr/bin/env 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 10Args: 11 An optional list of deps_os values. 12 13Environment Variables: 14 GIT_EXECUTABLE: path to "git" binary; if unset, will look for git in 15 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 22Git 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 33import os 34import subprocess 35import sys 36import threading 37 38 39def 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'] 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 59DEFAULT_DEPS_PATH = os.path.normpath( 60 os.path.join(os.path.dirname(__file__), os.pardir, 'DEPS')) 61 62 63def usage(deps_file_path = None): 64 sys.stderr.write( 65 'Usage: run to grab dependencies, with optional platform support:\n') 66 sys.stderr.write(' %s %s' % (sys.executable, __file__)) 67 if deps_file_path: 68 parsed_deps = parse_file_to_dict(deps_file_path) 69 if 'deps_os' in parsed_deps: 70 for deps_os in parsed_deps['deps_os']: 71 sys.stderr.write(' [%s]' % deps_os) 72 sys.stderr.write('\n\n') 73 sys.stderr.write(__doc__) 74 75 76def 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 85def 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 102def 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 110 111def git_checkout_to_directory(git, repo, checkoutable, directory, verbose): 112 """Checkout (and clone if needed) a Git repository. 113 114 Args: 115 git (string) the git executable 116 117 repo (string) the location of the repository, suitable 118 for passing to `git clone`. 119 120 checkoutable (string) a tag, branch, or commit, suitable for 121 passing to `git checkout` 122 123 directory (string) the path into which the repository 124 should be checked out. 125 126 verbose (boolean) 127 128 Raises an exception if any calls to git fail. 129 """ 130 if not os.path.isdir(directory): 131 subprocess.check_call( 132 [git, 'clone', '--quiet', repo, directory]) 133 134 if not is_git_toplevel(git, directory): 135 # if the directory exists, but isn't a git repo, you will modify 136 # the parent repostory, which isn't what you want. 137 sys.stdout.write('%s\n IS NOT TOP-LEVEL GIT DIRECTORY.\n' % directory) 138 return 139 140 # Check to see if this repo is disabled. Quick return. 141 if git_repository_sync_is_disabled(git, directory): 142 sys.stdout.write('%s\n SYNC IS DISABLED.\n' % directory) 143 return 144 145 with open(os.devnull, 'w') as devnull: 146 # If this fails, we will fetch before trying again. Don't spam user 147 # with error infomation. 148 if 0 == subprocess.call([git, 'checkout', '--quiet', checkoutable], 149 cwd=directory, stderr=devnull): 150 # if this succeeds, skip slow `git fetch`. 151 if verbose: 152 status(directory, checkoutable) # Success. 153 return 154 155 # If the repo has changed, always force use of the correct repo. 156 # If origin already points to repo, this is a quick no-op. 157 subprocess.check_call( 158 [git, 'remote', 'set-url', 'origin', repo], cwd=directory) 159 160 subprocess.check_call([git, 'fetch', '--quiet'], cwd=directory) 161 162 subprocess.check_call([git, 'checkout', '--quiet', checkoutable], cwd=directory) 163 164 if verbose: 165 status(directory, checkoutable) # Success. 166 167 168def parse_file_to_dict(path): 169 dictionary = {} 170 execfile(path, dictionary) 171 return dictionary 172 173 174def git_sync_deps(deps_file_path, command_line_os_requests, verbose): 175 """Grab dependencies, with optional platform support. 176 177 Args: 178 deps_file_path (string) Path to the DEPS file. 179 180 command_line_os_requests (list of strings) Can be empty list. 181 List of strings that should each be a key in the deps_os 182 dictionary in the DEPS file. 183 184 Raises git Exceptions. 185 """ 186 git = git_executable() 187 assert git 188 189 deps_file_directory = os.path.dirname(deps_file_path) 190 deps_file = parse_file_to_dict(deps_file_path) 191 dependencies = deps_file['deps'].copy() 192 os_specific_dependencies = deps_file.get('deps_os', dict()) 193 if 'all' in command_line_os_requests: 194 for value in os_specific_dependencies.itervalues(): 195 dependencies.update(value) 196 else: 197 for os_name in command_line_os_requests: 198 # Add OS-specific dependencies 199 if os_name in os_specific_dependencies: 200 dependencies.update(os_specific_dependencies[os_name]) 201 for directory in dependencies: 202 for other_dir in dependencies: 203 if directory.startswith(other_dir + '/'): 204 raise Exception('%r is parent of %r' % (other_dir, directory)) 205 list_of_arg_lists = [] 206 for directory in sorted(dependencies): 207 if not isinstance(dependencies[directory], basestring): 208 if verbose: 209 print 'Skipping "%s".' % directory 210 continue 211 if '@' in dependencies[directory]: 212 repo, checkoutable = dependencies[directory].split('@', 1) 213 else: 214 raise Exception("please specify commit or tag") 215 216 relative_directory = os.path.join(deps_file_directory, directory) 217 218 list_of_arg_lists.append( 219 (git, repo, checkoutable, relative_directory, verbose)) 220 221 multithread(git_checkout_to_directory, list_of_arg_lists) 222 223 224def multithread(function, list_of_arg_lists): 225 # for args in list_of_arg_lists: 226 # function(*args) 227 # return 228 threads = [] 229 for args in list_of_arg_lists: 230 thread = threading.Thread(None, function, None, args) 231 thread.start() 232 threads.append(thread) 233 for thread in threads: 234 thread.join() 235 236 237def main(argv): 238 deps_file_path = os.environ.get('GIT_SYNC_DEPS_PATH', DEFAULT_DEPS_PATH) 239 verbose = not bool(os.environ.get('GIT_SYNC_DEPS_QUIET', False)) 240 241 if '--help' in argv or '-h' in argv: 242 usage(deps_file_path) 243 return 1 244 245 git_sync_deps(deps_file_path, argv, verbose) 246 subprocess.check_call( 247 [sys.executable, 248 os.path.join(os.path.dirname(deps_file_path), 'bin', 'fetch-gn')]) 249 return 0 250 251 252if __name__ == '__main__': 253 exit(main(sys.argv[1:])) 254