2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """Script that reads omahaproxy and gsutil to determine version of SDK to put
10 # pylint is convinced the email module is missing attributes
11 # pylint: disable=E1101
13 import buildbot_common
20 import logging.handlers
33 MANIFEST_BASENAME = 'naclsdk_manifest2.json'
34 SCRIPT_DIR = os.path.dirname(__file__)
35 REPO_MANIFEST = os.path.join(SCRIPT_DIR, 'json', MANIFEST_BASENAME)
36 GS_BUCKET_PATH = 'gs://nativeclient-mirror/nacl/nacl_sdk/'
37 GS_SDK_MANIFEST = GS_BUCKET_PATH + MANIFEST_BASENAME
38 GS_SDK_MANIFEST_LOG = GS_BUCKET_PATH + MANIFEST_BASENAME + '.log'
39 GS_MANIFEST_BACKUP_DIR = GS_BUCKET_PATH + 'manifest_backups/'
41 CANARY_BUNDLE_NAME = 'pepper_canary'
43 NACLPORTS_ARCHIVE_NAME = 'naclports.tar.bz2'
46 logger = logging.getLogger(__name__)
49 def SplitVersion(version_string):
50 """Split a version string (e.g. "18.0.1025.163") into its components.
52 Note that this function doesn't handle versions in the form "trunk.###".
54 return tuple(map(int, version_string.split('.')))
57 def JoinVersion(version_tuple):
58 """Create a string from a version tuple.
60 The tuple should be of the form (18, 0, 1025, 163).
62 return '.'.join(map(str, version_tuple))
65 def GetTimestampManifestName():
66 """Create a manifest name with a timestamp.
69 A manifest name with an embedded date. This should make it easier to roll
72 return time.strftime('naclsdk_manifest2.%Y_%m_%d_%H_%M_%S.json',
76 def GetPlatformArchiveName(platform):
77 """Get the basename of an archive given a platform string.
80 platform: One of ('win', 'mac', 'linux').
83 The basename of the sdk archive for that platform.
85 return 'naclsdk_%s.tar.bz2' % platform
88 def GetCanonicalArchiveName(url):
89 """Get the canonical name of an archive given its URL.
91 This will convert "naclsdk_linux.bz2" -> "naclsdk_linux.tar.bz2", and also
92 remove everything but the filename of the URL.
94 This is used below to determine if an expected bundle is found in an version
95 directory; the archives all have the same name, but may not exist for a given
99 url: The url to parse.
102 The canonical name as described above.
104 name = posixpath.basename(url)
105 match = re.match(r'naclsdk_(.*?)(?:\.tar)?\.bz2', name)
107 return 'naclsdk_%s.tar.bz2' % match.group(1)
112 class Delegate(object):
113 """Delegate all external access; reading/writing to filesystem, gsutil etc."""
115 def GetRepoManifest(self):
116 """Read the manifest file from the NaCl SDK repository.
118 This manifest is used as a template for the auto updater; only pepper
119 bundles with no archives are considered for auto updating.
122 A manifest_util.SDKManifest object read from the NaCl SDK repo."""
123 raise NotImplementedError()
125 def GetHistory(self):
126 """Read Chrome release history from omahaproxy.appspot.com
128 Here is an example of data from this URL:
129 cros,stable,18.0.1025.168,2012-05-01 17:04:05.962578\n
130 win,canary,20.0.1123.0,2012-05-01 13:59:31.703020\n
131 mac,canary,20.0.1123.0,2012-05-01 11:54:13.041875\n
132 win,stable,18.0.1025.168,2012-04-30 20:34:56.078490\n
133 mac,stable,18.0.1025.168,2012-04-30 20:34:55.231141\n
135 Where each line has comma separated values in the following format:
136 platform, channel, version, date/time\n
139 A list where each element is a line from the document, represented as a
141 raise NotImplementedError()
143 def GetTrunkRevision(self, version):
144 """Given a Chrome version, get its trunk revision.
147 version: A version string of the form '18.0.1025.64'
149 The revision number for that version, as a string."""
150 raise NotImplementedError()
152 def GsUtil_ls(self, url):
153 """Runs gsutil ls |url|
156 url: The commondatastorage url to list.
158 A list of URLs, all with the gs:// schema."""
159 raise NotImplementedError()
161 def GsUtil_cat(self, url):
162 """Runs gsutil cat |url|
165 url: The commondatastorage url to read from.
167 A string with the contents of the file at |url|."""
168 raise NotImplementedError()
170 def GsUtil_cp(self, src, dest, stdin=None):
171 """Runs gsutil cp |src| |dest|
174 src: The file path or url to copy from.
175 dest: The file path or url to copy to.
176 stdin: If src is '-', this is used as the stdin to give to gsutil. The
177 effect is that text in stdin is copied to |dest|."""
178 raise NotImplementedError()
180 def SendMail(self, subject, text):
184 subject: The subject of the email.
185 text: The text of the email.
187 raise NotImplementedError()
190 class RealDelegate(Delegate):
191 def __init__(self, dryrun=False, gsutil=None, mailfrom=None, mailto=None):
192 super(RealDelegate, self).__init__()
194 self.mailfrom = mailfrom
199 self.gsutil = buildbot_common.GetGsutil()
201 def GetRepoManifest(self):
202 """See Delegate.GetRepoManifest"""
203 with open(REPO_MANIFEST, 'r') as sdk_stream:
204 sdk_json_string = sdk_stream.read()
206 manifest = manifest_util.SDKManifest()
207 manifest.LoadDataFromString(sdk_json_string, add_missing_info=True)
210 def GetHistory(self):
211 """See Delegate.GetHistory"""
212 url_stream = urllib2.urlopen('https://omahaproxy.appspot.com/history')
213 return [(platform, channel, version, date)
214 for platform, channel, version, date in csv.reader(url_stream)]
216 def GetTrunkRevision(self, version):
217 """See Delegate.GetTrunkRevision"""
218 url = 'http://omahaproxy.appspot.com/revision.json?version=%s' % (version,)
219 data = json.loads(urllib2.urlopen(url).read())
220 return 'trunk.%s' % int(data['chromium_revision'])
222 def GsUtil_ls(self, url):
223 """See Delegate.GsUtil_ls"""
225 stdout = self._RunGsUtil(None, False, 'ls', url)
226 except subprocess.CalledProcessError:
229 # filter out empty lines
230 return filter(None, stdout.split('\n'))
232 def GsUtil_cat(self, url):
233 """See Delegate.GsUtil_cat"""
234 return self._RunGsUtil(None, True, 'cat', url)
236 def GsUtil_cp(self, src, dest, stdin=None):
237 """See Delegate.GsUtil_cp"""
239 logger.info("Skipping upload: %s -> %s" % (src, dest))
241 logger.info(' contents = """%s"""' % stdin)
244 return self._RunGsUtil(stdin, True, 'cp', '-a', 'public-read', src, dest)
246 def SendMail(self, subject, text):
247 """See Delegate.SendMail"""
248 if self.mailfrom and self.mailto:
249 msg = email.MIMEMultipart.MIMEMultipart()
250 msg['From'] = self.mailfrom
251 msg['To'] = ', '.join(self.mailto)
252 msg['Date'] = email.Utils.formatdate(localtime=True)
253 msg['Subject'] = subject
254 msg.attach(email.MIMEText.MIMEText(text))
255 smtp_obj = smtplib.SMTP('localhost')
256 smtp_obj.sendmail(self.mailfrom, self.mailto, msg.as_string())
259 def _RunGsUtil(self, stdin, log_errors, *args):
260 """Run gsutil as a subprocess.
263 stdin: If non-None, used as input to the process.
264 log_errors: If True, write errors to stderr.
265 *args: Arguments to pass to gsutil. The first argument should be an
266 operation such as ls, cp or cat.
268 The stdout from the process."""
269 cmd = [self.gsutil] + list(args)
270 logger.debug("Running: %s" % str(cmd))
272 stdin_pipe = subprocess.PIPE
277 process = subprocess.Popen(cmd, stdin=stdin_pipe, stdout=subprocess.PIPE,
278 stderr=subprocess.PIPE)
279 stdout, stderr = process.communicate(stdin)
281 raise manifest_util.Error("Unable to run '%s': %s" % (cmd[0], str(e)))
283 if process.returncode:
286 raise subprocess.CalledProcessError(process.returncode, ' '.join(cmd))
290 class GsutilLoggingHandler(logging.handlers.BufferingHandler):
291 def __init__(self, delegate):
292 logging.handlers.BufferingHandler.__init__(self, capacity=0)
293 self.delegate = delegate
295 def shouldFlush(self, record):
296 # BufferingHandler.shouldFlush automatically flushes if the length of the
297 # buffer is greater than self.capacity. We don't want that behavior, so
302 # Do nothing here. We want to be explicit about uploading the log.
307 for record in self.buffer:
308 output_list.append(self.format(record))
309 output = '\n'.join(output_list)
310 self.delegate.GsUtil_cp('-', GS_SDK_MANIFEST_LOG, stdin=output)
312 logging.handlers.BufferingHandler.flush(self)
315 class NoSharedVersionException(Exception):
319 class VersionFinder(object):
320 """Finds a version of a pepper bundle that all desired platforms share.
323 delegate: See Delegate class above.
324 platforms: A sequence of platforms to consider, e.g.
325 ('mac', 'linux', 'win')
326 extra_archives: A sequence of tuples: (archive_basename, minimum_version),
327 e.g. [('foo.tar.bz2', '18.0.1000.0'), ('bar.tar.bz2', '19.0.1100.20')]
328 These archives must exist to consider a version for inclusion, as
329 long as that version is greater than the archive's minimum version.
331 def __init__(self, delegate, platforms, extra_archives=None):
332 self.delegate = delegate
333 self.history = delegate.GetHistory()
334 self.platforms = platforms
335 self.extra_archives = extra_archives
337 def GetMostRecentSharedVersion(self, major_version):
338 """Returns the most recent version of a pepper bundle that exists on all
341 Specifically, the resulting version should be the most recently released
342 (meaning closest to the top of the listing on
343 omahaproxy.appspot.com/history) version that has a Chrome release on all
344 given platforms, and has a pepper bundle archive for each platform as well.
347 major_version: The major version of the pepper bundle, e.g. 19.
349 A tuple (version, channel, archives). The version is a string such as
350 "19.0.1084.41". The channel is one of ('stable', 'beta', or 'dev').
351 |archives| is a list of archive URLs."""
352 def GetPlatformHistory(platform):
353 return self._GetPlatformMajorVersionHistory(major_version, platform)
355 shared_version_generator = self._FindNextSharedVersion(self.platforms,
357 return self._DoGetMostRecentSharedVersion(shared_version_generator,
358 allow_trunk_revisions=False)
360 def GetMostRecentSharedCanary(self):
361 """Returns the most recent version of a canary pepper bundle that exists on
364 Canary is special-cased because we don't care about its major version; we
365 always use the most recent canary, regardless of major version.
368 A tuple (version, channel, archives). The version is a string such as
369 "19.0.1084.41". The channel is always 'canary'. |archives| is a list of
371 # Canary versions that differ in the last digit shouldn't be considered
372 # different; this number is typically used to represent an experiment, e.g.
373 # using ASAN or aura.
374 def CanaryKey(version):
377 # We don't ship canary on Linux, so it won't appear in self.history.
378 # Instead, we can use the matching Linux trunk build for that version.
379 shared_version_generator = self._FindNextSharedVersion(
380 set(self.platforms) - set(('linux',)),
381 self._GetPlatformCanaryHistory, CanaryKey)
382 return self._DoGetMostRecentSharedVersion(shared_version_generator,
383 allow_trunk_revisions=True)
385 def GetAvailablePlatformArchivesFor(self, version, allow_trunk_revisions):
386 """Returns a sequence of archives that exist for a given version, on the
389 The second element of the returned tuple is a list of all platforms that do
390 not have an archive for the given version.
393 version: The version to find archives for. (e.g. "18.0.1025.164")
394 allow_trunk_revisions: If True, will search for archives using the
395 trunk revision that matches the branch version.
397 A tuple (archives, missing_archives). |archives| is a list of archive
398 URLs, |missing_archives| is a list of archive names.
400 archive_urls = self._GetAvailableArchivesFor(version)
401 platform_archives = set(GetPlatformArchiveName(p) for p in self.platforms)
402 expected_archives = platform_archives
403 if self.extra_archives:
404 for extra_archive, extra_archive_min_version in self.extra_archives:
405 if SplitVersion(version) >= SplitVersion(extra_archive_min_version):
406 expected_archives.add(extra_archive)
407 found_archives = set(GetCanonicalArchiveName(a) for a in archive_urls)
408 missing_archives = expected_archives - found_archives
409 if allow_trunk_revisions and missing_archives:
410 # Try to find trunk versions of any missing archives.
411 trunk_version = self.delegate.GetTrunkRevision(version)
412 trunk_archives = self._GetAvailableArchivesFor(trunk_version)
413 for trunk_archive_url in trunk_archives:
414 trunk_archive = GetCanonicalArchiveName(trunk_archive_url)
415 if trunk_archive in missing_archives:
416 archive_urls.append(trunk_archive_url)
417 missing_archives.discard(trunk_archive)
419 # Only return archives that are "expected".
421 return GetCanonicalArchiveName(url) in expected_archives
423 expected_archive_urls = [u for u in archive_urls if IsExpected(u)]
424 return expected_archive_urls, missing_archives
426 def _DoGetMostRecentSharedVersion(self, shared_version_generator,
427 allow_trunk_revisions):
428 """Returns the most recent version of a pepper bundle that exists on all
431 This function does the real work for the public GetMostRecentShared* above.
434 shared_version_generator: A generator that will yield (version, channel)
435 tuples in order of most recent to least recent.
436 allow_trunk_revisions: If True, will search for archives using the
437 trunk revision that matches the branch version.
439 A tuple (version, channel, archives). The version is a string such as
440 "19.0.1084.41". The channel is one of ('stable', 'beta', 'dev',
441 'canary'). |archives| is a list of archive URLs."""
443 skipped_versions = []
447 version, channel = shared_version_generator.next()
448 except StopIteration:
449 msg = 'No shared version for platforms: %s\n' % (
450 ', '.join(self.platforms))
451 msg += 'Last version checked = %s.\n' % (version,)
453 msg += 'Versions skipped due to missing archives:\n'
454 for version, channel, missing_archives in skipped_versions:
455 archive_msg = '(missing %s)' % (', '.join(missing_archives))
456 msg += ' %s (%s) %s\n' % (version, channel, archive_msg)
457 raise NoSharedVersionException(msg)
459 logger.info('Found shared version: %s, channel: %s' % (
462 archives, missing_archives = self.GetAvailablePlatformArchivesFor(
463 version, allow_trunk_revisions)
465 if not missing_archives:
466 return version, channel, archives
468 logger.info(' skipping. Missing archives: %s' % (
469 ', '.join(missing_archives)))
471 skipped_versions.append((version, channel, missing_archives))
473 def _GetPlatformMajorVersionHistory(self, with_major_version, with_platform):
474 """Yields Chrome history for a given platform and major version.
477 with_major_version: The major version to filter for. If 0, match all
479 with_platform: The name of the platform to filter for.
481 A generator that yields a tuple (channel, version) for each version that
482 matches the platform and major version. The version returned is a tuple as
483 returned from SplitVersion.
485 for platform, channel, version, _ in self.history:
486 version = SplitVersion(version)
487 if (with_platform == platform and
488 (with_major_version == 0 or with_major_version == version[0])):
489 yield channel, version
491 def _GetPlatformCanaryHistory(self, with_platform):
492 """Yields Chrome history for a given platform, but only for canary
496 with_platform: The name of the platform to filter for.
498 A generator that yields a tuple (channel, version) for each version that
499 matches the platform and uses the canary channel. The version returned is
500 a tuple as returned from SplitVersion.
502 for platform, channel, version, _ in self.history:
503 version = SplitVersion(version)
504 if with_platform == platform and channel == CANARY:
505 yield channel, version
508 def _FindNextSharedVersion(self, platforms, generator_func, key_func=None):
509 """Yields versions of Chrome that exist on all given platforms, in order of
512 Versions are compared in reverse order of release. That is, the most
513 recently updated version will be tested first.
516 platforms: A sequence of platforms to consider, e.g.
517 ('mac', 'linux', 'win')
518 generator_func: A function which takes a platform and returns a
519 generator that yields (channel, version) tuples.
520 key_func: A function to convert the version into a value that should be
521 used for comparison. See python built-in sorted() or min(), for
524 A generator that yields a tuple (version, channel) for each version that
525 matches all platforms and the major version. The version returned is a
526 string (e.g. "18.0.1025.164").
529 key_func = lambda x: x
531 platform_generators = []
532 for platform in platforms:
533 platform_generators.append(generator_func(platform))
535 shared_version = None
536 platform_versions = []
537 for platform_gen in platform_generators:
538 platform_versions.append(platform_gen.next())
541 if logger.isEnabledFor(logging.INFO):
543 for i, platform in enumerate(platforms):
544 msg_info.append('%s: %s' % (
545 platform, JoinVersion(platform_versions[i][1])))
546 logger.info('Checking versions: %s' % ', '.join(msg_info))
548 shared_version = min((v for c, v in platform_versions), key=key_func)
550 if all(key_func(v) == key_func(shared_version)
551 for c, v in platform_versions):
552 # The real shared_version should be the real minimum version. This will
553 # be different from shared_version above only if key_func compares two
554 # versions with different values as equal.
555 min_version = min((v for c, v in platform_versions))
557 # grab the channel from an arbitrary platform
558 first_platform = platform_versions[0]
559 channel = first_platform[0]
560 yield JoinVersion(min_version), channel
562 # force increment to next version for all platforms
563 shared_version = None
565 # Find the next version for any platform that isn't at the shared version.
567 for i, platform_gen in enumerate(platform_generators):
568 if platform_versions[i][1] != shared_version:
569 platform_versions[i] = platform_gen.next()
570 except StopIteration:
574 def _GetAvailableArchivesFor(self, version_string):
575 """Downloads a list of all available archives for a given version.
578 version_string: The version to find archives for. (e.g. "18.0.1025.164")
580 A list of strings, each of which is a platform-specific archive URL. (e.g.
581 "gs://nativeclient_mirror/nacl/nacl_sdk/18.0.1025.164/"
582 "naclsdk_linux.tar.bz2").
584 All returned URLs will use the gs:// schema."""
585 files = self.delegate.GsUtil_ls(GS_BUCKET_PATH + version_string)
587 assert all(file.startswith('gs://') for file in files)
589 archives = [f for f in files if not f.endswith('.json')]
590 manifests = [f for f in files if f.endswith('.json')]
592 # don't include any archives that don't have an associated manifest.
593 return filter(lambda a: a + '.json' in manifests, archives)
596 class UnknownLockedBundleException(Exception):
600 class Updater(object):
601 def __init__(self, delegate):
602 self.delegate = delegate
603 self.versions_to_update = []
604 self.locked_bundles = []
605 self.online_manifest = manifest_util.SDKManifest()
606 self._FetchOnlineManifest()
608 def AddVersionToUpdate(self, bundle_name, version, channel, archives):
609 """Add a pepper version to update in the uploaded manifest.
612 bundle_name: The name of the pepper bundle, e.g. 'pepper_18'
613 version: The version of the pepper bundle, e.g. '18.0.1025.64'
614 channel: The stability of the pepper bundle, e.g. 'beta'
615 archives: A sequence of archive URLs for this bundle."""
616 self.versions_to_update.append((bundle_name, version, channel, archives))
618 def AddLockedBundle(self, bundle_name):
619 """Add a "locked" bundle to the updater.
621 A locked bundle is a bundle that wasn't found in the history. When this
622 happens, the bundle is now "locked" to whatever was last found. We want to
623 ensure that the online manifest has this bundle.
626 bundle_name: The name of the locked bundle.
628 self.locked_bundles.append(bundle_name)
630 def Update(self, manifest):
631 """Update a manifest and upload it.
633 Note that bundles will not be updated if the current version is newer.
634 That is, the updater will never automatically update to an older version of
638 manifest: The manifest used as a template for updating. Only pepper
639 bundles that contain no archives will be considered for auto-updating."""
640 # Make sure there is only one stable branch: the one with the max version.
641 # All others are post-stable.
642 stable_major_versions = [SplitVersion(version)[0] for _, version, channel, _
643 in self.versions_to_update if channel == 'stable']
644 # Add 0 in case there are no stable versions.
645 max_stable_version = max([0] + stable_major_versions)
647 # Ensure that all locked bundles exist in the online manifest.
648 for bundle_name in self.locked_bundles:
649 online_bundle = self.online_manifest.GetBundle(bundle_name)
651 manifest.SetBundle(online_bundle)
653 msg = ('Attempted to update bundle "%s", but no shared versions were '
654 'found, and there is no online bundle with that name.')
655 raise UnknownLockedBundleException(msg % bundle_name)
657 if self.locked_bundles:
658 # Send a nagging email that we shouldn't be wasting time looking for
659 # bundles that are no longer in the history.
660 scriptname = os.path.basename(sys.argv[0])
661 subject = '[%s] Reminder: remove bundles from %s' % (scriptname,
663 text = 'These bundles are not in the omahaproxy history anymore: ' + \
664 ', '.join(self.locked_bundles)
665 self.delegate.SendMail(subject, text)
668 # Update all versions.
669 logger.info('>>> Updating bundles...')
670 for bundle_name, version, channel, archives in self.versions_to_update:
671 logger.info('Updating %s to %s...' % (bundle_name, version))
672 bundle = manifest.GetBundle(bundle_name)
673 for archive in archives:
674 platform_bundle = self._GetPlatformArchiveBundle(archive)
675 # Normally the manifest snippet's bundle name matches our bundle name.
676 # pepper_canary, however is called "pepper_###" in the manifest
678 platform_bundle.name = bundle_name
679 bundle.MergeWithBundle(platform_bundle)
681 # Fix the stability and recommended values
682 major_version = SplitVersion(version)[0]
683 if major_version < max_stable_version:
684 bundle.stability = 'post_stable'
686 bundle.stability = channel
687 # We always recommend the stable version.
688 if bundle.stability == 'stable':
689 bundle.recommended = 'yes'
691 bundle.recommended = 'no'
693 # Check to ensure this bundle is newer than the online bundle.
694 online_bundle = self.online_manifest.GetBundle(bundle_name)
696 # This test used to be online_bundle.revision >= bundle.revision.
697 # That doesn't do quite what we want: sometimes the metadata changes
698 # but the revision stays the same -- we still want to push those
700 if online_bundle.revision > bundle.revision or online_bundle == bundle:
702 ' Revision %s is not newer than than online revision %s. '
703 'Skipping.' % (bundle.revision, online_bundle.revision))
705 manifest.SetBundle(online_bundle)
707 self._UploadManifest(manifest)
710 def _GetPlatformArchiveBundle(self, archive):
711 """Downloads the manifest "snippet" for an archive, and reads it as a
715 archive: A full URL of a platform-specific archive, using the gs schema.
717 An object of type manifest_util.Bundle, read from a JSON file storing
718 metadata for this archive.
720 stdout = self.delegate.GsUtil_cat(archive + '.json')
721 bundle = manifest_util.Bundle('')
722 bundle.LoadDataFromString(stdout)
723 # Some snippets were uploaded with revisions and versions as strings. Fix
725 bundle.revision = int(bundle.revision)
726 bundle.version = int(bundle.version)
728 # HACK. The naclports archive specifies host_os as linux. Change it to all.
729 for archive in bundle.GetArchives():
730 if NACLPORTS_ARCHIVE_NAME in archive.url:
731 archive.host_os = 'all'
734 def _UploadManifest(self, manifest):
735 """Upload a serialized manifest_util.SDKManifest object.
737 Upload one copy to gs://<BUCKET_PATH>/naclsdk_manifest2.json, and a copy to
738 gs://<BUCKET_PATH>/manifest_backups/naclsdk_manifest2.<TIMESTAMP>.json.
741 manifest: The new manifest to upload.
743 new_manifest_string = manifest.GetDataAsString()
744 online_manifest_string = self.online_manifest.GetDataAsString()
746 if self.delegate.dryrun:
747 logger.info(''.join(list(difflib.unified_diff(
748 online_manifest_string.splitlines(1),
749 new_manifest_string.splitlines(1)))))
752 online_manifest = manifest_util.SDKManifest()
753 online_manifest.LoadDataFromString(online_manifest_string)
755 if online_manifest == manifest:
756 logger.info('New manifest doesn\'t differ from online manifest.'
760 timestamp_manifest_path = GS_MANIFEST_BACKUP_DIR + \
761 GetTimestampManifestName()
762 self.delegate.GsUtil_cp('-', timestamp_manifest_path,
763 stdin=manifest.GetDataAsString())
765 # copy from timestampped copy over the official manifest.
766 self.delegate.GsUtil_cp(timestamp_manifest_path, GS_SDK_MANIFEST)
768 def _FetchOnlineManifest(self):
770 online_manifest_string = self.delegate.GsUtil_cat(GS_SDK_MANIFEST)
771 except subprocess.CalledProcessError:
772 # It is not a failure if the online manifest doesn't exist.
773 online_manifest_string = ''
775 if online_manifest_string:
776 self.online_manifest.LoadDataFromString(online_manifest_string)
779 def Run(delegate, platforms, extra_archives, fixed_bundle_versions=None):
780 """Entry point for the auto-updater.
783 delegate: The Delegate object to use for reading Urls, files, etc.
784 platforms: A sequence of platforms to consider, e.g.
785 ('mac', 'linux', 'win')
786 extra_archives: A sequence of tuples: (archive_basename, minimum_version),
787 e.g. [('foo.tar.bz2', '18.0.1000.0'), ('bar.tar.bz2', '19.0.1100.20')]
788 These archives must exist to consider a version for inclusion, as
789 long as that version is greater than the archive's minimum version.
790 fixed_bundle_versions: A sequence of tuples (bundle_name, version_string).
791 e.g. ('pepper_21', '21.0.1145.0')
793 if fixed_bundle_versions:
794 fixed_bundle_versions = dict(fixed_bundle_versions)
796 fixed_bundle_versions = {}
798 manifest = delegate.GetRepoManifest()
799 auto_update_bundles = []
800 for bundle in manifest.GetBundles():
801 if not bundle.name.startswith('pepper_'):
803 archives = bundle.GetArchives()
805 auto_update_bundles.append(bundle)
807 if not auto_update_bundles:
808 logger.info('No versions need auto-updating.')
811 version_finder = VersionFinder(delegate, platforms, extra_archives)
812 updater = Updater(delegate)
814 for bundle in auto_update_bundles:
816 if bundle.name == CANARY_BUNDLE_NAME:
817 logger.info('>>> Looking for most recent pepper_canary...')
818 version, channel, archives = version_finder.GetMostRecentSharedCanary()
820 logger.info('>>> Looking for most recent pepper_%s...' %
822 version, channel, archives = version_finder.GetMostRecentSharedVersion(
824 except NoSharedVersionException:
825 # If we can't find a shared version, make sure that there is an uploaded
826 # bundle with that name already.
827 updater.AddLockedBundle(bundle.name)
830 if bundle.name in fixed_bundle_versions:
831 # Ensure this version is valid for all platforms.
832 # If it is, use the channel found above (because the channel for this
833 # version may not be in the history.)
834 version = fixed_bundle_versions[bundle.name]
835 logger.info('Fixed bundle version: %s, %s' % (bundle.name, version))
836 allow_trunk_revisions = bundle.name == CANARY_BUNDLE_NAME
837 archives, missing = version_finder.GetAvailablePlatformArchivesFor(
838 version, allow_trunk_revisions)
841 'Some archives for version %s of bundle %s don\'t exist: '
842 'Missing %s' % (version, bundle.name, ', '.join(missing)))
845 updater.AddVersionToUpdate(bundle.name, version, channel, archives)
847 updater.Update(manifest)
850 class CapturedFile(object):
851 """A file-like object that captures text written to it, but also passes it
852 through to an underlying file-like object."""
853 def __init__(self, passthrough):
854 self.passthrough = passthrough
855 self.written = cStringIO.StringIO()
858 self.written.write(s)
860 self.passthrough.write(s)
863 return self.written.getvalue()
867 parser = optparse.OptionParser()
868 parser.add_option('--gsutil', help='path to gsutil.')
869 parser.add_option('-d', '--debug', help='run in debug mode.',
871 parser.add_option('--mailfrom', help='email address of sender.')
872 parser.add_option('--mailto', help='send error mails to...', action='append')
873 parser.add_option('-n', '--dryrun', help="don't upload the manifest.",
875 parser.add_option('-v', '--verbose', help='print more diagnotic messages. '
876 'Use more than once for more info.',
878 parser.add_option('--log-file', metavar='FILE', help='log to FILE')
879 parser.add_option('--upload-log', help='Upload log alongside the manifest.',
881 parser.add_option('--bundle-version',
882 help='Manually set a bundle version. This can be passed more than once. '
883 'format: --bundle-version pepper_24=24.0.1312.25', action='append')
884 options, args = parser.parse_args(args[1:])
886 if (options.mailfrom is None) != (not options.mailto):
887 options.mailfrom = None
888 options.mailto = None
889 logger.warning('Disabling email, one of --mailto or --mailfrom '
892 if options.verbose >= 2:
893 logging.basicConfig(level=logging.DEBUG, filename=options.log_file)
894 elif options.verbose:
895 logging.basicConfig(level=logging.INFO, filename=options.log_file)
897 logging.basicConfig(level=logging.WARNING, filename=options.log_file)
899 # Parse bundle versions.
900 fixed_bundle_versions = {}
901 if options.bundle_version:
902 for bundle_version_string in options.bundle_version:
903 bundle_name, version = bundle_version_string.split('=')
904 fixed_bundle_versions[bundle_name] = version
906 if options.mailfrom and options.mailto:
907 # Capture stderr so it can be emailed, if necessary.
908 sys.stderr = CapturedFile(sys.stderr)
912 delegate = RealDelegate(options.dryrun, options.gsutil,
913 options.mailfrom, options.mailto)
915 if options.upload_log:
916 gsutil_logging_handler = GsutilLoggingHandler(delegate)
917 logger.addHandler(gsutil_logging_handler)
919 # Only look for naclports archives >= 27. The old ports bundles don't
920 # include license information.
921 extra_archives = [('naclports.tar.bz2', '27.0.0.0')]
922 Run(delegate, ('mac', 'win', 'linux'), extra_archives,
923 fixed_bundle_versions)
926 if options.mailfrom and options.mailto:
927 traceback.print_exc()
928 scriptname = os.path.basename(sys.argv[0])
929 subject = '[%s] Failed to update manifest' % (scriptname,)
930 text = '%s failed.\n\nSTDERR:\n%s\n' % (scriptname,
931 sys.stderr.getvalue())
932 delegate.SendMail(subject, text)
937 if options.upload_log:
938 gsutil_logging_handler.upload()
939 except manifest_util.Error as e:
942 sys.stderr.write(str(e) + '\n')
946 if __name__ == '__main__':
947 sys.exit(main(sys.argv))