xref: /aosp_15_r20/external/libchrome/build/get_syzygy_binaries.py (revision 635a864187cb8b6c713ff48b7e790a6b21769273)
1*635a8641SAndroid Build Coastguard Worker#!/usr/bin/env python
2*635a8641SAndroid Build Coastguard Worker# Copyright 2014 The Chromium Authors. All rights reserved.
3*635a8641SAndroid Build Coastguard Worker# Use of this source code is governed by a BSD-style license that can be
4*635a8641SAndroid Build Coastguard Worker# found in the LICENSE file.
5*635a8641SAndroid Build Coastguard Worker
6*635a8641SAndroid Build Coastguard Worker"""A utility script for downloading versioned Syzygy binaries."""
7*635a8641SAndroid Build Coastguard Worker
8*635a8641SAndroid Build Coastguard Workerimport hashlib
9*635a8641SAndroid Build Coastguard Workerimport errno
10*635a8641SAndroid Build Coastguard Workerimport json
11*635a8641SAndroid Build Coastguard Workerimport logging
12*635a8641SAndroid Build Coastguard Workerimport optparse
13*635a8641SAndroid Build Coastguard Workerimport os
14*635a8641SAndroid Build Coastguard Workerimport re
15*635a8641SAndroid Build Coastguard Workerimport shutil
16*635a8641SAndroid Build Coastguard Workerimport stat
17*635a8641SAndroid Build Coastguard Workerimport sys
18*635a8641SAndroid Build Coastguard Workerimport subprocess
19*635a8641SAndroid Build Coastguard Workerimport tempfile
20*635a8641SAndroid Build Coastguard Workerimport time
21*635a8641SAndroid Build Coastguard Workerimport zipfile
22*635a8641SAndroid Build Coastguard Worker
23*635a8641SAndroid Build Coastguard Worker
24*635a8641SAndroid Build Coastguard Worker_LOGGER = logging.getLogger(os.path.basename(__file__))
25*635a8641SAndroid Build Coastguard Worker
26*635a8641SAndroid Build Coastguard Worker# The relative path where official builds are archived in their GS bucket.
27*635a8641SAndroid Build Coastguard Worker_SYZYGY_ARCHIVE_PATH = ('/builds/official/%(revision)s')
28*635a8641SAndroid Build Coastguard Worker
29*635a8641SAndroid Build Coastguard Worker# A JSON file containing the state of the download directory. If this file and
30*635a8641SAndroid Build Coastguard Worker# directory state do not agree, then the binaries will be downloaded and
31*635a8641SAndroid Build Coastguard Worker# installed again.
32*635a8641SAndroid Build Coastguard Worker_STATE = '.state'
33*635a8641SAndroid Build Coastguard Worker
34*635a8641SAndroid Build Coastguard Worker# This matches an integer (an SVN revision number) or a SHA1 value (a GIT hash).
35*635a8641SAndroid Build Coastguard Worker# The archive exclusively uses lowercase GIT hashes.
36*635a8641SAndroid Build Coastguard Worker_REVISION_RE = re.compile('^(?:\d+|[a-f0-9]{40})$')
37*635a8641SAndroid Build Coastguard Worker
38*635a8641SAndroid Build Coastguard Worker# This matches an MD5 hash.
39*635a8641SAndroid Build Coastguard Worker_MD5_RE = re.compile('^[a-f0-9]{32}$')
40*635a8641SAndroid Build Coastguard Worker
41*635a8641SAndroid Build Coastguard Worker# List of reources to be downloaded and installed. These are tuples with the
42*635a8641SAndroid Build Coastguard Worker# following format:
43*635a8641SAndroid Build Coastguard Worker# (basename, logging name, relative installation path, extraction filter)
44*635a8641SAndroid Build Coastguard Worker_RESOURCES = [
45*635a8641SAndroid Build Coastguard Worker  ('benchmark.zip', 'benchmark', '', None),
46*635a8641SAndroid Build Coastguard Worker  ('binaries.zip', 'binaries', 'exe', None),
47*635a8641SAndroid Build Coastguard Worker  ('symbols.zip', 'symbols', 'exe',
48*635a8641SAndroid Build Coastguard Worker      lambda x: x.filename.endswith('.dll.pdb'))]
49*635a8641SAndroid Build Coastguard Worker
50*635a8641SAndroid Build Coastguard Worker
51*635a8641SAndroid Build Coastguard Worker# Name of the MS DIA dll that we need to copy to the binaries directory.
52*635a8641SAndroid Build Coastguard Worker_DIA_DLL_NAME = "msdia140.dll"
53*635a8641SAndroid Build Coastguard Worker
54*635a8641SAndroid Build Coastguard Worker
55*635a8641SAndroid Build Coastguard Workerdef _LoadState(output_dir):
56*635a8641SAndroid Build Coastguard Worker  """Loads the contents of the state file for a given |output_dir|, returning
57*635a8641SAndroid Build Coastguard Worker  None if it doesn't exist.
58*635a8641SAndroid Build Coastguard Worker  """
59*635a8641SAndroid Build Coastguard Worker  path = os.path.join(output_dir, _STATE)
60*635a8641SAndroid Build Coastguard Worker  if not os.path.exists(path):
61*635a8641SAndroid Build Coastguard Worker    _LOGGER.debug('No state file found.')
62*635a8641SAndroid Build Coastguard Worker    return None
63*635a8641SAndroid Build Coastguard Worker  with open(path, 'rb') as f:
64*635a8641SAndroid Build Coastguard Worker    _LOGGER.debug('Reading state file: %s', path)
65*635a8641SAndroid Build Coastguard Worker    try:
66*635a8641SAndroid Build Coastguard Worker      return json.load(f)
67*635a8641SAndroid Build Coastguard Worker    except ValueError:
68*635a8641SAndroid Build Coastguard Worker      _LOGGER.debug('Invalid state file.')
69*635a8641SAndroid Build Coastguard Worker      return None
70*635a8641SAndroid Build Coastguard Worker
71*635a8641SAndroid Build Coastguard Worker
72*635a8641SAndroid Build Coastguard Workerdef _SaveState(output_dir, state, dry_run=False):
73*635a8641SAndroid Build Coastguard Worker  """Saves the |state| dictionary to the given |output_dir| as a JSON file."""
74*635a8641SAndroid Build Coastguard Worker  path = os.path.join(output_dir, _STATE)
75*635a8641SAndroid Build Coastguard Worker  _LOGGER.debug('Writing state file: %s', path)
76*635a8641SAndroid Build Coastguard Worker  if dry_run:
77*635a8641SAndroid Build Coastguard Worker    return
78*635a8641SAndroid Build Coastguard Worker  with open(path, 'wb') as f:
79*635a8641SAndroid Build Coastguard Worker    f.write(json.dumps(state, sort_keys=True, indent=2))
80*635a8641SAndroid Build Coastguard Worker
81*635a8641SAndroid Build Coastguard Worker
82*635a8641SAndroid Build Coastguard Workerdef _Md5(path):
83*635a8641SAndroid Build Coastguard Worker  """Returns the MD5 hash of the file at |path|, which must exist."""
84*635a8641SAndroid Build Coastguard Worker  return hashlib.md5(open(path, 'rb').read()).hexdigest()
85*635a8641SAndroid Build Coastguard Worker
86*635a8641SAndroid Build Coastguard Worker
87*635a8641SAndroid Build Coastguard Workerdef _StateIsValid(state):
88*635a8641SAndroid Build Coastguard Worker  """Returns true if the given state structure is valid."""
89*635a8641SAndroid Build Coastguard Worker  if not isinstance(state, dict):
90*635a8641SAndroid Build Coastguard Worker    _LOGGER.debug('State must be a dict.')
91*635a8641SAndroid Build Coastguard Worker    return False
92*635a8641SAndroid Build Coastguard Worker  r = state.get('revision', None)
93*635a8641SAndroid Build Coastguard Worker  if not isinstance(r, basestring) or not _REVISION_RE.match(r):
94*635a8641SAndroid Build Coastguard Worker    _LOGGER.debug('State contains an invalid revision.')
95*635a8641SAndroid Build Coastguard Worker    return False
96*635a8641SAndroid Build Coastguard Worker  c = state.get('contents', None)
97*635a8641SAndroid Build Coastguard Worker  if not isinstance(c, dict):
98*635a8641SAndroid Build Coastguard Worker    _LOGGER.debug('State must contain a contents dict.')
99*635a8641SAndroid Build Coastguard Worker    return False
100*635a8641SAndroid Build Coastguard Worker  for (relpath, md5) in c.items():
101*635a8641SAndroid Build Coastguard Worker    if not isinstance(relpath, basestring) or len(relpath) == 0:
102*635a8641SAndroid Build Coastguard Worker      _LOGGER.debug('State contents dict contains an invalid path.')
103*635a8641SAndroid Build Coastguard Worker      return False
104*635a8641SAndroid Build Coastguard Worker    if not isinstance(md5, basestring) or not _MD5_RE.match(md5):
105*635a8641SAndroid Build Coastguard Worker      _LOGGER.debug('State contents dict contains an invalid MD5 digest.')
106*635a8641SAndroid Build Coastguard Worker      return False
107*635a8641SAndroid Build Coastguard Worker  return True
108*635a8641SAndroid Build Coastguard Worker
109*635a8641SAndroid Build Coastguard Worker
110*635a8641SAndroid Build Coastguard Workerdef _BuildActualState(stored, revision, output_dir):
111*635a8641SAndroid Build Coastguard Worker  """Builds the actual state using the provided |stored| state as a template.
112*635a8641SAndroid Build Coastguard Worker  Only examines files listed in the stored state, causing the script to ignore
113*635a8641SAndroid Build Coastguard Worker  files that have been added to the directories locally. |stored| must be a
114*635a8641SAndroid Build Coastguard Worker  valid state dictionary.
115*635a8641SAndroid Build Coastguard Worker  """
116*635a8641SAndroid Build Coastguard Worker  contents = {}
117*635a8641SAndroid Build Coastguard Worker  state = { 'revision': revision, 'contents': contents }
118*635a8641SAndroid Build Coastguard Worker  for relpath, md5 in stored['contents'].items():
119*635a8641SAndroid Build Coastguard Worker    abspath = os.path.abspath(os.path.join(output_dir, relpath))
120*635a8641SAndroid Build Coastguard Worker    if os.path.isfile(abspath):
121*635a8641SAndroid Build Coastguard Worker      m = _Md5(abspath)
122*635a8641SAndroid Build Coastguard Worker      contents[relpath] = m
123*635a8641SAndroid Build Coastguard Worker
124*635a8641SAndroid Build Coastguard Worker  return state
125*635a8641SAndroid Build Coastguard Worker
126*635a8641SAndroid Build Coastguard Worker
127*635a8641SAndroid Build Coastguard Workerdef _StatesAreConsistent(stored, actual):
128*635a8641SAndroid Build Coastguard Worker  """Validates whether two state dictionaries are consistent. Both must be valid
129*635a8641SAndroid Build Coastguard Worker  state dictionaries. Additional entries in |actual| are ignored.
130*635a8641SAndroid Build Coastguard Worker  """
131*635a8641SAndroid Build Coastguard Worker  if stored['revision'] != actual['revision']:
132*635a8641SAndroid Build Coastguard Worker    _LOGGER.debug('Mismatched revision number.')
133*635a8641SAndroid Build Coastguard Worker    return False
134*635a8641SAndroid Build Coastguard Worker  cont_stored = stored['contents']
135*635a8641SAndroid Build Coastguard Worker  cont_actual = actual['contents']
136*635a8641SAndroid Build Coastguard Worker  for relpath, md5 in cont_stored.items():
137*635a8641SAndroid Build Coastguard Worker    if relpath not in cont_actual:
138*635a8641SAndroid Build Coastguard Worker      _LOGGER.debug('Missing content: %s', relpath)
139*635a8641SAndroid Build Coastguard Worker      return False
140*635a8641SAndroid Build Coastguard Worker    if md5 != cont_actual[relpath]:
141*635a8641SAndroid Build Coastguard Worker      _LOGGER.debug('Modified content: %s', relpath)
142*635a8641SAndroid Build Coastguard Worker      return False
143*635a8641SAndroid Build Coastguard Worker  return True
144*635a8641SAndroid Build Coastguard Worker
145*635a8641SAndroid Build Coastguard Worker
146*635a8641SAndroid Build Coastguard Workerdef _GetCurrentState(revision, output_dir):
147*635a8641SAndroid Build Coastguard Worker  """Loads the current state and checks to see if it is consistent. Returns
148*635a8641SAndroid Build Coastguard Worker  a tuple (state, bool). The returned state will always be valid, even if an
149*635a8641SAndroid Build Coastguard Worker  invalid state is present on disk.
150*635a8641SAndroid Build Coastguard Worker  """
151*635a8641SAndroid Build Coastguard Worker  stored = _LoadState(output_dir)
152*635a8641SAndroid Build Coastguard Worker  if not _StateIsValid(stored):
153*635a8641SAndroid Build Coastguard Worker    _LOGGER.debug('State is invalid.')
154*635a8641SAndroid Build Coastguard Worker    # Return a valid but empty state.
155*635a8641SAndroid Build Coastguard Worker    return ({'revision': '0', 'contents': {}}, False)
156*635a8641SAndroid Build Coastguard Worker  actual = _BuildActualState(stored, revision, output_dir)
157*635a8641SAndroid Build Coastguard Worker  # If the script has been modified consider the state invalid.
158*635a8641SAndroid Build Coastguard Worker  path = os.path.join(output_dir, _STATE)
159*635a8641SAndroid Build Coastguard Worker  if os.path.getmtime(__file__) > os.path.getmtime(path):
160*635a8641SAndroid Build Coastguard Worker    return (stored, False)
161*635a8641SAndroid Build Coastguard Worker  # Otherwise, explicitly validate the state.
162*635a8641SAndroid Build Coastguard Worker  if not _StatesAreConsistent(stored, actual):
163*635a8641SAndroid Build Coastguard Worker    return (stored, False)
164*635a8641SAndroid Build Coastguard Worker  return (stored, True)
165*635a8641SAndroid Build Coastguard Worker
166*635a8641SAndroid Build Coastguard Worker
167*635a8641SAndroid Build Coastguard Workerdef _DirIsEmpty(path):
168*635a8641SAndroid Build Coastguard Worker  """Returns true if the given directory is empty, false otherwise."""
169*635a8641SAndroid Build Coastguard Worker  for root, dirs, files in os.walk(path):
170*635a8641SAndroid Build Coastguard Worker    return not dirs and not files
171*635a8641SAndroid Build Coastguard Worker
172*635a8641SAndroid Build Coastguard Worker
173*635a8641SAndroid Build Coastguard Workerdef _RmTreeHandleReadOnly(func, path, exc):
174*635a8641SAndroid Build Coastguard Worker  """An error handling function for use with shutil.rmtree. This will
175*635a8641SAndroid Build Coastguard Worker  detect failures to remove read-only files, and will change their properties
176*635a8641SAndroid Build Coastguard Worker  prior to removing them. This is necessary on Windows as os.remove will return
177*635a8641SAndroid Build Coastguard Worker  an access error for read-only files, and git repos contain read-only
178*635a8641SAndroid Build Coastguard Worker  pack/index files.
179*635a8641SAndroid Build Coastguard Worker  """
180*635a8641SAndroid Build Coastguard Worker  excvalue = exc[1]
181*635a8641SAndroid Build Coastguard Worker  if func in (os.rmdir, os.remove) and excvalue.errno == errno.EACCES:
182*635a8641SAndroid Build Coastguard Worker    _LOGGER.debug('Removing read-only path: %s', path)
183*635a8641SAndroid Build Coastguard Worker    os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
184*635a8641SAndroid Build Coastguard Worker    func(path)
185*635a8641SAndroid Build Coastguard Worker  else:
186*635a8641SAndroid Build Coastguard Worker    raise
187*635a8641SAndroid Build Coastguard Worker
188*635a8641SAndroid Build Coastguard Worker
189*635a8641SAndroid Build Coastguard Workerdef _RmTree(path):
190*635a8641SAndroid Build Coastguard Worker  """A wrapper of shutil.rmtree that handles read-only files."""
191*635a8641SAndroid Build Coastguard Worker  shutil.rmtree(path, ignore_errors=False, onerror=_RmTreeHandleReadOnly)
192*635a8641SAndroid Build Coastguard Worker
193*635a8641SAndroid Build Coastguard Worker
194*635a8641SAndroid Build Coastguard Workerdef _CleanState(output_dir, state, dry_run=False):
195*635a8641SAndroid Build Coastguard Worker  """Cleans up files/directories in |output_dir| that are referenced by
196*635a8641SAndroid Build Coastguard Worker  the given |state|. Raises an error if there are local changes. Returns a
197*635a8641SAndroid Build Coastguard Worker  dictionary of files that were deleted.
198*635a8641SAndroid Build Coastguard Worker  """
199*635a8641SAndroid Build Coastguard Worker  _LOGGER.debug('Deleting files from previous installation.')
200*635a8641SAndroid Build Coastguard Worker  deleted = {}
201*635a8641SAndroid Build Coastguard Worker
202*635a8641SAndroid Build Coastguard Worker  # Generate a list of files to delete, relative to |output_dir|.
203*635a8641SAndroid Build Coastguard Worker  contents = state['contents']
204*635a8641SAndroid Build Coastguard Worker  files = sorted(contents.keys())
205*635a8641SAndroid Build Coastguard Worker
206*635a8641SAndroid Build Coastguard Worker  # Try to delete the files. Keep track of directories to delete as well.
207*635a8641SAndroid Build Coastguard Worker  dirs = {}
208*635a8641SAndroid Build Coastguard Worker  for relpath in files:
209*635a8641SAndroid Build Coastguard Worker    fullpath = os.path.join(output_dir, relpath)
210*635a8641SAndroid Build Coastguard Worker    fulldir = os.path.dirname(fullpath)
211*635a8641SAndroid Build Coastguard Worker    dirs[fulldir] = True
212*635a8641SAndroid Build Coastguard Worker    if os.path.exists(fullpath):
213*635a8641SAndroid Build Coastguard Worker      # If somehow the file has become a directory complain about it.
214*635a8641SAndroid Build Coastguard Worker      if os.path.isdir(fullpath):
215*635a8641SAndroid Build Coastguard Worker        raise Exception('Directory exists where file expected: %s' % fullpath)
216*635a8641SAndroid Build Coastguard Worker
217*635a8641SAndroid Build Coastguard Worker      # Double check that the file doesn't have local changes. If it does
218*635a8641SAndroid Build Coastguard Worker      # then refuse to delete it.
219*635a8641SAndroid Build Coastguard Worker      if relpath in contents:
220*635a8641SAndroid Build Coastguard Worker        stored_md5 = contents[relpath]
221*635a8641SAndroid Build Coastguard Worker        actual_md5 = _Md5(fullpath)
222*635a8641SAndroid Build Coastguard Worker        if actual_md5 != stored_md5:
223*635a8641SAndroid Build Coastguard Worker          raise Exception('File has local changes: %s' % fullpath)
224*635a8641SAndroid Build Coastguard Worker
225*635a8641SAndroid Build Coastguard Worker      # The file is unchanged so it can safely be deleted.
226*635a8641SAndroid Build Coastguard Worker      _LOGGER.debug('Deleting file "%s".', fullpath)
227*635a8641SAndroid Build Coastguard Worker      deleted[relpath] = True
228*635a8641SAndroid Build Coastguard Worker      if not dry_run:
229*635a8641SAndroid Build Coastguard Worker        os.unlink(fullpath)
230*635a8641SAndroid Build Coastguard Worker
231*635a8641SAndroid Build Coastguard Worker  # Sort directories from longest name to shortest. This lets us remove empty
232*635a8641SAndroid Build Coastguard Worker  # directories from the most nested paths first.
233*635a8641SAndroid Build Coastguard Worker  dirs = sorted(dirs.keys(), key=lambda x: len(x), reverse=True)
234*635a8641SAndroid Build Coastguard Worker  for p in dirs:
235*635a8641SAndroid Build Coastguard Worker    if os.path.exists(p) and _DirIsEmpty(p):
236*635a8641SAndroid Build Coastguard Worker      _LOGGER.debug('Deleting empty directory "%s".', p)
237*635a8641SAndroid Build Coastguard Worker      if not dry_run:
238*635a8641SAndroid Build Coastguard Worker        _RmTree(p)
239*635a8641SAndroid Build Coastguard Worker
240*635a8641SAndroid Build Coastguard Worker  return deleted
241*635a8641SAndroid Build Coastguard Worker
242*635a8641SAndroid Build Coastguard Worker
243*635a8641SAndroid Build Coastguard Workerdef _FindGsUtil():
244*635a8641SAndroid Build Coastguard Worker  """Looks for depot_tools and returns the absolute path to gsutil.py."""
245*635a8641SAndroid Build Coastguard Worker  for path in os.environ['PATH'].split(os.pathsep):
246*635a8641SAndroid Build Coastguard Worker    path = os.path.abspath(path)
247*635a8641SAndroid Build Coastguard Worker    git_cl = os.path.join(path, 'git_cl.py')
248*635a8641SAndroid Build Coastguard Worker    gs_util = os.path.join(path, 'gsutil.py')
249*635a8641SAndroid Build Coastguard Worker    if os.path.exists(git_cl) and os.path.exists(gs_util):
250*635a8641SAndroid Build Coastguard Worker      return gs_util
251*635a8641SAndroid Build Coastguard Worker  return None
252*635a8641SAndroid Build Coastguard Worker
253*635a8641SAndroid Build Coastguard Worker
254*635a8641SAndroid Build Coastguard Workerdef _GsUtil(*cmd):
255*635a8641SAndroid Build Coastguard Worker  """Runs the given command in gsutil with exponential backoff and retries."""
256*635a8641SAndroid Build Coastguard Worker  gs_util = _FindGsUtil()
257*635a8641SAndroid Build Coastguard Worker  cmd = [sys.executable, gs_util] + list(cmd)
258*635a8641SAndroid Build Coastguard Worker
259*635a8641SAndroid Build Coastguard Worker  retries = 3
260*635a8641SAndroid Build Coastguard Worker  timeout = 4  # Seconds.
261*635a8641SAndroid Build Coastguard Worker  while True:
262*635a8641SAndroid Build Coastguard Worker    _LOGGER.debug('Running %s', cmd)
263*635a8641SAndroid Build Coastguard Worker    prog = subprocess.Popen(cmd, shell=False)
264*635a8641SAndroid Build Coastguard Worker    prog.communicate()
265*635a8641SAndroid Build Coastguard Worker
266*635a8641SAndroid Build Coastguard Worker    # Stop retrying on success.
267*635a8641SAndroid Build Coastguard Worker    if prog.returncode == 0:
268*635a8641SAndroid Build Coastguard Worker      return
269*635a8641SAndroid Build Coastguard Worker
270*635a8641SAndroid Build Coastguard Worker    # Raise a permanent failure if retries have been exhausted.
271*635a8641SAndroid Build Coastguard Worker    if retries == 0:
272*635a8641SAndroid Build Coastguard Worker      raise RuntimeError('Command "%s" returned %d.' % (cmd, prog.returncode))
273*635a8641SAndroid Build Coastguard Worker
274*635a8641SAndroid Build Coastguard Worker    _LOGGER.debug('Sleeping %d seconds and trying again.', timeout)
275*635a8641SAndroid Build Coastguard Worker    time.sleep(timeout)
276*635a8641SAndroid Build Coastguard Worker    retries -= 1
277*635a8641SAndroid Build Coastguard Worker    timeout *= 2
278*635a8641SAndroid Build Coastguard Worker
279*635a8641SAndroid Build Coastguard Worker
280*635a8641SAndroid Build Coastguard Workerdef _Download(resource):
281*635a8641SAndroid Build Coastguard Worker  """Downloads the given GS resource to a temporary file, returning its path."""
282*635a8641SAndroid Build Coastguard Worker  tmp = tempfile.mkstemp(suffix='syzygy_archive')
283*635a8641SAndroid Build Coastguard Worker  os.close(tmp[0])
284*635a8641SAndroid Build Coastguard Worker  tmp_file = tmp[1]
285*635a8641SAndroid Build Coastguard Worker  url = 'gs://syzygy-archive' + resource
286*635a8641SAndroid Build Coastguard Worker  if sys.platform == 'cygwin':
287*635a8641SAndroid Build Coastguard Worker    # Change temporary path to Windows path for gsutil
288*635a8641SAndroid Build Coastguard Worker    def winpath(path):
289*635a8641SAndroid Build Coastguard Worker      return subprocess.check_output(['cygpath', '-w', path]).strip()
290*635a8641SAndroid Build Coastguard Worker    tmp_file = winpath(tmp_file)
291*635a8641SAndroid Build Coastguard Worker  _GsUtil('cp', url, tmp_file)
292*635a8641SAndroid Build Coastguard Worker  return tmp[1]
293*635a8641SAndroid Build Coastguard Worker
294*635a8641SAndroid Build Coastguard Worker
295*635a8641SAndroid Build Coastguard Workerdef _MaybeCopyDIABinaries(options, contents):
296*635a8641SAndroid Build Coastguard Worker  """Try to copy the DIA DLL to the binaries exe directory."""
297*635a8641SAndroid Build Coastguard Worker  toolchain_data_file = os.path.join(os.path.dirname(__file__),
298*635a8641SAndroid Build Coastguard Worker                                     'win_toolchain.json')
299*635a8641SAndroid Build Coastguard Worker  if not os.path.exists(toolchain_data_file):
300*635a8641SAndroid Build Coastguard Worker    _LOGGER.debug('Toolchain JSON data file doesn\'t exist, skipping.')
301*635a8641SAndroid Build Coastguard Worker    return
302*635a8641SAndroid Build Coastguard Worker  with open(toolchain_data_file) as temp_f:
303*635a8641SAndroid Build Coastguard Worker    toolchain_data = json.load(temp_f)
304*635a8641SAndroid Build Coastguard Worker  if not os.path.isdir(toolchain_data['path']):
305*635a8641SAndroid Build Coastguard Worker    _LOGGER.error('The toolchain JSON file is invalid.')
306*635a8641SAndroid Build Coastguard Worker    return
307*635a8641SAndroid Build Coastguard Worker  dia_sdk_binaries_dir = os.path.join(toolchain_data['path'], 'DIA SDK', 'bin')
308*635a8641SAndroid Build Coastguard Worker  dia_dll = os.path.join(dia_sdk_binaries_dir, _DIA_DLL_NAME)
309*635a8641SAndroid Build Coastguard Worker  if not os.path.exists(dia_dll):
310*635a8641SAndroid Build Coastguard Worker    _LOGGER.debug('%s is missing, skipping.')
311*635a8641SAndroid Build Coastguard Worker    return
312*635a8641SAndroid Build Coastguard Worker  dia_dll_dest = os.path.join(options.output_dir, 'exe', _DIA_DLL_NAME)
313*635a8641SAndroid Build Coastguard Worker  _LOGGER.debug('Copying %s to %s.' % (dia_dll, dia_dll_dest))
314*635a8641SAndroid Build Coastguard Worker  if not options.dry_run:
315*635a8641SAndroid Build Coastguard Worker    shutil.copy(dia_dll, dia_dll_dest)
316*635a8641SAndroid Build Coastguard Worker    contents[os.path.relpath(dia_dll_dest, options.output_dir)] = (
317*635a8641SAndroid Build Coastguard Worker        _Md5(dia_dll_dest))
318*635a8641SAndroid Build Coastguard Worker
319*635a8641SAndroid Build Coastguard Worker
320*635a8641SAndroid Build Coastguard Workerdef _InstallBinaries(options, deleted={}):
321*635a8641SAndroid Build Coastguard Worker  """Installs Syzygy binaries. This assumes that the output directory has
322*635a8641SAndroid Build Coastguard Worker  already been cleaned, as it will refuse to overwrite existing files."""
323*635a8641SAndroid Build Coastguard Worker  contents = {}
324*635a8641SAndroid Build Coastguard Worker  state = { 'revision': options.revision, 'contents': contents }
325*635a8641SAndroid Build Coastguard Worker  archive_path = _SYZYGY_ARCHIVE_PATH % { 'revision': options.revision }
326*635a8641SAndroid Build Coastguard Worker  if options.resources:
327*635a8641SAndroid Build Coastguard Worker    resources = [(resource, resource, '', None)
328*635a8641SAndroid Build Coastguard Worker                 for resource in options.resources]
329*635a8641SAndroid Build Coastguard Worker  else:
330*635a8641SAndroid Build Coastguard Worker    resources = _RESOURCES
331*635a8641SAndroid Build Coastguard Worker  for (base, name, subdir, filt) in resources:
332*635a8641SAndroid Build Coastguard Worker    # Create the output directory if it doesn't exist.
333*635a8641SAndroid Build Coastguard Worker    fulldir = os.path.join(options.output_dir, subdir)
334*635a8641SAndroid Build Coastguard Worker    if os.path.isfile(fulldir):
335*635a8641SAndroid Build Coastguard Worker      raise Exception('File exists where a directory needs to be created: %s' %
336*635a8641SAndroid Build Coastguard Worker                      fulldir)
337*635a8641SAndroid Build Coastguard Worker    if not os.path.exists(fulldir):
338*635a8641SAndroid Build Coastguard Worker      _LOGGER.debug('Creating directory: %s', fulldir)
339*635a8641SAndroid Build Coastguard Worker      if not options.dry_run:
340*635a8641SAndroid Build Coastguard Worker        os.makedirs(fulldir)
341*635a8641SAndroid Build Coastguard Worker
342*635a8641SAndroid Build Coastguard Worker    # Download and read the archive.
343*635a8641SAndroid Build Coastguard Worker    resource = archive_path + '/' + base
344*635a8641SAndroid Build Coastguard Worker    _LOGGER.debug('Retrieving %s archive at "%s".', name, resource)
345*635a8641SAndroid Build Coastguard Worker    path = _Download(resource)
346*635a8641SAndroid Build Coastguard Worker
347*635a8641SAndroid Build Coastguard Worker    _LOGGER.debug('Unzipping %s archive.', name)
348*635a8641SAndroid Build Coastguard Worker    with open(path, 'rb') as data:
349*635a8641SAndroid Build Coastguard Worker      archive = zipfile.ZipFile(data)
350*635a8641SAndroid Build Coastguard Worker      for entry in archive.infolist():
351*635a8641SAndroid Build Coastguard Worker        if not filt or filt(entry):
352*635a8641SAndroid Build Coastguard Worker          fullpath = os.path.normpath(os.path.join(fulldir, entry.filename))
353*635a8641SAndroid Build Coastguard Worker          relpath = os.path.relpath(fullpath, options.output_dir)
354*635a8641SAndroid Build Coastguard Worker          if os.path.exists(fullpath):
355*635a8641SAndroid Build Coastguard Worker            # If in a dry-run take into account the fact that the file *would*
356*635a8641SAndroid Build Coastguard Worker            # have been deleted.
357*635a8641SAndroid Build Coastguard Worker            if options.dry_run and relpath in deleted:
358*635a8641SAndroid Build Coastguard Worker              pass
359*635a8641SAndroid Build Coastguard Worker            else:
360*635a8641SAndroid Build Coastguard Worker              raise Exception('Path already exists: %s' % fullpath)
361*635a8641SAndroid Build Coastguard Worker
362*635a8641SAndroid Build Coastguard Worker          # Extract the file and update the state dictionary.
363*635a8641SAndroid Build Coastguard Worker          _LOGGER.debug('Extracting "%s".', fullpath)
364*635a8641SAndroid Build Coastguard Worker          if not options.dry_run:
365*635a8641SAndroid Build Coastguard Worker            archive.extract(entry.filename, fulldir)
366*635a8641SAndroid Build Coastguard Worker            md5 = _Md5(fullpath)
367*635a8641SAndroid Build Coastguard Worker            contents[relpath] = md5
368*635a8641SAndroid Build Coastguard Worker            if sys.platform == 'cygwin':
369*635a8641SAndroid Build Coastguard Worker              os.chmod(fullpath, os.stat(fullpath).st_mode | stat.S_IXUSR)
370*635a8641SAndroid Build Coastguard Worker
371*635a8641SAndroid Build Coastguard Worker    _LOGGER.debug('Removing temporary file "%s".', path)
372*635a8641SAndroid Build Coastguard Worker    os.remove(path)
373*635a8641SAndroid Build Coastguard Worker
374*635a8641SAndroid Build Coastguard Worker  if options.copy_dia_binaries:
375*635a8641SAndroid Build Coastguard Worker    # Try to copy the DIA binaries to the binaries directory.
376*635a8641SAndroid Build Coastguard Worker    _MaybeCopyDIABinaries(options, contents)
377*635a8641SAndroid Build Coastguard Worker
378*635a8641SAndroid Build Coastguard Worker  return state
379*635a8641SAndroid Build Coastguard Worker
380*635a8641SAndroid Build Coastguard Worker
381*635a8641SAndroid Build Coastguard Workerdef _ParseCommandLine():
382*635a8641SAndroid Build Coastguard Worker  """Parses the command-line and returns an options structure."""
383*635a8641SAndroid Build Coastguard Worker  option_parser = optparse.OptionParser()
384*635a8641SAndroid Build Coastguard Worker  option_parser.add_option('--dry-run', action='store_true', default=False,
385*635a8641SAndroid Build Coastguard Worker      help='If true then will simply list actions that would be performed.')
386*635a8641SAndroid Build Coastguard Worker  option_parser.add_option('--force', action='store_true', default=False,
387*635a8641SAndroid Build Coastguard Worker      help='Force an installation even if the binaries are up to date.')
388*635a8641SAndroid Build Coastguard Worker  option_parser.add_option('--no-cleanup', action='store_true', default=False,
389*635a8641SAndroid Build Coastguard Worker      help='Allow installation on non-Windows platforms, and skip the forced '
390*635a8641SAndroid Build Coastguard Worker           'cleanup step.')
391*635a8641SAndroid Build Coastguard Worker  option_parser.add_option('--output-dir', type='string',
392*635a8641SAndroid Build Coastguard Worker      help='The path where the binaries will be replaced. Existing binaries '
393*635a8641SAndroid Build Coastguard Worker           'will only be overwritten if not up to date.')
394*635a8641SAndroid Build Coastguard Worker  option_parser.add_option('--overwrite', action='store_true', default=False,
395*635a8641SAndroid Build Coastguard Worker      help='If specified then the installation will happily delete and rewrite '
396*635a8641SAndroid Build Coastguard Worker           'the entire output directory, blasting any local changes.')
397*635a8641SAndroid Build Coastguard Worker  option_parser.add_option('--revision', type='string',
398*635a8641SAndroid Build Coastguard Worker      help='The SVN revision or GIT hash associated with the required version.')
399*635a8641SAndroid Build Coastguard Worker  option_parser.add_option('--revision-file', type='string',
400*635a8641SAndroid Build Coastguard Worker      help='A text file containing an SVN revision or GIT hash.')
401*635a8641SAndroid Build Coastguard Worker  option_parser.add_option('--resource', type='string', action='append',
402*635a8641SAndroid Build Coastguard Worker      dest='resources', help='A resource to be downloaded.')
403*635a8641SAndroid Build Coastguard Worker  option_parser.add_option('--verbose', dest='log_level', action='store_const',
404*635a8641SAndroid Build Coastguard Worker      default=logging.INFO, const=logging.DEBUG,
405*635a8641SAndroid Build Coastguard Worker      help='Enables verbose logging.')
406*635a8641SAndroid Build Coastguard Worker  option_parser.add_option('--quiet', dest='log_level', action='store_const',
407*635a8641SAndroid Build Coastguard Worker      default=logging.INFO, const=logging.ERROR,
408*635a8641SAndroid Build Coastguard Worker      help='Disables all output except for errors.')
409*635a8641SAndroid Build Coastguard Worker  option_parser.add_option('--copy-dia-binaries', action='store_true',
410*635a8641SAndroid Build Coastguard Worker      default=False, help='If true then the DIA dll will get copied into the '
411*635a8641SAndroid Build Coastguard Worker                          'binaries directory if it\'s available.')
412*635a8641SAndroid Build Coastguard Worker  options, args = option_parser.parse_args()
413*635a8641SAndroid Build Coastguard Worker  if args:
414*635a8641SAndroid Build Coastguard Worker    option_parser.error('Unexpected arguments: %s' % args)
415*635a8641SAndroid Build Coastguard Worker  if not options.output_dir:
416*635a8641SAndroid Build Coastguard Worker    option_parser.error('Must specify --output-dir.')
417*635a8641SAndroid Build Coastguard Worker  if not options.revision and not options.revision_file:
418*635a8641SAndroid Build Coastguard Worker    option_parser.error('Must specify one of --revision or --revision-file.')
419*635a8641SAndroid Build Coastguard Worker  if options.revision and options.revision_file:
420*635a8641SAndroid Build Coastguard Worker    option_parser.error('Must not specify both --revision and --revision-file.')
421*635a8641SAndroid Build Coastguard Worker
422*635a8641SAndroid Build Coastguard Worker  # Configure logging.
423*635a8641SAndroid Build Coastguard Worker  logging.basicConfig(level=options.log_level)
424*635a8641SAndroid Build Coastguard Worker
425*635a8641SAndroid Build Coastguard Worker  # If a revision file has been specified then read it.
426*635a8641SAndroid Build Coastguard Worker  if options.revision_file:
427*635a8641SAndroid Build Coastguard Worker    options.revision = open(options.revision_file, 'rb').read().strip()
428*635a8641SAndroid Build Coastguard Worker    _LOGGER.debug('Parsed revision "%s" from file "%s".',
429*635a8641SAndroid Build Coastguard Worker                 options.revision, options.revision_file)
430*635a8641SAndroid Build Coastguard Worker
431*635a8641SAndroid Build Coastguard Worker  # Ensure that the specified SVN revision or GIT hash is valid.
432*635a8641SAndroid Build Coastguard Worker  if not _REVISION_RE.match(options.revision):
433*635a8641SAndroid Build Coastguard Worker    option_parser.error('Must specify a valid SVN or GIT revision.')
434*635a8641SAndroid Build Coastguard Worker
435*635a8641SAndroid Build Coastguard Worker  # This just makes output prettier to read.
436*635a8641SAndroid Build Coastguard Worker  options.output_dir = os.path.normpath(options.output_dir)
437*635a8641SAndroid Build Coastguard Worker
438*635a8641SAndroid Build Coastguard Worker  return options
439*635a8641SAndroid Build Coastguard Worker
440*635a8641SAndroid Build Coastguard Worker
441*635a8641SAndroid Build Coastguard Workerdef _RemoveOrphanedFiles(options):
442*635a8641SAndroid Build Coastguard Worker  """This is run on non-Windows systems to remove orphaned files that may have
443*635a8641SAndroid Build Coastguard Worker  been downloaded by a previous version of this script.
444*635a8641SAndroid Build Coastguard Worker  """
445*635a8641SAndroid Build Coastguard Worker  # Reconfigure logging to output info messages. This will allow inspection of
446*635a8641SAndroid Build Coastguard Worker  # cleanup status on non-Windows buildbots.
447*635a8641SAndroid Build Coastguard Worker  _LOGGER.setLevel(logging.INFO)
448*635a8641SAndroid Build Coastguard Worker
449*635a8641SAndroid Build Coastguard Worker  output_dir = os.path.abspath(options.output_dir)
450*635a8641SAndroid Build Coastguard Worker
451*635a8641SAndroid Build Coastguard Worker  # We only want to clean up the folder in 'src/third_party/syzygy', and we
452*635a8641SAndroid Build Coastguard Worker  # expect to be called with that as an output directory. This is an attempt to
453*635a8641SAndroid Build Coastguard Worker  # not start deleting random things if the script is run from an alternate
454*635a8641SAndroid Build Coastguard Worker  # location, or not called from the gclient hooks.
455*635a8641SAndroid Build Coastguard Worker  expected_syzygy_dir = os.path.abspath(os.path.join(
456*635a8641SAndroid Build Coastguard Worker      os.path.dirname(__file__), '..', 'third_party', 'syzygy'))
457*635a8641SAndroid Build Coastguard Worker  expected_output_dir = os.path.join(expected_syzygy_dir, 'binaries')
458*635a8641SAndroid Build Coastguard Worker  if expected_output_dir != output_dir:
459*635a8641SAndroid Build Coastguard Worker    _LOGGER.info('Unexpected output directory, skipping cleanup.')
460*635a8641SAndroid Build Coastguard Worker    return
461*635a8641SAndroid Build Coastguard Worker
462*635a8641SAndroid Build Coastguard Worker  if not os.path.isdir(expected_syzygy_dir):
463*635a8641SAndroid Build Coastguard Worker    _LOGGER.info('Output directory does not exist, skipping cleanup.')
464*635a8641SAndroid Build Coastguard Worker    return
465*635a8641SAndroid Build Coastguard Worker
466*635a8641SAndroid Build Coastguard Worker  def OnError(function, path, excinfo):
467*635a8641SAndroid Build Coastguard Worker    """Logs error encountered by shutil.rmtree."""
468*635a8641SAndroid Build Coastguard Worker    _LOGGER.error('Error when running %s(%s)', function, path, exc_info=excinfo)
469*635a8641SAndroid Build Coastguard Worker
470*635a8641SAndroid Build Coastguard Worker  _LOGGER.info('Removing orphaned files from %s', expected_syzygy_dir)
471*635a8641SAndroid Build Coastguard Worker  if not options.dry_run:
472*635a8641SAndroid Build Coastguard Worker    shutil.rmtree(expected_syzygy_dir, True, OnError)
473*635a8641SAndroid Build Coastguard Worker
474*635a8641SAndroid Build Coastguard Worker
475*635a8641SAndroid Build Coastguard Workerdef main():
476*635a8641SAndroid Build Coastguard Worker  options = _ParseCommandLine()
477*635a8641SAndroid Build Coastguard Worker
478*635a8641SAndroid Build Coastguard Worker  if options.dry_run:
479*635a8641SAndroid Build Coastguard Worker    _LOGGER.debug('Performing a dry-run.')
480*635a8641SAndroid Build Coastguard Worker
481*635a8641SAndroid Build Coastguard Worker  # We only care about Windows platforms, as the Syzygy binaries aren't used
482*635a8641SAndroid Build Coastguard Worker  # elsewhere. However, there was a short period of time where this script
483*635a8641SAndroid Build Coastguard Worker  # wasn't gated on OS types, and those OSes downloaded and installed binaries.
484*635a8641SAndroid Build Coastguard Worker  # This will cleanup orphaned files on those operating systems.
485*635a8641SAndroid Build Coastguard Worker  if sys.platform not in ('win32', 'cygwin'):
486*635a8641SAndroid Build Coastguard Worker    if options.no_cleanup:
487*635a8641SAndroid Build Coastguard Worker      _LOGGER.debug('Skipping usual cleanup for non-Windows platforms.')
488*635a8641SAndroid Build Coastguard Worker    else:
489*635a8641SAndroid Build Coastguard Worker      return _RemoveOrphanedFiles(options)
490*635a8641SAndroid Build Coastguard Worker
491*635a8641SAndroid Build Coastguard Worker  # Load the current installation state, and validate it against the
492*635a8641SAndroid Build Coastguard Worker  # requested installation.
493*635a8641SAndroid Build Coastguard Worker  state, is_consistent = _GetCurrentState(options.revision, options.output_dir)
494*635a8641SAndroid Build Coastguard Worker
495*635a8641SAndroid Build Coastguard Worker  # Decide whether or not an install is necessary.
496*635a8641SAndroid Build Coastguard Worker  if options.force:
497*635a8641SAndroid Build Coastguard Worker    _LOGGER.debug('Forcing reinstall of binaries.')
498*635a8641SAndroid Build Coastguard Worker  elif is_consistent:
499*635a8641SAndroid Build Coastguard Worker    # Avoid doing any work if the contents of the directory are consistent.
500*635a8641SAndroid Build Coastguard Worker    _LOGGER.debug('State unchanged, no reinstall necessary.')
501*635a8641SAndroid Build Coastguard Worker    return
502*635a8641SAndroid Build Coastguard Worker
503*635a8641SAndroid Build Coastguard Worker  # Under normal logging this is the only only message that will be reported.
504*635a8641SAndroid Build Coastguard Worker  _LOGGER.info('Installing revision %s Syzygy binaries.',
505*635a8641SAndroid Build Coastguard Worker               options.revision[0:12])
506*635a8641SAndroid Build Coastguard Worker
507*635a8641SAndroid Build Coastguard Worker  # Clean up the old state to begin with.
508*635a8641SAndroid Build Coastguard Worker  deleted = []
509*635a8641SAndroid Build Coastguard Worker  if options.overwrite:
510*635a8641SAndroid Build Coastguard Worker    if os.path.exists(options.output_dir):
511*635a8641SAndroid Build Coastguard Worker      # If overwrite was specified then take a heavy-handed approach.
512*635a8641SAndroid Build Coastguard Worker      _LOGGER.debug('Deleting entire installation directory.')
513*635a8641SAndroid Build Coastguard Worker      if not options.dry_run:
514*635a8641SAndroid Build Coastguard Worker        _RmTree(options.output_dir)
515*635a8641SAndroid Build Coastguard Worker  else:
516*635a8641SAndroid Build Coastguard Worker    # Otherwise only delete things that the previous installation put in place,
517*635a8641SAndroid Build Coastguard Worker    # and take care to preserve any local changes.
518*635a8641SAndroid Build Coastguard Worker    deleted = _CleanState(options.output_dir, state, options.dry_run)
519*635a8641SAndroid Build Coastguard Worker
520*635a8641SAndroid Build Coastguard Worker  # Install the new binaries. In a dry-run this will actually download the
521*635a8641SAndroid Build Coastguard Worker  # archives, but it won't write anything to disk.
522*635a8641SAndroid Build Coastguard Worker  state = _InstallBinaries(options, deleted)
523*635a8641SAndroid Build Coastguard Worker
524*635a8641SAndroid Build Coastguard Worker  # Build and save the state for the directory.
525*635a8641SAndroid Build Coastguard Worker  _SaveState(options.output_dir, state, options.dry_run)
526*635a8641SAndroid Build Coastguard Worker
527*635a8641SAndroid Build Coastguard Worker
528*635a8641SAndroid Build Coastguard Workerif __name__ == '__main__':
529*635a8641SAndroid Build Coastguard Worker  main()
530