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