| #!/usr/bin/env python |
| # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Script that reads omahaproxy and gsutil to determine version of SDK to put |
| in manifest. |
| """ |
| |
| # pylint is convinced the email module is missing attributes |
| # pylint: disable=E1101 |
| |
| import argparse |
| import buildbot_common |
| import csv |
| import cStringIO |
| import difflib |
| import email |
| import logging |
| import logging.handlers |
| import manifest_util |
| import os |
| import posixpath |
| import re |
| import smtplib |
| import subprocess |
| import sys |
| import time |
| import traceback |
| import urllib2 |
| |
| MANIFEST_BASENAME = 'naclsdk_manifest2.json' |
| SCRIPT_DIR = os.path.dirname(__file__) |
| REPO_MANIFEST = os.path.join(SCRIPT_DIR, 'json', MANIFEST_BASENAME) |
| GS_BUCKET_PATH = 'gs://nativeclient-mirror/nacl/nacl_sdk/' |
| GS_SDK_MANIFEST = GS_BUCKET_PATH + MANIFEST_BASENAME |
| GS_SDK_MANIFEST_LOG = GS_BUCKET_PATH + MANIFEST_BASENAME + '.log' |
| GS_MANIFEST_BACKUP_DIR = GS_BUCKET_PATH + 'manifest_backups/' |
| |
| CANARY_BUNDLE_NAME = 'pepper_canary' |
| CANARY = 'canary' |
| NACLPORTS_ARCHIVE_NAME = 'naclports.tar.bz2' |
| |
| |
| logger = logging.getLogger(__name__) |
| |
| |
| def SplitVersion(version_string): |
| """Split a version string (e.g. "18.0.1025.163") into its components. |
| |
| e.g. |
| SplitVersion("trunk.123456") => ("trunk", "123456") |
| SplitVersion("18.0.1025.163") => (18, 0, 1025, 163) |
| """ |
| parts = version_string.split('.') |
| if parts[0] == 'trunk': |
| return (parts[0], int(parts[1])) |
| return tuple([int(p) for p in parts]) |
| |
| |
| def GetMajorVersion(version_string): |
| """Get the major version number from a version string (e.g. "18.0.1025.163"). |
| |
| e.g. |
| GetMajorVersion("trunk.123456") => "trunk" |
| GetMajorVersion("18.0.1025.163") => 18 |
| """ |
| return SplitVersion(version_string)[0] |
| |
| |
| def CompareVersions(version1, version2): |
| """Compare two version strings and return -1, 0, 1 (similar to cmp). |
| |
| Versions can only be compared if they are both trunk versions, or neither is. |
| |
| e.g. |
| CompareVersions("trunk.123", "trunk.456") => -1 |
| CompareVersions("18.0.1025.163", "37.0.2054.3") => -1 |
| CompareVersions("trunk.123", "18.0.1025.163") => Error |
| |
| """ |
| split1 = SplitVersion(version1) |
| split2 = SplitVersion(version2) |
| if split1[0] == split2[0]: |
| return cmp(split1[1:], split2[1:]) |
| |
| if split1 == 'trunk' or split2 == 'trunk': |
| raise Exception("Unable to compare versions %s and %s" % ( |
| version1, version2)) |
| |
| return cmp(split1, split2) |
| |
| |
| def JoinVersion(version_tuple): |
| """Create a string from a version tuple. |
| |
| The tuple should be of the form (18, 0, 1025, 163). |
| """ |
| assert len(version_tuple) == 4 |
| assert version_tuple[0] != 'trunk' |
| return '.'.join(map(str, version_tuple)) |
| |
| |
| def GetTimestampManifestName(): |
| """Create a manifest name with a timestamp. |
| |
| Returns: |
| A manifest name with an embedded date. This should make it easier to roll |
| back if necessary. |
| """ |
| return time.strftime('naclsdk_manifest2.%Y_%m_%d_%H_%M_%S.json', |
| time.gmtime()) |
| |
| |
| def GetPlatformArchiveName(platform): |
| """Get the basename of an archive given a platform string. |
| |
| Args: |
| platform: One of ('win', 'mac', 'linux'). |
| |
| Returns: |
| The basename of the sdk archive for that platform. |
| """ |
| return 'naclsdk_%s.tar.bz2' % platform |
| |
| |
| def GetCanonicalArchiveName(url): |
| """Get the canonical name of an archive given its URL. |
| |
| This will convert "naclsdk_linux.bz2" -> "naclsdk_linux.tar.bz2", and also |
| remove everything but the filename of the URL. |
| |
| This is used below to determine if an expected bundle is found in an version |
| directory; the archives all have the same name, but may not exist for a given |
| version. |
| |
| Args: |
| url: The url to parse. |
| |
| Returns: |
| The canonical name as described above. |
| """ |
| name = posixpath.basename(url) |
| match = re.match(r'naclsdk_(.*?)(?:\.tar)?\.bz2', name) |
| if match: |
| return 'naclsdk_%s.tar.bz2' % match.group(1) |
| |
| return name |
| |
| |
| class Delegate(object): |
| """Delegate all external access; reading/writing to filesystem, gsutil etc.""" |
| |
| def GetRepoManifest(self): |
| """Read the manifest file from the NaCl SDK repository. |
| |
| This manifest is used as a template for the auto updater; only pepper |
| bundles with no archives are considered for auto updating. |
| |
| Returns: |
| A manifest_util.SDKManifest object read from the NaCl SDK repo.""" |
| raise NotImplementedError() |
| |
| def GetHistory(self): |
| """Read Chrome release history from omahaproxy.appspot.com |
| |
| Here is an example of data from this URL: |
| cros,stable,18.0.1025.168,2012-05-01 17:04:05.962578\n |
| win,canary,20.0.1123.0,2012-05-01 13:59:31.703020\n |
| mac,canary,20.0.1123.0,2012-05-01 11:54:13.041875\n |
| win,stable,18.0.1025.168,2012-04-30 20:34:56.078490\n |
| mac,stable,18.0.1025.168,2012-04-30 20:34:55.231141\n |
| ... |
| Where each line has comma separated values in the following format: |
| platform, channel, version, date/time\n |
| |
| Returns: |
| A list where each element is a line from the document, represented as a |
| tuple.""" |
| raise NotImplementedError() |
| |
| def GsUtil_ls(self, url): |
| """Runs gsutil ls |url| |
| |
| Args: |
| url: The cloud storage url to list. |
| Returns: |
| A list of URLs, all with the gs:// schema.""" |
| raise NotImplementedError() |
| |
| def GsUtil_cat(self, url): |
| """Runs gsutil cat |url| |
| |
| Args: |
| url: The cloud storage url to read from. |
| Returns: |
| A string with the contents of the file at |url|.""" |
| raise NotImplementedError() |
| |
| def GsUtil_cp(self, src, dest, stdin=None): |
| """Runs gsutil cp |src| |dest| |
| |
| Args: |
| src: The file path or url to copy from. |
| dest: The file path or url to copy to. |
| stdin: If src is '-', this is used as the stdin to give to gsutil. The |
| effect is that text in stdin is copied to |dest|.""" |
| raise NotImplementedError() |
| |
| def SendMail(self, subject, text): |
| """Send an email. |
| |
| Args: |
| subject: The subject of the email. |
| text: The text of the email. |
| """ |
| raise NotImplementedError() |
| |
| |
| class RealDelegate(Delegate): |
| def __init__(self, dryrun=False, gsutil=None, mailfrom=None, mailto=None): |
| super(RealDelegate, self).__init__() |
| self.dryrun = dryrun |
| self.mailfrom = mailfrom |
| self.mailto = mailto |
| if gsutil: |
| self.gsutil = gsutil |
| else: |
| self.gsutil = buildbot_common.GetGsutil() |
| |
| def GetRepoManifest(self): |
| """See Delegate.GetRepoManifest""" |
| with open(REPO_MANIFEST, 'r') as sdk_stream: |
| sdk_json_string = sdk_stream.read() |
| |
| manifest = manifest_util.SDKManifest() |
| manifest.LoadDataFromString(sdk_json_string, add_missing_info=True) |
| return manifest |
| |
| def GetHistory(self): |
| """See Delegate.GetHistory""" |
| url_stream = urllib2.urlopen('https://omahaproxy.appspot.com/history') |
| history = [(platform, channel, version, date) |
| for platform, channel, version, date in csv.reader(url_stream)] |
| |
| # The first line of this URL is the header: |
| # os,channel,version,timestamp |
| return history[1:] |
| |
| def GsUtil_ls(self, url): |
| """See Delegate.GsUtil_ls""" |
| try: |
| stdout = self._RunGsUtil(None, False, 'ls', url) |
| except subprocess.CalledProcessError: |
| return [] |
| |
| # filter out empty lines |
| return filter(None, stdout.split('\n')) |
| |
| def GsUtil_cat(self, url): |
| """See Delegate.GsUtil_cat""" |
| return self._RunGsUtil(None, True, 'cat', url) |
| |
| def GsUtil_cp(self, src, dest, stdin=None): |
| """See Delegate.GsUtil_cp""" |
| if self.dryrun: |
| logger.info("Skipping upload: %s -> %s" % (src, dest)) |
| if src == '-': |
| logger.info(' contents = """%s"""' % stdin) |
| return |
| |
| return self._RunGsUtil(stdin, True, 'cp', '-a', 'public-read', src, dest) |
| |
| def SendMail(self, subject, text): |
| """See Delegate.SendMail""" |
| if self.mailfrom and self.mailto: |
| msg = email.MIMEMultipart.MIMEMultipart() |
| msg['From'] = self.mailfrom |
| msg['To'] = ', '.join(self.mailto) |
| msg['Date'] = email.Utils.formatdate(localtime=True) |
| msg['Subject'] = subject |
| msg.attach(email.MIMEText.MIMEText(text)) |
| smtp_obj = smtplib.SMTP('localhost') |
| smtp_obj.sendmail(self.mailfrom, self.mailto, msg.as_string()) |
| smtp_obj.close() |
| |
| def _RunGsUtil(self, stdin, log_errors, *args): |
| """Run gsutil as a subprocess. |
| |
| Args: |
| stdin: If non-None, used as input to the process. |
| log_errors: If True, write errors to stderr. |
| *args: Arguments to pass to gsutil. The first argument should be an |
| operation such as ls, cp or cat. |
| Returns: |
| The stdout from the process.""" |
| cmd = [self.gsutil] + list(args) |
| logger.debug("Running: %s" % str(cmd)) |
| if stdin: |
| stdin_pipe = subprocess.PIPE |
| else: |
| stdin_pipe = None |
| |
| try: |
| process = subprocess.Popen(cmd, stdin=stdin_pipe, stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE) |
| stdout, stderr = process.communicate(stdin) |
| except OSError as e: |
| raise manifest_util.Error("Unable to run '%s': %s" % (cmd[0], str(e))) |
| |
| if process.returncode: |
| if log_errors: |
| logger.error(stderr) |
| raise subprocess.CalledProcessError(process.returncode, ' '.join(cmd)) |
| return stdout |
| |
| |
| class GsutilLoggingHandler(logging.handlers.BufferingHandler): |
| def __init__(self, delegate): |
| logging.handlers.BufferingHandler.__init__(self, capacity=0) |
| self.delegate = delegate |
| |
| def shouldFlush(self, record): |
| # BufferingHandler.shouldFlush automatically flushes if the length of the |
| # buffer is greater than self.capacity. We don't want that behavior, so |
| # return False here. |
| return False |
| |
| def flush(self): |
| # Do nothing here. We want to be explicit about uploading the log. |
| pass |
| |
| def upload(self): |
| output_list = [] |
| for record in self.buffer: |
| output_list.append(self.format(record)) |
| output = '\n'.join(output_list) |
| self.delegate.GsUtil_cp('-', GS_SDK_MANIFEST_LOG, stdin=output) |
| |
| logging.handlers.BufferingHandler.flush(self) |
| |
| |
| class NoSharedVersionException(Exception): |
| pass |
| |
| |
| class VersionFinder(object): |
| """Finds a version of a pepper bundle that all desired platforms share. |
| |
| Args: |
| delegate: See Delegate class above. |
| platforms: A sequence of platforms to consider, e.g. |
| ('mac', 'linux', 'win') |
| extra_archives: A sequence of tuples: (archive_basename, minimum_version), |
| e.g. [('foo.tar.bz2', '18.0.1000.0'), ('bar.tar.bz2', '19.0.1100.20')] |
| These archives must exist to consider a version for inclusion, as |
| long as that version is greater than the archive's minimum version. |
| """ |
| def __init__(self, delegate, platforms, extra_archives=None): |
| self.delegate = delegate |
| self.history = delegate.GetHistory() |
| self.platforms = platforms |
| self.extra_archives = extra_archives |
| |
| def GetMostRecentSharedVersion(self, major_version): |
| """Returns the most recent version of a pepper bundle that exists on all |
| given platforms. |
| |
| Specifically, the resulting version should be the most recently released |
| (meaning closest to the top of the listing on |
| omahaproxy.appspot.com/history) version that has a Chrome release on all |
| given platforms, and has a pepper bundle archive for each platform as well. |
| |
| Args: |
| major_version: The major version of the pepper bundle, e.g. 19. |
| Returns: |
| A tuple (version, channel, archives). The version is a string such as |
| "19.0.1084.41". The channel is one of ('stable', 'beta', or 'dev'). |
| |archives| is a list of archive URLs.""" |
| def GetPlatformHistory(platform): |
| return self._GetPlatformMajorVersionHistory(major_version, platform) |
| |
| shared_version_generator = self._FindNextSharedVersion(self.platforms, |
| GetPlatformHistory) |
| return self._DoGetMostRecentSharedVersion(shared_version_generator) |
| |
| def GetMostRecentSharedCanary(self): |
| """Returns the most recent version of a canary pepper bundle that exists on |
| all given platforms. |
| |
| Canary is special-cased because we don't care about its major version; we |
| always use the most recent canary, regardless of major version. |
| |
| Returns: |
| A tuple (version, channel, archives). The version is a string such as |
| "trunk.123456". The channel is always 'canary'. |archives| is a list of |
| archive URLs.""" |
| version_generator = self._FindNextTrunkVersion() |
| return self._DoGetMostRecentSharedVersion(version_generator) |
| |
| def GetAvailablePlatformArchivesFor(self, version): |
| """Returns a sequence of archives that exist for a given version, on the |
| given platforms. |
| |
| The second element of the returned tuple is a list of all platforms that do |
| not have an archive for the given version. |
| |
| Args: |
| version: The version to find archives for. (e.g. "18.0.1025.164") |
| Returns: |
| A tuple (archives, missing_archives). |archives| is a list of archive |
| URLs, |missing_archives| is a list of archive names. |
| """ |
| archive_urls = self._GetAvailableArchivesFor(version) |
| |
| expected_archives = set(GetPlatformArchiveName(p) for p in self.platforms) |
| |
| if self.extra_archives: |
| for extra_archive, min_version, max_version in self.extra_archives: |
| if (CompareVersions(version, min_version) >= 0 and |
| CompareVersions(version, max_version) < 0): |
| expected_archives.add(extra_archive) |
| found_archives = set(GetCanonicalArchiveName(a) for a in archive_urls) |
| missing_archives = expected_archives - found_archives |
| |
| # Only return archives that are "expected". |
| def IsExpected(url): |
| return GetCanonicalArchiveName(url) in expected_archives |
| |
| expected_archive_urls = [u for u in archive_urls if IsExpected(u)] |
| return expected_archive_urls, missing_archives |
| |
| def _DoGetMostRecentSharedVersion(self, shared_version_generator): |
| """Returns the most recent version of a pepper bundle that exists on all |
| given platforms. |
| |
| This function does the real work for the public GetMostRecentShared* above. |
| |
| Args: |
| shared_version_generator: A generator that will yield (version, channel) |
| tuples in order of most recent to least recent. |
| Returns: |
| A tuple (version, channel, archives). The version is a string such as |
| "19.0.1084.41". The channel is one of ('stable', 'beta', 'dev', |
| 'canary'). |archives| is a list of archive URLs.""" |
| version = None |
| skipped_versions = [] |
| channel = '' |
| while True: |
| try: |
| version, channel = shared_version_generator.next() |
| except StopIteration: |
| msg = 'No shared version for platforms: %s\n' % ( |
| ', '.join(self.platforms)) |
| msg += 'Last version checked = %s.\n' % (version,) |
| if skipped_versions: |
| msg += 'Versions skipped due to missing archives:\n' |
| for version, channel, missing_archives in skipped_versions: |
| archive_msg = '(missing %s)' % (', '.join(missing_archives)) |
| msg += ' %s (%s) %s\n' % (version, channel, archive_msg) |
| raise NoSharedVersionException(msg) |
| |
| logger.info('Found shared version: %s, channel: %s' % ( |
| version, channel)) |
| |
| archives, missing_archives = self.GetAvailablePlatformArchivesFor(version) |
| |
| if not missing_archives: |
| return version, channel, archives |
| |
| logger.info(' skipping. Missing archives: %s' % ( |
| ', '.join(missing_archives))) |
| |
| skipped_versions.append((version, channel, missing_archives)) |
| |
| def _GetPlatformMajorVersionHistory(self, with_major_version, with_platform): |
| """Yields Chrome history for a given platform and major version. |
| |
| Args: |
| with_major_version: The major version to filter for. If 0, match all |
| versions. |
| with_platform: The name of the platform to filter for. |
| Returns: |
| A generator that yields a tuple (channel, version) for each version that |
| matches the platform and major version. The version returned is a tuple as |
| returned from SplitVersion. |
| """ |
| for platform, channel, version, _ in self.history: |
| version = SplitVersion(version) |
| if (with_platform == platform and |
| (with_major_version == 0 or with_major_version == version[0])): |
| yield channel, version |
| |
| def _FindNextSharedVersion(self, platforms, generator_func): |
| """Yields versions of Chrome that exist on all given platforms, in order of |
| newest to oldest. |
| |
| Versions are compared in reverse order of release. That is, the most |
| recently updated version will be tested first. |
| |
| Args: |
| platforms: A sequence of platforms to consider, e.g. |
| ('mac', 'linux', 'win') |
| generator_func: A function which takes a platform and returns a |
| generator that yields (channel, version) tuples. |
| Returns: |
| A generator that yields a tuple (version, channel) for each version that |
| matches all platforms and the major version. The version returned is a |
| string (e.g. "18.0.1025.164"). |
| """ |
| platform_generators = [] |
| for platform in platforms: |
| platform_generators.append(generator_func(platform)) |
| |
| shared_version = None |
| platform_versions = [] |
| for platform_gen in platform_generators: |
| platform_versions.append(platform_gen.next()) |
| |
| while True: |
| if logger.isEnabledFor(logging.INFO): |
| msg_info = [] |
| for i, platform in enumerate(platforms): |
| msg_info.append('%s: %s' % ( |
| platform, JoinVersion(platform_versions[i][1]))) |
| logger.info('Checking versions: %s' % ', '.join(msg_info)) |
| |
| shared_version = min((v for c, v in platform_versions)) |
| |
| if all(v == shared_version for c, v in platform_versions): |
| # grab the channel from an arbitrary platform |
| first_platform = platform_versions[0] |
| channel = first_platform[0] |
| yield JoinVersion(shared_version), channel |
| |
| # force increment to next version for all platforms |
| shared_version = None |
| |
| # Find the next version for any platform that isn't at the shared version. |
| try: |
| for i, platform_gen in enumerate(platform_generators): |
| if platform_versions[i][1] != shared_version: |
| platform_versions[i] = platform_gen.next() |
| except StopIteration: |
| return |
| |
| |
| def _FindNextTrunkVersion(self): |
| """Yields all trunk versions that exist in the cloud storage bucket, newest |
| to oldest. |
| |
| Returns: |
| A generator that yields a tuple (version, channel) for each version that |
| matches all platforms and the major version. The version returned is a |
| string (e.g. "trunk.123456"). |
| """ |
| files = self.delegate.GsUtil_ls(GS_BUCKET_PATH) |
| assert all(f.startswith('gs://') for f in files) |
| |
| trunk_versions = [] |
| for f in files: |
| match = re.search(r'(trunk\.\d+)', f) |
| if match: |
| trunk_versions.append(match.group(1)) |
| |
| trunk_versions.sort(reverse=True) |
| |
| for version in trunk_versions: |
| yield version, 'canary' |
| |
| |
| def _GetAvailableArchivesFor(self, version_string): |
| """Downloads a list of all available archives for a given version. |
| |
| Args: |
| version_string: The version to find archives for. (e.g. "18.0.1025.164") |
| Returns: |
| A list of strings, each of which is a platform-specific archive URL. (e.g. |
| "gs://nativeclient_mirror/nacl/nacl_sdk/18.0.1025.164/" |
| "naclsdk_linux.tar.bz2"). |
| |
| All returned URLs will use the gs:// schema.""" |
| files = self.delegate.GsUtil_ls(GS_BUCKET_PATH + version_string) |
| |
| assert all(f.startswith('gs://') for f in files) |
| |
| archives = [f for f in files if not f.endswith('.json')] |
| manifests = [f for f in files if f.endswith('.json')] |
| |
| # don't include any archives that don't have an associated manifest. |
| return filter(lambda a: a + '.json' in manifests, archives) |
| |
| |
| class UnknownLockedBundleException(Exception): |
| pass |
| |
| |
| class Updater(object): |
| def __init__(self, delegate): |
| self.delegate = delegate |
| self.versions_to_update = [] |
| self.locked_bundles = [] |
| self.online_manifest = manifest_util.SDKManifest() |
| self._FetchOnlineManifest() |
| |
| def AddVersionToUpdate(self, bundle_name, version, channel, archives): |
| """Add a pepper version to update in the uploaded manifest. |
| |
| Args: |
| bundle_name: The name of the pepper bundle, e.g. 'pepper_18' |
| version: The version of the pepper bundle, e.g. '18.0.1025.64' |
| channel: The stability of the pepper bundle, e.g. 'beta' |
| archives: A sequence of archive URLs for this bundle.""" |
| self.versions_to_update.append((bundle_name, version, channel, archives)) |
| |
| def AddLockedBundle(self, bundle_name): |
| """Add a "locked" bundle to the updater. |
| |
| A locked bundle is a bundle that wasn't found in the history. When this |
| happens, the bundle is now "locked" to whatever was last found. We want to |
| ensure that the online manifest has this bundle. |
| |
| Args: |
| bundle_name: The name of the locked bundle. |
| """ |
| self.locked_bundles.append(bundle_name) |
| |
| def Update(self, manifest): |
| """Update a manifest and upload it. |
| |
| Note that bundles will not be updated if the current version is newer. |
| That is, the updater will never automatically update to an older version of |
| a bundle. |
| |
| Args: |
| manifest: The manifest used as a template for updating. Only pepper |
| bundles that contain no archives will be considered for auto-updating.""" |
| # Make sure there is only one stable branch: the one with the max version. |
| # All others are post-stable. |
| stable_major_versions = [GetMajorVersion(version) for _, version, channel, _ |
| in self.versions_to_update if channel == 'stable'] |
| # Add 0 in case there are no stable versions. |
| max_stable_version = max([0] + stable_major_versions) |
| |
| # Ensure that all locked bundles exist in the online manifest. |
| for bundle_name in self.locked_bundles: |
| online_bundle = self.online_manifest.GetBundle(bundle_name) |
| if online_bundle: |
| manifest.SetBundle(online_bundle) |
| else: |
| msg = ('Attempted to update bundle "%s", but no shared versions were ' |
| 'found, and there is no online bundle with that name.') |
| raise UnknownLockedBundleException(msg % bundle_name) |
| |
| if self.locked_bundles: |
| # Send a nagging email that we shouldn't be wasting time looking for |
| # bundles that are no longer in the history. |
| scriptname = os.path.basename(sys.argv[0]) |
| subject = '[%s] Reminder: remove bundles from %s' % (scriptname, |
| MANIFEST_BASENAME) |
| text = 'These bundles are not in the omahaproxy history anymore: ' + \ |
| ', '.join(self.locked_bundles) |
| self.delegate.SendMail(subject, text) |
| |
| |
| # Update all versions. |
| logger.info('>>> Updating bundles...') |
| for bundle_name, version, channel, archives in self.versions_to_update: |
| logger.info('Updating %s to %s...' % (bundle_name, version)) |
| bundle = manifest.GetBundle(bundle_name) |
| for archive in archives: |
| platform_bundle = self._GetPlatformArchiveBundle(archive) |
| # Normally the manifest snippet's bundle name matches our bundle name. |
| # pepper_canary, however is called "pepper_###" in the manifest |
| # snippet. |
| platform_bundle.name = bundle_name |
| bundle.MergeWithBundle(platform_bundle) |
| |
| # Fix the stability and recommended values |
| major_version = GetMajorVersion(version) |
| if major_version < max_stable_version: |
| bundle.stability = 'post_stable' |
| else: |
| bundle.stability = channel |
| # We always recommend the stable version. |
| if bundle.stability == 'stable': |
| bundle.recommended = 'yes' |
| else: |
| bundle.recommended = 'no' |
| |
| # Check to ensure this bundle is newer than the online bundle. |
| online_bundle = self.online_manifest.GetBundle(bundle_name) |
| if online_bundle: |
| # This test used to be online_bundle.revision >= bundle.revision. |
| # That doesn't do quite what we want: sometimes the metadata changes |
| # but the revision stays the same -- we still want to push those |
| # changes. |
| if online_bundle.revision > bundle.revision or online_bundle == bundle: |
| logger.info( |
| ' Revision %s is not newer than than online revision %s. ' |
| 'Skipping.' % (bundle.revision, online_bundle.revision)) |
| |
| manifest.SetBundle(online_bundle) |
| continue |
| self._UploadManifest(manifest) |
| logger.info('Done.') |
| |
| def _GetPlatformArchiveBundle(self, archive): |
| """Downloads the manifest "snippet" for an archive, and reads it as a |
| Bundle. |
| |
| Args: |
| archive: A full URL of a platform-specific archive, using the gs schema. |
| Returns: |
| An object of type manifest_util.Bundle, read from a JSON file storing |
| metadata for this archive. |
| """ |
| stdout = self.delegate.GsUtil_cat(archive + '.json') |
| bundle = manifest_util.Bundle('') |
| bundle.LoadDataFromString(stdout) |
| # Some snippets were uploaded with revisions and versions as strings. Fix |
| # those here. |
| bundle.revision = int(bundle.revision) |
| bundle.version = int(bundle.version) |
| |
| # HACK. The naclports archive specifies host_os as linux. Change it to all. |
| for archive in bundle.GetArchives(): |
| if NACLPORTS_ARCHIVE_NAME in archive.url: |
| archive.host_os = 'all' |
| return bundle |
| |
| def _UploadManifest(self, manifest): |
| """Upload a serialized manifest_util.SDKManifest object. |
| |
| Upload one copy to gs://<BUCKET_PATH>/naclsdk_manifest2.json, and a copy to |
| gs://<BUCKET_PATH>/manifest_backups/naclsdk_manifest2.<TIMESTAMP>.json. |
| |
| Args: |
| manifest: The new manifest to upload. |
| """ |
| new_manifest_string = manifest.GetDataAsString() |
| online_manifest_string = self.online_manifest.GetDataAsString() |
| |
| if self.delegate.dryrun: |
| logger.info(''.join(list(difflib.unified_diff( |
| online_manifest_string.splitlines(1), |
| new_manifest_string.splitlines(1))))) |
| return |
| else: |
| online_manifest = manifest_util.SDKManifest() |
| online_manifest.LoadDataFromString(online_manifest_string) |
| |
| if online_manifest == manifest: |
| logger.info('New manifest doesn\'t differ from online manifest.' |
| 'Skipping upload.') |
| return |
| |
| timestamp_manifest_path = GS_MANIFEST_BACKUP_DIR + \ |
| GetTimestampManifestName() |
| self.delegate.GsUtil_cp('-', timestamp_manifest_path, |
| stdin=manifest.GetDataAsString()) |
| |
| # copy from timestampped copy over the official manifest. |
| self.delegate.GsUtil_cp(timestamp_manifest_path, GS_SDK_MANIFEST) |
| |
| def _FetchOnlineManifest(self): |
| try: |
| online_manifest_string = self.delegate.GsUtil_cat(GS_SDK_MANIFEST) |
| except subprocess.CalledProcessError: |
| # It is not a failure if the online manifest doesn't exist. |
| online_manifest_string = '' |
| |
| if online_manifest_string: |
| self.online_manifest.LoadDataFromString(online_manifest_string) |
| |
| |
| def Run(delegate, platforms, extra_archives, fixed_bundle_versions=None): |
| """Entry point for the auto-updater. |
| |
| Args: |
| delegate: The Delegate object to use for reading Urls, files, etc. |
| platforms: A sequence of platforms to consider, e.g. |
| ('mac', 'linux', 'win') |
| extra_archives: A sequence of tuples: (archive_basename, minimum_version, |
| max_version), e.g. [('foo.tar.bz2', '18.0.1000.0', '19.0.0.0)] |
| These archives must exist to consider a version for inclusion, as |
| long as that version is greater than the archive's minimum version. |
| fixed_bundle_versions: A sequence of tuples (bundle_name, version_string). |
| e.g. ('pepper_21', '21.0.1145.0') |
| """ |
| if fixed_bundle_versions: |
| fixed_bundle_versions = dict(fixed_bundle_versions) |
| else: |
| fixed_bundle_versions = {} |
| |
| manifest = delegate.GetRepoManifest() |
| auto_update_bundles = [] |
| for bundle in manifest.GetBundles(): |
| if not bundle.name.startswith('pepper_'): |
| continue |
| archives = bundle.GetArchives() |
| if not archives: |
| auto_update_bundles.append(bundle) |
| |
| if not auto_update_bundles: |
| logger.info('No versions need auto-updating.') |
| return |
| |
| updater = Updater(delegate) |
| |
| for bundle in auto_update_bundles: |
| try: |
| if bundle.name == CANARY_BUNDLE_NAME: |
| logger.info('>>> Looking for most recent pepper_canary...') |
| version_finder = VersionFinder(delegate, platforms, extra_archives) |
| version, channel, archives = version_finder.GetMostRecentSharedCanary() |
| else: |
| logger.info('>>> Looking for most recent pepper_%s...' % |
| bundle.version) |
| version_finder = VersionFinder(delegate, platforms, extra_archives) |
| version, channel, archives = version_finder.GetMostRecentSharedVersion( |
| bundle.version) |
| except NoSharedVersionException: |
| # If we can't find a shared version, make sure that there is an uploaded |
| # bundle with that name already. |
| updater.AddLockedBundle(bundle.name) |
| continue |
| |
| if bundle.name in fixed_bundle_versions: |
| # Ensure this version is valid for all platforms. |
| # If it is, use the channel found above (because the channel for this |
| # version may not be in the history.) |
| version = fixed_bundle_versions[bundle.name] |
| logger.info('Fixed bundle version: %s, %s' % (bundle.name, version)) |
| archives, missing = \ |
| version_finder.GetAvailablePlatformArchivesFor(version) |
| if missing: |
| logger.warn( |
| 'Some archives for version %s of bundle %s don\'t exist: ' |
| 'Missing %s' % (version, bundle.name, ', '.join(missing))) |
| return |
| |
| updater.AddVersionToUpdate(bundle.name, version, channel, archives) |
| |
| updater.Update(manifest) |
| |
| |
| class CapturedFile(object): |
| """A file-like object that captures text written to it, but also passes it |
| through to an underlying file-like object.""" |
| def __init__(self, passthrough): |
| self.passthrough = passthrough |
| self.written = cStringIO.StringIO() |
| |
| def write(self, s): |
| self.written.write(s) |
| if self.passthrough: |
| self.passthrough.write(s) |
| |
| def getvalue(self): |
| return self.written.getvalue() |
| |
| |
| def main(args): |
| parser = argparse.ArgumentParser() |
| parser.add_argument('--gsutil', help='path to gsutil.') |
| parser.add_argument('-d', '--debug', help='run in debug mode.', |
| action='store_true') |
| parser.add_argument('--mailfrom', help='email address of sender.') |
| parser.add_argument('--mailto', help='send error mails to...', |
| action='append') |
| parser.add_argument('-n', '--dryrun', help="don't upload the manifest.", |
| action='store_true') |
| parser.add_argument('-v', '--verbose', help='print more diagnotic messages. ' |
| 'Use more than once for more info.', |
| action='count') |
| parser.add_argument('--log-file', metavar='FILE', help='log to FILE') |
| parser.add_argument('--upload-log', help='Upload log alongside the manifest.', |
| action='store_true') |
| parser.add_argument('--bundle-version', |
| help='Manually set a bundle version. This can be passed more than once. ' |
| 'format: --bundle-version pepper_24=24.0.1312.25', action='append') |
| options = parser.parse_args(args) |
| |
| if (options.mailfrom is None) != (not options.mailto): |
| options.mailfrom = None |
| options.mailto = None |
| logger.warning('Disabling email, one of --mailto or --mailfrom ' |
| 'was missing.\n') |
| |
| if options.verbose >= 2: |
| logging.basicConfig(level=logging.DEBUG, filename=options.log_file) |
| elif options.verbose: |
| logging.basicConfig(level=logging.INFO, filename=options.log_file) |
| else: |
| logging.basicConfig(level=logging.WARNING, filename=options.log_file) |
| |
| # Parse bundle versions. |
| fixed_bundle_versions = {} |
| if options.bundle_version: |
| for bundle_version_string in options.bundle_version: |
| bundle_name, version = bundle_version_string.split('=') |
| fixed_bundle_versions[bundle_name] = version |
| |
| if options.mailfrom and options.mailto: |
| # Capture stderr so it can be emailed, if necessary. |
| sys.stderr = CapturedFile(sys.stderr) |
| |
| try: |
| try: |
| delegate = RealDelegate(options.dryrun, options.gsutil, |
| options.mailfrom, options.mailto) |
| |
| if options.upload_log: |
| gsutil_logging_handler = GsutilLoggingHandler(delegate) |
| logger.addHandler(gsutil_logging_handler) |
| |
| # Only look for naclports archives >= 27. The old ports bundles don't |
| # include license information. The naclports bundle was removed in |
| # pepper_47. |
| extra_archives = [(NACLPORTS_ARCHIVE_NAME, '27.0.0.0', '47.0.0.0')] |
| Run(delegate, ('mac', 'win', 'linux'), extra_archives, |
| fixed_bundle_versions) |
| except Exception: |
| if options.mailfrom and options.mailto: |
| traceback.print_exc() |
| scriptname = os.path.basename(sys.argv[0]) |
| subject = '[%s] Failed to update manifest' % (scriptname,) |
| text = '%s failed.\n\nSTDERR:\n%s\n' % (scriptname, |
| sys.stderr.getvalue()) |
| delegate.SendMail(subject, text) |
| return 1 |
| else: |
| raise |
| finally: |
| if options.upload_log: |
| gsutil_logging_handler.upload() |
| except manifest_util.Error as e: |
| if options.debug: |
| raise |
| sys.stderr.write(str(e) + '\n') |
| return 1 |
| |
| return 0 |
| |
| if __name__ == '__main__': |
| sys.exit(main(sys.argv[1:])) |