1cb93a386Sopenharmony_ci#!/usr/bin/env python
2cb93a386Sopenharmony_ci# Copyright 2014 Google Inc.
3cb93a386Sopenharmony_ci#
4cb93a386Sopenharmony_ci# Use of this source code is governed by a BSD-style license that can be
5cb93a386Sopenharmony_ci# found in the LICENSE file.
6cb93a386Sopenharmony_ci
7cb93a386Sopenharmony_ci
8cb93a386Sopenharmony_ci"""Parse a DEPS file and git checkout all of the dependencies.
9cb93a386Sopenharmony_ci
10cb93a386Sopenharmony_ciArgs:
11cb93a386Sopenharmony_ci  An optional list of deps_os values.
12cb93a386Sopenharmony_ci
13cb93a386Sopenharmony_ciEnvironment Variables:
14cb93a386Sopenharmony_ci  GIT_EXECUTABLE: path to "git" binary; if unset, will look for git in
15cb93a386Sopenharmony_ci  your default path.
16cb93a386Sopenharmony_ci
17cb93a386Sopenharmony_ci  GIT_SYNC_DEPS_PATH: file to get the dependency list from; if unset,
18cb93a386Sopenharmony_ci  will use the file ../DEPS relative to this script's directory.
19cb93a386Sopenharmony_ci
20cb93a386Sopenharmony_ci  GIT_SYNC_DEPS_QUIET: if set to non-empty string, suppress messages.
21cb93a386Sopenharmony_ci
22cb93a386Sopenharmony_ciGit Config:
23cb93a386Sopenharmony_ci  To disable syncing of a single repository:
24cb93a386Sopenharmony_ci      cd path/to/repository
25cb93a386Sopenharmony_ci      git config sync-deps.disable true
26cb93a386Sopenharmony_ci
27cb93a386Sopenharmony_ci  To re-enable sync:
28cb93a386Sopenharmony_ci      cd path/to/repository
29cb93a386Sopenharmony_ci      git config --unset sync-deps.disable
30cb93a386Sopenharmony_ci"""
31cb93a386Sopenharmony_ci
32cb93a386Sopenharmony_ci
33cb93a386Sopenharmony_ciimport os
34cb93a386Sopenharmony_ciimport subprocess
35cb93a386Sopenharmony_ciimport sys
36cb93a386Sopenharmony_ciimport threading
37cb93a386Sopenharmony_ci
38cb93a386Sopenharmony_ci
39cb93a386Sopenharmony_cidef git_executable():
40cb93a386Sopenharmony_ci  """Find the git executable.
41cb93a386Sopenharmony_ci
42cb93a386Sopenharmony_ci  Returns:
43cb93a386Sopenharmony_ci      A string suitable for passing to subprocess functions, or None.
44cb93a386Sopenharmony_ci  """
45cb93a386Sopenharmony_ci  envgit = os.environ.get('GIT_EXECUTABLE')
46cb93a386Sopenharmony_ci  searchlist = ['git']
47cb93a386Sopenharmony_ci  if envgit:
48cb93a386Sopenharmony_ci    searchlist.insert(0, envgit)
49cb93a386Sopenharmony_ci  with open(os.devnull, 'w') as devnull:
50cb93a386Sopenharmony_ci    for git in searchlist:
51cb93a386Sopenharmony_ci      try:
52cb93a386Sopenharmony_ci        subprocess.call([git, '--version'], stdout=devnull)
53cb93a386Sopenharmony_ci      except (OSError,):
54cb93a386Sopenharmony_ci        continue
55cb93a386Sopenharmony_ci      return git
56cb93a386Sopenharmony_ci  return None
57cb93a386Sopenharmony_ci
58cb93a386Sopenharmony_ci
59cb93a386Sopenharmony_ciDEFAULT_DEPS_PATH = os.path.normpath(
60cb93a386Sopenharmony_ci  os.path.join(os.path.dirname(__file__), os.pardir, 'DEPS'))
61cb93a386Sopenharmony_ci
62cb93a386Sopenharmony_ci
63cb93a386Sopenharmony_cidef usage(deps_file_path = None):
64cb93a386Sopenharmony_ci  sys.stderr.write(
65cb93a386Sopenharmony_ci    'Usage: run to grab dependencies, with optional platform support:\n')
66cb93a386Sopenharmony_ci  sys.stderr.write('  %s %s' % (sys.executable, __file__))
67cb93a386Sopenharmony_ci  if deps_file_path:
68cb93a386Sopenharmony_ci    parsed_deps = parse_file_to_dict(deps_file_path)
69cb93a386Sopenharmony_ci    if 'deps_os' in parsed_deps:
70cb93a386Sopenharmony_ci      for deps_os in parsed_deps['deps_os']:
71cb93a386Sopenharmony_ci        sys.stderr.write(' [%s]' % deps_os)
72cb93a386Sopenharmony_ci  sys.stderr.write('\n\n')
73cb93a386Sopenharmony_ci  sys.stderr.write(__doc__)
74cb93a386Sopenharmony_ci
75cb93a386Sopenharmony_ci
76cb93a386Sopenharmony_cidef git_repository_sync_is_disabled(git, directory):
77cb93a386Sopenharmony_ci  try:
78cb93a386Sopenharmony_ci    disable = subprocess.check_output(
79cb93a386Sopenharmony_ci      [git, 'config', 'sync-deps.disable'], cwd=directory)
80cb93a386Sopenharmony_ci    return disable.lower().strip() in ['true', '1', 'yes', 'on']
81cb93a386Sopenharmony_ci  except subprocess.CalledProcessError:
82cb93a386Sopenharmony_ci    return False
83cb93a386Sopenharmony_ci
84cb93a386Sopenharmony_ci
85cb93a386Sopenharmony_cidef is_git_toplevel(git, directory):
86cb93a386Sopenharmony_ci  """Return true iff the directory is the top level of a Git repository.
87cb93a386Sopenharmony_ci
88cb93a386Sopenharmony_ci  Args:
89cb93a386Sopenharmony_ci    git (string) the git executable
90cb93a386Sopenharmony_ci
91cb93a386Sopenharmony_ci    directory (string) the path into which the repository
92cb93a386Sopenharmony_ci              is expected to be checked out.
93cb93a386Sopenharmony_ci  """
94cb93a386Sopenharmony_ci  try:
95cb93a386Sopenharmony_ci    toplevel = subprocess.check_output(
96cb93a386Sopenharmony_ci      [git, 'rev-parse', '--show-toplevel'], cwd=directory).strip()
97cb93a386Sopenharmony_ci    return os.path.realpath(directory) == os.path.realpath(toplevel.decode())
98cb93a386Sopenharmony_ci  except subprocess.CalledProcessError:
99cb93a386Sopenharmony_ci    return False
100cb93a386Sopenharmony_ci
101cb93a386Sopenharmony_ci
102cb93a386Sopenharmony_cidef status(directory, commithash, change):
103cb93a386Sopenharmony_ci  def truncate_beginning(s, length):
104cb93a386Sopenharmony_ci    return s if len(s) <= length else '...' + s[-(length-3):]
105cb93a386Sopenharmony_ci  def truncate_end(s, length):
106cb93a386Sopenharmony_ci    return s if len(s) <= length else s[:(length - 3)] + '...'
107cb93a386Sopenharmony_ci
108cb93a386Sopenharmony_ci  dlen = 36
109cb93a386Sopenharmony_ci  directory = truncate_beginning(directory, dlen)
110cb93a386Sopenharmony_ci  commithash = truncate_end(commithash, 40)
111cb93a386Sopenharmony_ci  symbol = '>' if change else '@'
112cb93a386Sopenharmony_ci  sys.stdout.write('%-*s %s %s\n' % (dlen, directory, symbol, commithash))
113cb93a386Sopenharmony_ci
114cb93a386Sopenharmony_ci
115cb93a386Sopenharmony_cidef git_checkout_to_directory(git, repo, commithash, directory, verbose):
116cb93a386Sopenharmony_ci  """Checkout (and clone if needed) a Git repository.
117cb93a386Sopenharmony_ci
118cb93a386Sopenharmony_ci  Args:
119cb93a386Sopenharmony_ci    git (string) the git executable
120cb93a386Sopenharmony_ci
121cb93a386Sopenharmony_ci    repo (string) the location of the repository, suitable
122cb93a386Sopenharmony_ci         for passing to `git clone`.
123cb93a386Sopenharmony_ci
124cb93a386Sopenharmony_ci    commithash (string) a commit, suitable for passing to `git checkout`
125cb93a386Sopenharmony_ci
126cb93a386Sopenharmony_ci    directory (string) the path into which the repository
127cb93a386Sopenharmony_ci              should be checked out.
128cb93a386Sopenharmony_ci
129cb93a386Sopenharmony_ci    verbose (boolean)
130cb93a386Sopenharmony_ci
131cb93a386Sopenharmony_ci  Raises an exception if any calls to git fail.
132cb93a386Sopenharmony_ci  """
133cb93a386Sopenharmony_ci  if not os.path.isdir(directory):
134cb93a386Sopenharmony_ci    subprocess.check_call(
135cb93a386Sopenharmony_ci      [git, 'clone', '--quiet', '--no-checkout', repo, directory])
136cb93a386Sopenharmony_ci    subprocess.check_call([git, 'checkout', '--quiet', commithash],
137cb93a386Sopenharmony_ci                          cwd=directory)
138cb93a386Sopenharmony_ci    if verbose:
139cb93a386Sopenharmony_ci      status(directory, commithash, True)
140cb93a386Sopenharmony_ci    return
141cb93a386Sopenharmony_ci
142cb93a386Sopenharmony_ci  if not is_git_toplevel(git, directory):
143cb93a386Sopenharmony_ci    # if the directory exists, but isn't a git repo, you will modify
144cb93a386Sopenharmony_ci    # the parent repostory, which isn't what you want.
145cb93a386Sopenharmony_ci    sys.stdout.write('%s\n  IS NOT TOP-LEVEL GIT DIRECTORY.\n' % directory)
146cb93a386Sopenharmony_ci    return
147cb93a386Sopenharmony_ci
148cb93a386Sopenharmony_ci  # Check to see if this repo is disabled.  Quick return.
149cb93a386Sopenharmony_ci  if git_repository_sync_is_disabled(git, directory):
150cb93a386Sopenharmony_ci    sys.stdout.write('%s\n  SYNC IS DISABLED.\n' % directory)
151cb93a386Sopenharmony_ci    return
152cb93a386Sopenharmony_ci
153cb93a386Sopenharmony_ci  with open(os.devnull, 'w') as devnull:
154cb93a386Sopenharmony_ci    # If this fails, we will fetch before trying again.  Don't spam user
155cb93a386Sopenharmony_ci    # with error infomation.
156cb93a386Sopenharmony_ci    if 0 == subprocess.call([git, 'checkout', '--quiet', commithash],
157cb93a386Sopenharmony_ci                            cwd=directory, stderr=devnull):
158cb93a386Sopenharmony_ci      # if this succeeds, skip slow `git fetch`.
159cb93a386Sopenharmony_ci      if verbose:
160cb93a386Sopenharmony_ci        status(directory, commithash, False)  # Success.
161cb93a386Sopenharmony_ci      return
162cb93a386Sopenharmony_ci
163cb93a386Sopenharmony_ci  # If the repo has changed, always force use of the correct repo.
164cb93a386Sopenharmony_ci  # If origin already points to repo, this is a quick no-op.
165cb93a386Sopenharmony_ci  subprocess.check_call(
166cb93a386Sopenharmony_ci      [git, 'remote', 'set-url', 'origin', repo], cwd=directory)
167cb93a386Sopenharmony_ci
168cb93a386Sopenharmony_ci  subprocess.check_call([git, 'fetch', '--quiet'], cwd=directory)
169cb93a386Sopenharmony_ci
170cb93a386Sopenharmony_ci  subprocess.check_call([git, 'checkout', '--quiet', commithash], cwd=directory)
171cb93a386Sopenharmony_ci
172cb93a386Sopenharmony_ci  if verbose:
173cb93a386Sopenharmony_ci    status(directory, commithash, True)  # Success.
174cb93a386Sopenharmony_ci
175cb93a386Sopenharmony_ci
176cb93a386Sopenharmony_cidef parse_file_to_dict(path):
177cb93a386Sopenharmony_ci  dictionary = {}
178cb93a386Sopenharmony_ci  with open(path) as f:
179cb93a386Sopenharmony_ci    exec('def Var(x): return vars[x]\n' + f.read(), dictionary)
180cb93a386Sopenharmony_ci  return dictionary
181cb93a386Sopenharmony_ci
182cb93a386Sopenharmony_ci
183cb93a386Sopenharmony_cidef is_sha1_sum(s):
184cb93a386Sopenharmony_ci  """SHA1 sums are 160 bits, encoded as lowercase hexadecimal."""
185cb93a386Sopenharmony_ci  return len(s) == 40 and all(c in '0123456789abcdef' for c in s)
186cb93a386Sopenharmony_ci
187cb93a386Sopenharmony_ci
188cb93a386Sopenharmony_cidef git_sync_deps(deps_file_path, command_line_os_requests, verbose):
189cb93a386Sopenharmony_ci  """Grab dependencies, with optional platform support.
190cb93a386Sopenharmony_ci
191cb93a386Sopenharmony_ci  Args:
192cb93a386Sopenharmony_ci    deps_file_path (string) Path to the DEPS file.
193cb93a386Sopenharmony_ci
194cb93a386Sopenharmony_ci    command_line_os_requests (list of strings) Can be empty list.
195cb93a386Sopenharmony_ci        List of strings that should each be a key in the deps_os
196cb93a386Sopenharmony_ci        dictionary in the DEPS file.
197cb93a386Sopenharmony_ci
198cb93a386Sopenharmony_ci  Raises git Exceptions.
199cb93a386Sopenharmony_ci  """
200cb93a386Sopenharmony_ci  git = git_executable()
201cb93a386Sopenharmony_ci  assert git
202cb93a386Sopenharmony_ci
203cb93a386Sopenharmony_ci  deps_file_directory = os.path.dirname(deps_file_path)
204cb93a386Sopenharmony_ci  deps_file = parse_file_to_dict(deps_file_path)
205cb93a386Sopenharmony_ci  dependencies = deps_file['deps'].copy()
206cb93a386Sopenharmony_ci  os_specific_dependencies = deps_file.get('deps_os', dict())
207cb93a386Sopenharmony_ci  if 'all' in command_line_os_requests:
208cb93a386Sopenharmony_ci    for value in os_specific_dependencies.itervalues():
209cb93a386Sopenharmony_ci      dependencies.update(value)
210cb93a386Sopenharmony_ci  else:
211cb93a386Sopenharmony_ci    for os_name in command_line_os_requests:
212cb93a386Sopenharmony_ci      # Add OS-specific dependencies
213cb93a386Sopenharmony_ci      if os_name in os_specific_dependencies:
214cb93a386Sopenharmony_ci        dependencies.update(os_specific_dependencies[os_name])
215cb93a386Sopenharmony_ci  for directory in dependencies:
216cb93a386Sopenharmony_ci    for other_dir in dependencies:
217cb93a386Sopenharmony_ci      if directory.startswith(other_dir + '/'):
218cb93a386Sopenharmony_ci        raise Exception('%r is parent of %r' % (other_dir, directory))
219cb93a386Sopenharmony_ci  list_of_arg_lists = []
220cb93a386Sopenharmony_ci  for directory in sorted(dependencies):
221cb93a386Sopenharmony_ci    if not isinstance(dependencies[directory], str):
222cb93a386Sopenharmony_ci      if verbose:
223cb93a386Sopenharmony_ci        sys.stdout.write( 'Skipping "%s".\n' % directory)
224cb93a386Sopenharmony_ci      continue
225cb93a386Sopenharmony_ci    if '@' in dependencies[directory]:
226cb93a386Sopenharmony_ci      repo, commithash = dependencies[directory].split('@', 1)
227cb93a386Sopenharmony_ci    else:
228cb93a386Sopenharmony_ci      raise Exception("please specify commit")
229cb93a386Sopenharmony_ci    if not is_sha1_sum(commithash):
230cb93a386Sopenharmony_ci      raise Exception("poorly formed commit hash: %r" % commithash)
231cb93a386Sopenharmony_ci
232cb93a386Sopenharmony_ci    relative_directory = os.path.join(deps_file_directory, directory)
233cb93a386Sopenharmony_ci
234cb93a386Sopenharmony_ci    list_of_arg_lists.append(
235cb93a386Sopenharmony_ci      (git, repo, commithash, relative_directory, verbose))
236cb93a386Sopenharmony_ci
237cb93a386Sopenharmony_ci  multithread(git_checkout_to_directory, list_of_arg_lists)
238cb93a386Sopenharmony_ci
239cb93a386Sopenharmony_ci
240cb93a386Sopenharmony_cidef multithread(function, list_of_arg_lists):
241cb93a386Sopenharmony_ci  # for args in list_of_arg_lists:
242cb93a386Sopenharmony_ci  #   function(*args)
243cb93a386Sopenharmony_ci  # return
244cb93a386Sopenharmony_ci  threads = []
245cb93a386Sopenharmony_ci  for args in list_of_arg_lists:
246cb93a386Sopenharmony_ci    thread = threading.Thread(None, function, None, args)
247cb93a386Sopenharmony_ci    thread.start()
248cb93a386Sopenharmony_ci    threads.append(thread)
249cb93a386Sopenharmony_ci  for thread in threads:
250cb93a386Sopenharmony_ci    thread.join()
251cb93a386Sopenharmony_ci
252cb93a386Sopenharmony_ci
253cb93a386Sopenharmony_cidef main(argv):
254cb93a386Sopenharmony_ci  deps_file_path = os.environ.get('GIT_SYNC_DEPS_PATH', DEFAULT_DEPS_PATH)
255cb93a386Sopenharmony_ci  verbose = not bool(os.environ.get('GIT_SYNC_DEPS_QUIET', False))
256cb93a386Sopenharmony_ci
257cb93a386Sopenharmony_ci  if '--help' in argv or '-h' in argv:
258cb93a386Sopenharmony_ci    usage(deps_file_path)
259cb93a386Sopenharmony_ci    return 1
260cb93a386Sopenharmony_ci
261cb93a386Sopenharmony_ci  git_sync_deps(deps_file_path, argv, verbose)
262cb93a386Sopenharmony_ci  subprocess.check_call(
263cb93a386Sopenharmony_ci      [sys.executable,
264cb93a386Sopenharmony_ci       os.path.join(os.path.dirname(deps_file_path), 'bin', 'fetch-gn')])
265cb93a386Sopenharmony_ci  return 0
266cb93a386Sopenharmony_ci
267cb93a386Sopenharmony_ci
268cb93a386Sopenharmony_ciif __name__ == '__main__':
269cb93a386Sopenharmony_ci  exit(main(sys.argv[1:]))
270