OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright 2014 The Chromium Authors. All rights reserved. | 2 # Copyright 2014 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """A utility script for downloading versioned Syzygy binaries.""" | 6 """A utility script for downloading versioned Syzygy binaries.""" |
7 | 7 |
8 import cStringIO | 8 import cStringIO |
9 import hashlib | 9 import hashlib |
10 import errno | |
11 import json | 10 import json |
12 import logging | 11 import logging |
13 import optparse | 12 import optparse |
14 import os | 13 import os |
15 import re | 14 import re |
16 import shutil | 15 import shutil |
17 import stat | |
18 import sys | |
19 import subprocess | 16 import subprocess |
20 import urllib2 | 17 import urllib2 |
21 import zipfile | 18 import zipfile |
22 | 19 |
23 | 20 |
24 _LOGGER = logging.getLogger(os.path.basename(__file__)) | 21 _LOGGER = logging.getLogger(os.path.basename(__file__)) |
25 | 22 |
26 # The URL where official builds are archived. | 23 # The URL where official builds are archived. |
27 _SYZYGY_ARCHIVE_URL = ('http://syzygy-archive.commondatastorage.googleapis.com/' | 24 _SYZYGY_ARCHIVE_URL = ('http://syzygy-archive.commondatastorage.googleapis.com/' |
28 'builds/official/%(revision)s') | 25 'builds/official/%(revision)s') |
(...skipping 139 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
168 return (stored, False) | 165 return (stored, False) |
169 return (stored, True) | 166 return (stored, True) |
170 | 167 |
171 | 168 |
172 def _DirIsEmpty(path): | 169 def _DirIsEmpty(path): |
173 """Returns true if the given directory is empty, false otherwise.""" | 170 """Returns true if the given directory is empty, false otherwise.""" |
174 for root, dirs, files in os.walk(path): | 171 for root, dirs, files in os.walk(path): |
175 return not dirs and not files | 172 return not dirs and not files |
176 | 173 |
177 | 174 |
178 def _RmTreeHandleReadOnly(func, path, exc): | |
179 """An error handling function for use with shutil.rmtree. This will | |
180 detect failures to remove read-only files, and will change their properties | |
181 prior to removing them. This is necessary on Windows as os.remove will return | |
182 an access error for read-only files, and git repos contain read-only | |
183 pack/index files. | |
184 """ | |
185 excvalue = exc[1] | |
186 if func in (os.rmdir, os.remove) and excvalue.errno == errno.EACCES: | |
187 _LOGGER.debug('Removing read-only path: %s', path) | |
188 os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) | |
189 func(path) | |
190 else: | |
191 raise | |
192 | |
193 | |
194 def _RmTree(path): | |
195 """A wrapper of shutil.rmtree that handles read-only files.""" | |
196 shutil.rmtree(path, ignore_errors=False, onerror=_RmTreeHandleReadOnly) | |
197 | |
198 | |
199 def _CleanState(output_dir, state, dry_run=False): | 175 def _CleanState(output_dir, state, dry_run=False): |
200 """Cleans up files/directories in |output_dir| that are referenced by | 176 """Cleans up files/directories in |output_dir| that are referenced by |
201 the given |state|. Raises an error if there are local changes. Returns a | 177 the given |state|. Raises an error if there are local changes. Returns a |
202 dictionary of files that were deleted. | 178 dictionary of files that were deleted. |
203 """ | 179 """ |
204 _LOGGER.debug('Deleting files from previous installation.') | 180 _LOGGER.debug('Deleting files from previous installation.') |
205 deleted = {} | 181 deleted = {} |
206 | 182 |
207 # Generate a list of files to delete, relative to |output_dir|. | 183 # Generate a list of files to delete, relative to |output_dir|. |
208 contents = state['contents'] | 184 contents = state['contents'] |
(...skipping 24 matching lines...) Expand all Loading... |
233 if not dry_run: | 209 if not dry_run: |
234 os.unlink(fullpath) | 210 os.unlink(fullpath) |
235 | 211 |
236 # Sort directories from longest name to shortest. This lets us remove empty | 212 # Sort directories from longest name to shortest. This lets us remove empty |
237 # directories from the most nested paths first. | 213 # directories from the most nested paths first. |
238 dirs = sorted(dirs.keys(), key=lambda x: len(x), reverse=True) | 214 dirs = sorted(dirs.keys(), key=lambda x: len(x), reverse=True) |
239 for p in dirs: | 215 for p in dirs: |
240 if os.path.exists(p) and _DirIsEmpty(p): | 216 if os.path.exists(p) and _DirIsEmpty(p): |
241 _LOGGER.debug('Deleting empty directory "%s".', p) | 217 _LOGGER.debug('Deleting empty directory "%s".', p) |
242 if not dry_run: | 218 if not dry_run: |
243 _RmTree(p) | 219 shutil.rmtree(p, False) |
244 | 220 |
245 return deleted | 221 return deleted |
246 | 222 |
247 | 223 |
248 def _Download(url): | 224 def _Download(url): |
249 """Downloads the given URL and returns the contents as a string.""" | 225 """Downloads the given URL and returns the contents as a string.""" |
250 response = urllib2.urlopen(url) | 226 response = urllib2.urlopen(url) |
251 if response.code != 200: | 227 if response.code != 200: |
252 raise RuntimeError('Failed to download "%s".' % url) | 228 raise RuntimeError('Failed to download "%s".' % url) |
253 return response.read() | 229 return response.read() |
(...skipping 91 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
345 if not _REVISION_RE.match(options.revision): | 321 if not _REVISION_RE.match(options.revision): |
346 option_parser.error('Must specify a valid SVN or GIT revision.') | 322 option_parser.error('Must specify a valid SVN or GIT revision.') |
347 | 323 |
348 # This just makes output prettier to read. | 324 # This just makes output prettier to read. |
349 options.output_dir = os.path.normpath(options.output_dir) | 325 options.output_dir = os.path.normpath(options.output_dir) |
350 | 326 |
351 return options | 327 return options |
352 | 328 |
353 | 329 |
354 def main(): | 330 def main(): |
355 # We only care about Windows platforms, as the Syzygy binaries aren't used | |
356 # elsewhere. | |
357 if sys.platform != 'win32': | |
358 return | |
359 | |
360 options = _ParseCommandLine() | 331 options = _ParseCommandLine() |
361 | 332 |
362 if options.dry_run: | 333 if options.dry_run: |
363 _LOGGER.debug('Performing a dry-run.') | 334 _LOGGER.debug('Performing a dry-run.') |
364 | 335 |
365 # Load the current installation state, and validate it against the | 336 # Load the current installation state, and validate it against the |
366 # requested installation. | 337 # requested installation. |
367 state, is_consistent = _GetCurrentState(options.revision, options.output_dir) | 338 state, is_consistent = _GetCurrentState(options.revision, options.output_dir) |
368 | 339 |
369 # Decide whether or not an install is necessary. | 340 # Decide whether or not an install is necessary. |
370 if options.force: | 341 if options.force: |
371 _LOGGER.debug('Forcing reinstall of binaries.') | 342 _LOGGER.debug('Forcing reinstall of binaries.') |
372 elif is_consistent: | 343 elif is_consistent: |
373 # Avoid doing any work if the contents of the directory are consistent. | 344 # Avoid doing any work if the contents of the directory are consistent. |
374 _LOGGER.debug('State unchanged, no reinstall necessary.') | 345 _LOGGER.debug('State unchanged, no reinstall necessary.') |
375 return | 346 return |
376 | 347 |
377 # Under normal logging this is the only only message that will be reported. | 348 # Under normal logging this is the only only message that will be reported. |
378 _LOGGER.info('Installing revision %s Syzygy binaries.', | 349 _LOGGER.info('Installing revision %s Syzygy binaries.', |
379 options.revision[0:12]) | 350 options.revision[0:12]) |
380 | 351 |
381 # Clean up the old state to begin with. | 352 # Clean up the old state to begin with. |
382 deleted = [] | 353 deleted = [] |
383 if options.overwrite: | 354 if options.overwrite: |
384 if os.path.exists(options.output_dir): | 355 if os.path.exists(options.output_dir): |
385 # If overwrite was specified then take a heavy-handed approach. | 356 # If overwrite was specified then take a heavy-handed approach. |
386 _LOGGER.debug('Deleting entire installation directory.') | 357 _LOGGER.debug('Deleting entire installation directory.') |
387 if not options.dry_run: | 358 if not options.dry_run: |
388 _RmTree(options.output_dir) | 359 shutil.rmtree(options.output_dir, False) |
389 else: | 360 else: |
390 # Otherwise only delete things that the previous installation put in place, | 361 # Otherwise only delete things that the previous installation put in place, |
391 # and take care to preserve any local changes. | 362 # and take care to preserve any local changes. |
392 deleted = _CleanState(options.output_dir, state, options.dry_run) | 363 deleted = _CleanState(options.output_dir, state, options.dry_run) |
393 | 364 |
394 # Install the new binaries. In a dry-run this will actually download the | 365 # Install the new binaries. In a dry-run this will actually download the |
395 # archives, but it won't write anything to disk. | 366 # archives, but it won't write anything to disk. |
396 state = _InstallBinaries(options, deleted) | 367 state = _InstallBinaries(options, deleted) |
397 | 368 |
398 # Build and save the state for the directory. | 369 # Build and save the state for the directory. |
399 _SaveState(options.output_dir, state, options.dry_run) | 370 _SaveState(options.output_dir, state, options.dry_run) |
400 | 371 |
401 | 372 |
402 if __name__ == '__main__': | 373 if __name__ == '__main__': |
403 main() | 374 main() |
OLD | NEW |