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