Bug 1162191 - Add |mach artifact| for installing downloaded Fennec binaries. r=gps
authorNick Alexander <nalexander@mozilla.com>
Wed, 24 Jun 2015 23:12:00 -0700
changeset 280968 516232663a0b9a80cdb311200487c8f55f7c3db2
parent 280906 6c6917570a3ddb96d899702732e23c68307f97c4
child 280969 3ce3e827434e3cc68e0e69dc04b205943b404be6
push id4932
push userjlund@mozilla.com
push dateMon, 10 Aug 2015 18:23:06 +0000
treeherdermozilla-beta@6dd5a4f5f745 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps
bugs1162191, 1124378
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1162191 - Add |mach artifact| for installing downloaded Fennec binaries. r=gps DONTBUILD ON A CLOSED TREE: Android-only and the build changes are cosmetic. Very much a first cut, but I'd like to get some Fennec early adopters testing. This adds: * |mach artifact install| to fetch and install Fennec binaries; * |mach artifact last| to print details about what was last installed; * |mach artifact {print,clear}-caches|, for debugging. This code is exposed as a new mozbuild.artifacts Python but it's not particularly general. My intention was to get things out of the mach command more than produce a general artifact fetching API. We can leave that bike shed to Bug 1124378. I've been testing this with --disable-compile-environment and it works well locally, although there's no reason a knowledgeable developer couldn't use this in conjunction with a fully-built tree. (I don't know when such a situation would arise, but I know of no technical impediment.)
mobile/android/mach_commands.py
python/mozbuild/mozbuild/artifacts.py
python/mozbuild/mozbuild/base.py
python/mozbuild/mozbuild/util.py
toolkit/mozapps/installer/upload-files.mk
--- a/mobile/android/mach_commands.py
+++ b/mobile/android/mach_commands.py
@@ -18,28 +18,30 @@ from mozbuild.base import (
 from mozbuild.util import (
     FileAvoidWrite,
 )
 
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
     Command,
+    SubCommand,
 )
 
 SUCCESS = '''
 You should be ready to build with Gradle and import into IntelliJ!  Test with
 
     ./mach gradle build
 
 and in IntelliJ select File > Import project... and choose
 
     {topobjdir}/mobile/android/gradle
 '''
 
+
 @CommandProvider
 class MachCommands(MachCommandBase):
     @Command('gradle', category='devenv',
         description='Run gradle.',
         conditions=[conditions.is_android])
     @CommandArgument('args', nargs=argparse.REMAINDER)
     def gradle(self, args):
         # Avoid logging the command
@@ -49,17 +51,16 @@ class MachCommands(MachCommandBase):
         if code:
             return code
 
         return self.run_process(['./gradlew'] + args,
             pass_thru=True, # Allow user to run gradle interactively.
             ensure_exit_code=False, # Don't throw on non-zero exit code.
             cwd=mozpath.join(self.topobjdir, 'mobile', 'android', 'gradle'))
 
-
     @Command('gradle-install', category='devenv',
         description='Install gradle environment.',
         conditions=[conditions.is_android])
     def gradle_install(self, quiet=False):
         import mozpack.manifests
         m = mozpack.manifests.InstallManifest()
 
         def srcdir(dst, src):
@@ -157,8 +158,109 @@ class MachCommands(MachCommandBase):
             ensure_exit_code=False, # Don't throw on non-zero exit code.
             cwd=mozpath.join(self.topsrcdir, 'mobile', 'android'))
 
         if not quiet:
             if not code:
                 print(SUCCESS.format(topobjdir=self.topobjdir))
 
         return code
+
+
+@CommandProvider
+class PackageFrontend(MachCommandBase):
+    """Fetch and install binary artifacts from Mozilla automation."""
+
+    @Command('artifact', category='post-build',
+        description='Use pre-built artifacts to build Fennec.',
+        conditions=[
+            conditions.is_android,  # mobile/android only for now.
+            conditions.is_hg,  # mercurial only for now.
+        ])
+    def artifact(self):
+        '''Download, cache, and install pre-built binary artifacts to build Fennec.
+
+        Invoke |mach artifact| before each |mach package| to freshen your installed
+        binary libraries.  That is, package using
+
+        mach artifact install && mach package
+
+        to download, cache, and install binary artifacts from Mozilla automation,
+        replacing whatever may be in your object directory.  Use |mach artifact last|
+        to see what binary artifacts were last used.
+
+        Never build libxul again!
+        '''
+        pass
+
+    def _make_artifacts(self, tree=None, job=None):
+        self.log_manager.terminal_handler.setLevel(logging.INFO)
+
+        self._activate_virtualenv()
+        self.virtualenv_manager.install_pip_package('pylru==1.0.9')
+        self.virtualenv_manager.install_pip_package('taskcluster==0.0.16')
+
+        state_dir = self._mach_context.state_dir
+        cache_dir = os.path.join(state_dir, 'package-frontend')
+
+        import which
+        hg = which.which('hg')
+
+        # Absolutely must come after the virtualenv is populated!
+        from mozbuild.artifacts import Artifacts
+        artifacts = Artifacts(tree, job, log=self.log, cache_dir=cache_dir, hg=hg)
+        return artifacts
+
+    @SubCommand('artifact', 'install',
+        'Install a good pre-built artifact.')
+    @CommandArgument('--tree', metavar='TREE', type=str,
+        help='Firefox tree.',
+        default='fx-team')  # TODO: switch to central as this stabilizes.
+    @CommandArgument('--job', metavar='JOB', choices=['android-api-11'],
+        help='Build job.',
+        default='android-api-11')  # TODO: fish job from build configuration.
+    @CommandArgument('source', metavar='SRC', nargs='?', type=str,
+        help='Where to fetch and install artifacts from.  Can be omitted, in '
+            'which case the current hg repository is inspected; an hg revision; '
+            'a remote URL; or a local file.',
+        default=None)
+    def artifact_install(self, source=None, tree=None, job=None):
+        artifacts = self._make_artifacts(tree=tree, job=job)
+        return artifacts.install_from(source, self.distdir)
+
+    @SubCommand('artifact', 'last',
+        'Print the last pre-built artifact installed.')
+    @CommandArgument('--tree', metavar='TREE', type=str,
+        help='Firefox tree.',
+        default='fx-team')
+    @CommandArgument('--job', metavar='JOB', type=str,
+        help='Build job.',
+        default='android-api-11')
+    def artifact_print_last(self, tree=None, job=None):
+        artifacts = self._make_artifacts(tree=tree, job=job)
+        artifacts.print_last()
+        return 0
+
+    @SubCommand('artifact', 'print-cache',
+        'Print local artifact cache for debugging.')
+    @CommandArgument('--tree', metavar='TREE', type=str,
+        help='Firefox tree.',
+        default='fx-team')
+    @CommandArgument('--job', metavar='JOB', type=str,
+        help='Build job.',
+        default='android-api-11')
+    def artifact_print_cache(self, tree=None, job=None):
+        artifacts = self._make_artifacts(tree=tree, job=job)
+        artifacts.print_cache()
+        return 0
+
+    @SubCommand('artifact', 'clear-cache',
+        'Delete local artifacts and reset local artifact cache.')
+    @CommandArgument('--tree', metavar='TREE', type=str,
+        help='Firefox tree.',
+        default='fx-team')
+    @CommandArgument('--job', metavar='JOB', type=str,
+        help='Build job.',
+        default='android-api-11')
+    def artifact_clear_cache(self, tree=None, job=None):
+        artifacts = self._make_artifacts(tree=tree, job=job)
+        artifacts.clear_cache()
+        return 0
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/artifacts.py
@@ -0,0 +1,398 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+'''
+Fetch build artifacts from a Firefox tree.
+
+This provides an (at-the-moment special purpose) interface to download Android
+artifacts from Mozilla's Task Cluster.
+
+This module performs the following steps:
+
+* find a candidate hg parent revision using the local pushlog.  The local
+  pushlog is maintained by mozext locally and updated on every pull.
+
+* map the candidate parent to candidate Task Cluster tasks and artifact
+  locations.  Pushlog entries might not correspond to tasks (yet), and those
+  tasks might not produce the desired class of artifacts.
+
+* fetch fresh Task Cluster artifacts and purge old artifacts, using a simple
+  Least Recently Used cache.
+
+The bulk of the complexity is in managing and persisting several caches.  If
+we found a Python LRU cache that pickled cleanly, we could remove a lot of
+this code!  Sadly, I found no such candidate implementations, so we pickle
+pylru caches manually.
+
+None of the instances (or the underlying caches) are safe for concurrent use.
+A future need, perhaps.
+
+This module requires certain modules be importable from the ambient Python
+environment.  |mach artifact| ensures these modules are available, but other
+consumers will need to arrange this themselves.
+'''
+
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+import functools
+import logging
+import operator
+import os
+import pickle
+import re
+import shutil
+import subprocess
+import urlparse
+import zipfile
+
+import pylru
+import taskcluster
+
+from mozbuild.util import (
+    ensureParentDir,
+    FileAvoidWrite,
+)
+
+MAX_CACHED_PARENTS = 100  # Number of parent changesets to cache candidate pushheads for.
+NUM_PUSHHEADS_TO_QUERY_PER_PARENT = 50  # Number of candidate pushheads to cache per parent changeset.
+
+MAX_CACHED_TASKS = 400  # Number of pushheads to cache Task Cluster task data for.
+
+# Number of downloaded artifacts to cache.  Each artifact can be very large,
+# so don't make this to large!  TODO: make this a size (like 500 megs) rather than an artifact count.
+MAX_CACHED_ARTIFACTS = 6
+
+# TODO: handle multiple artifacts with the same filename.
+# TODO: handle installing binaries from different types of artifacts (.tar.bz2, .dmg, etc).
+# Keep the keys of this map in sync with the |mach artifact| --job options.
+JOB_DETAILS = {
+    # 'android-api-9': {'re': re.compile('public/build/fennec-(.*)\.android-arm\.apk')},
+    'android-api-11': {'re': re.compile('public/build/geckolibs-(.*)\.aar')},
+    # 'linux': {'re': re.compile('public/build/firefox-(.*)\.linux-i686\.tar\.bz2')},
+    # 'linux64': {'re': re.compile('public/build/firefox-(.*)\.linux-x86_64\.tar\.bz2')},
+    # 'macosx64': {'re': re.compile('public/build/firefox-(.*)\.mac\.dmg')},
+}
+
+
+def cachedmethod(cachefunc):
+    '''Decorator to wrap a class or instance method with a memoizing callable that
+    saves results in a (possibly shared) cache.
+    '''
+    def decorator(method):
+        def wrapper(self, *args, **kwargs):
+            mapping = cachefunc(self)
+            if mapping is None:
+                return method(self, *args, **kwargs)
+            key = (method.__name__, args, tuple(sorted(kwargs.items())))
+            try:
+                value = mapping[key]
+                return value
+            except KeyError:
+                pass
+            result = method(self, *args, **kwargs)
+            mapping[key] = result
+            return result
+        return functools.update_wrapper(wrapper, method)
+    return decorator
+
+
+class CacheManager(object):
+    '''Maintain an LRU cache.  Provide simple persistence, including support for
+    loading and saving the state using a "with" block.  Allow clearing the cache
+    and printing the cache for debugging.
+
+    Provide simple logging.
+    '''
+
+    def __init__(self, cache_dir, cache_name, cache_size, cache_callback=None, log=None):
+        self._cache = pylru.lrucache(cache_size, callback=cache_callback)
+        self._cache_filename = os.path.join(cache_dir, cache_name + '-cache.pickle')
+        self._log = log
+
+    def log(self, *args, **kwargs):
+        if self._log:
+            self._log(*args, **kwargs)
+
+    def load_cache(self):
+        try:
+            items = pickle.load(open(self._cache_filename, 'rb'))
+            for key, value in items:
+                self._cache[key] = value
+        except Exception as e:
+            # Corrupt cache, perhaps?  Sadly, pickle raises many different
+            # exceptions, so it's not worth trying to be fine grained here.
+            # We ignore any exception, so the cache is effectively dropped.
+            self.log(logging.INFO, 'artifact',
+                {'filename': self._cache_filename, 'exception': repr(e)},
+                'Ignoring exception unpickling cache file {filename}: {exception}')
+            pass
+
+    def dump_cache(self):
+        ensureParentDir(self._cache_filename)
+        pickle.dump(list(reversed(list(self._cache.items()))), open(self._cache_filename, 'wb'), -1)
+
+    def clear_cache(self):
+        with self:
+            self._cache.clear()
+
+    def print_cache(self):
+        with self:
+            for item in self._cache.items():
+                self.log(logging.INFO, 'artifact',
+                    {'item': item},
+                    '{item}')
+
+    def print_last_item(self, args, sorted_kwargs, result):
+        # By default, show nothing.
+        pass
+
+    def print_last(self):
+        # We use the persisted LRU caches to our advantage.  The first item is
+        # most recent.
+        with self:
+            item = next(self._cache.items(), None)
+            if item is not None:
+                (name, args, sorted_kwargs), result = item
+                self.print_last_item(args, sorted_kwargs, result)
+            else:
+                self.log(logging.WARN, 'artifact',
+                    {},
+                    'No last cached item found.')
+
+    def __enter__(self):
+        self.load_cache()
+        return self
+
+    def __exit__(self, type, value, traceback):
+        self.dump_cache()
+
+
+class PushHeadCache(CacheManager):
+    '''Map parent hg revisions to candidate pushheads.'''
+
+    def __init__(self, hg, cache_dir, log=None):
+        # It's not unusual to pull hundreds of changesets at once, and perhaps
+        # |hg up| back and forth a few times.
+        CacheManager.__init__(self, cache_dir, 'pushheads', MAX_CACHED_PARENTS, log=log)
+        self._hg = hg
+
+    @cachedmethod(operator.attrgetter('_cache'))
+    def pushheads(self, tree, parent):
+        pushheads = subprocess.check_output([self._hg, 'log',
+            '--template', '{node}\n',
+            '-r', 'last(pushhead("{tree}") & ::"{parent}", {num})'.format(
+                tree=tree, parent=parent, num=NUM_PUSHHEADS_TO_QUERY_PER_PARENT)])
+        pushheads = pushheads.strip().split('\n')
+        return pushheads
+
+
+class TaskCache(CacheManager):
+    '''Map candidate pushheads to Task Cluster task IDs and artifact URLs.'''
+
+    def __init__(self, cache_dir, log=None):
+        CacheManager.__init__(self, cache_dir, 'artifact_url', MAX_CACHED_TASKS, log=log)
+        self._index = taskcluster.Index()
+        self._queue = taskcluster.Queue()
+
+    @cachedmethod(operator.attrgetter('_cache'))
+    def artifact_url(self, tree, job, rev):
+        try:
+            artifact_re = JOB_DETAILS[job]['re']
+        except KeyError:
+            self.log(logging.INFO, 'artifact',
+                {'job': job},
+                'Unknown job {job}')
+            raise KeyError("Unknown job")
+
+        # Bug 1175655: it appears that the Task Cluster index only takes
+        # 12-char hex hashes.
+        key = '{rev}.{tree}.{job}'.format(rev=rev[:12], tree=tree, job=job)
+        try:
+            namespace = 'buildbot.revisions.{key}'.format(key=key)
+            task = self._index.findTask(namespace)
+        except Exception:
+            # Not all revisions correspond to pushes that produce the job we
+            # care about; and even those that do may not have completed yet.
+            raise ValueError('Task for {key} does not exist (yet)!'.format(key=key))
+        taskId = task['taskId']
+
+        # TODO: Make this not Android-only by matching a regular expression.
+        artifacts = self._queue.listLatestArtifacts(taskId)['artifacts']
+
+        def names():
+            for artifact in artifacts:
+                name = artifact['name']
+                if artifact_re.match(name):
+                    yield name
+
+        # TODO: Handle multiple artifacts, taking the latest one.
+        for name in names():
+            # We can easily extract the task ID and the build ID from the URL.
+            url = self._queue.buildUrl('getLatestArtifact', taskId, name)
+            return url
+        raise ValueError('Task for {key} existed, but no artifacts found!'.format(key=key))
+
+    def print_last_item(self, args, sorted_kwargs, result):
+        tree, job, rev = args
+        self.log(logging.INFO, 'artifact',
+            {'rev': rev},
+            'Last installed binaries from hg parent revision {rev}')
+
+
+class ArtifactCache(CacheManager):
+    '''Fetch Task Cluster artifact URLs and purge least recently used artifacts from disk.'''
+
+    def __init__(self, cache_dir, log=None):
+        # TODO: instead of storing N artifact packages, store M megabytes.
+        CacheManager.__init__(self, cache_dir, 'fetch', MAX_CACHED_ARTIFACTS, cache_callback=self.delete_file, log=log)
+        self._cache_dir = cache_dir
+
+    def delete_file(self, key, value):
+        try:
+            os.remove(value)
+            self.log(logging.INFO, 'artifact',
+                {'filename': value},
+                'Purged artifact {filename}')
+        except IOError:
+            pass
+
+    @cachedmethod(operator.attrgetter('_cache'))
+    def fetch(self, url, force=False):
+        args = ['wget', url]
+
+        if not force:
+            args[1:1] = ['--timestamping']
+
+        proc = subprocess.Popen(args, cwd=self._cache_dir)
+        status = None
+        # Leave it to the subprocess to handle Ctrl+C. If it terminates as
+        # a result of Ctrl+C, proc.wait() will return a status code, and,
+        # we get out of the loop. If it doesn't, like e.g. gdb, we continue
+        # waiting.
+        while status is None:
+            try:
+                status = proc.wait()
+            except KeyboardInterrupt:
+                pass
+
+        if status != 0:
+            raise Exception('Process executed with non-0 exit code: %s' % args)
+
+        return os.path.abspath(os.path.join(self._cache_dir, os.path.basename(url)))
+
+    def print_last_item(self, args, sorted_kwargs, result):
+        url, = args
+        self.log(logging.INFO, 'artifact',
+            {'url': url},
+            'Last installed binaries from url {url}')
+        self.log(logging.INFO, 'artifact',
+            {'filename': result},
+            'Last installed binaries from local file {filename}')
+
+
+class Artifacts(object):
+    '''Maintain state to efficiently fetch build artifacts from a Firefox tree.'''
+
+    def __init__(self, tree, job, log=None, cache_dir='.', hg='hg'):
+        self._tree = tree
+        self._job = job
+        self._log = log
+        self._hg = hg
+        self._cache_dir = cache_dir
+
+        self._pushhead_cache = PushHeadCache(self._hg, self._cache_dir, log=self._log)
+        self._task_cache = TaskCache(self._cache_dir, log=self._log)
+        self._artifact_cache = ArtifactCache(self._cache_dir, log=self._log)
+
+    def log(self, *args, **kwargs):
+        if self._log:
+            self._log(*args, **kwargs)
+
+    def install_from_file(self, filename, distdir):
+        self.log(logging.INFO, 'artifact',
+            {'filename': filename},
+            'Installing from {filename}')
+
+        # Copy all .so files to dist/bin, avoiding modification where possible.
+        ensureParentDir(os.path.join(distdir, 'bin', '.dummy'))
+
+        with zipfile.ZipFile(filename) as zf:
+            for info in zf.infolist():
+                if not info.filename.endswith('.so'):
+                    continue
+                n = os.path.join(distdir, 'bin', os.path.basename(info.filename))
+                fh = FileAvoidWrite(n, mode='r')
+                shutil.copyfileobj(zf.open(info), fh)
+                fh.write(zf.open(info).read())
+                file_existed, file_updated = fh.close()
+                self.log(logging.INFO, 'artifact',
+                    {'updating': 'Updating' if file_updated else 'Not updating', 'filename': n},
+                    '{updating} {filename}')
+        return 0
+
+    def install_from_url(self, url, distdir):
+        self.log(logging.INFO, 'artifact',
+            {'url': url},
+            'Installing from {url}')
+        with self._artifact_cache as artifact_cache:  # The with block handles persistence.
+            filename = artifact_cache.fetch(url)
+        return self.install_from_file(filename, distdir)
+
+    def install_from_hg(self, revset, distdir):
+        if not revset:
+            revset = '.'
+        if len(revset) != 40:
+            revset = subprocess.check_output([self._hg, 'log', '--template', '{node}\n', '-r', revset]).strip()
+            if len(revset.split('\n')) != 1:
+                raise ValueError('hg revision specification must resolve to exactly one commit')
+
+        self.log(logging.INFO, 'artifact',
+            {'revset': revset},
+            'Installing from {revset}')
+
+        url = None
+        with self._task_cache as task_cache, self._pushhead_cache as pushhead_cache:
+            # with blocks handle handle persistence.
+            for pushhead in pushhead_cache.pushheads(self._tree, revset):
+                try:
+                    url = task_cache.artifact_url(self._tree, self._job, pushhead)
+                    break
+                except ValueError:
+                    pass
+        if url:
+            return self.install_from_url(url, distdir)
+        return 1
+
+    def install_from(self, source, distdir):
+        if source and os.path.isfile(source):
+            return self.install_from_file(source, distdir)
+        elif source and urlparse.urlparse(source).scheme:
+            return self.install_from_url(source, distdir)
+        else:
+            return self.install_from_hg(source, distdir)
+
+    def print_last(self):
+        self.log(logging.INFO, 'artifact',
+            {},
+            'Printing last used artifact details.')
+        self._pushhead_cache.print_last()
+        self._task_cache.print_last()
+        self._artifact_cache.print_last()
+
+    def clear_cache(self):
+        self.log(logging.INFO, 'artifact',
+            {},
+            'Deleting cached artifacts and caches.')
+        self._pushhead_cache.clear_cache()
+        self._task_cache.clear_cache()
+        self._artifact_cache.clear_cache()
+
+    def print_cache(self):
+        self.log(logging.INFO, 'artifact',
+            {},
+            'Printing cached artifacts and caches.')
+        self._pushhead_cache.print_cache()
+        self._task_cache.print_cache()
+        self._artifact_cache.print_cache()
--- a/python/mozbuild/mozbuild/base.py
+++ b/python/mozbuild/mozbuild/base.py
@@ -759,16 +759,32 @@ class MachCommandConditions(object):
 
     @staticmethod
     def is_android(cls):
         """Must have an Android build."""
         if hasattr(cls, 'substs'):
             return cls.substs.get('MOZ_WIDGET_TOOLKIT') == 'android'
         return False
 
+    @staticmethod
+    def is_hg(cls):
+        """Must have a mercurial source checkout."""
+        if hasattr(cls, 'substs'):
+            top_srcdir = cls.substs.get('top_srcdir')
+            return top_srcdir and os.path.isdir(os.path.join(top_srcdir, '.hg'))
+        return False
+
+    @staticmethod
+    def is_git(cls):
+        """Must have a git source checkout."""
+        if hasattr(cls, 'substs'):
+            top_srcdir = cls.substs.get('top_srcdir')
+            return top_srcdir and os.path.isdir(os.path.join(top_srcdir, '.git'))
+        return False
+
 
 class PathArgument(object):
     """Parse a filesystem path argument and transform it in various ways."""
 
     def __init__(self, arg, topsrcdir, topobjdir, cwd=None):
         self.arg = arg
         self.topsrcdir = topsrcdir
         self.topobjdir = topobjdir
--- a/python/mozbuild/mozbuild/util.py
+++ b/python/mozbuild/mozbuild/util.py
@@ -114,21 +114,22 @@ class FileAvoidWrite(StringIO):
     it. When we close the file object, if the content in the in-memory buffer
     differs from what is on disk, then we write out the new content. Otherwise,
     the original file is untouched.
 
     Instances can optionally capture diffs of file changes. This feature is not
     enabled by default because it a) doesn't make sense for binary files b)
     could add unwanted overhead to calls.
     """
-    def __init__(self, filename, capture_diff=False):
+    def __init__(self, filename, capture_diff=False, mode='rU'):
         StringIO.__init__(self)
         self.name = filename
         self._capture_diff = capture_diff
         self.diff = None
+        self.mode = mode
 
     def close(self):
         """Stop accepting writes, compare file contents, and rewrite if needed.
 
         Returns a tuple of bools indicating what action was performed:
 
             (file existed, file updated)
 
@@ -137,17 +138,17 @@ class FileAvoidWrite(StringIO):
         of the result.
         """
         buf = self.getvalue()
         StringIO.close(self)
         existed = False
         old_content = None
 
         try:
-            existing = open(self.name, 'rU')
+            existing = open(self.name, self.mode)
             existed = True
         except IOError:
             pass
         else:
             try:
                 old_content = existing.read()
                 if old_content == buf:
                     return True, False
--- a/toolkit/mozapps/installer/upload-files.mk
+++ b/toolkit/mozapps/installer/upload-files.mk
@@ -408,16 +408,21 @@ DIST_FILES := $(filter-out $(SO_LIBRARIE
 NON_DIST_FILES += libmozglue.so $(MOZ_CHILD_PROCESS_NAME) $(ASSET_SO_LIBRARIES)
 
 ifdef MOZ_ENABLE_SZIP
 # These libraries are szipped in-place in the
 # assets/$(ANDROID_CPU_ARCH) directory.
 SZIP_LIBRARIES := $(ASSET_SO_LIBRARIES)
 endif
 
+ifndef COMPILE_ENVIRONMENT
+# Any Fennec binary libraries we download are already szipped.
+ALREADY_SZIPPED=1
+endif
+
 # Fennec's OMNIJAR_NAME can include a directory; for example, it might
 # be "assets/omni.ja". This path specifies where the omni.ja file
 # lives in the APK, but should not root the resources it contains
 # under assets/ (i.e., resources should not live at chrome://assets/).
 # packager.py writes /omni.ja in order to be consistent with the
 # layout expected by language repacks. Therefore, we move it to the
 # correct path here, in INNER_MAKE_PACKAGE. See comment about
 # OMNIJAR_NAME in configure.in.
@@ -454,17 +459,17 @@ INNER_MAKE_PACKAGE	= \
   make -C $(GECKO_APP_AP_PATH) gecko-nodeps.ap_ && \
   cp $(GECKO_APP_AP_PATH)/gecko-nodeps.ap_ $(_ABS_DIST)/gecko.ap_ && \
   ( (test ! -f $(GECKO_APP_AP_PATH)/R.txt && echo "*** Warning: The R.txt that is being packaged might not agree with the R.txt that was built. This is normal during l10n repacks.") || \
     diff $(GECKO_APP_AP_PATH)/R.txt $(GECKO_APP_AP_PATH)/gecko-nodeps/R.txt >/dev/null || \
     (echo "*** Error: The R.txt that was built and the R.txt that is being packaged are not the same. Rebuild mobile/android/base and re-package." && exit 1)) && \
   ( cd $(STAGEPATH)$(MOZ_PKG_DIR)$(_BINPATH) && \
     unzip -o $(_ABS_DIST)/gecko.ap_ && \
     rm $(_ABS_DIST)/gecko.ap_ && \
-    $(ZIP) $(if $(MOZ_ENABLE_SZIP),-0 )$(_ABS_DIST)/gecko.ap_ $(ASSET_SO_LIBRARIES) && \
+    $(ZIP) $(if $(ALREADY_SZIPPED),-0 ,$(if $(MOZ_ENABLE_SZIP),-0 ))$(_ABS_DIST)/gecko.ap_ $(ASSET_SO_LIBRARIES) && \
     $(ZIP) -r9D $(_ABS_DIST)/gecko.ap_ $(DIST_FILES) -x $(NON_DIST_FILES) $(SZIP_LIBRARIES) && \
     $(if $(filter-out ./,$(OMNIJAR_DIR)), \
       mkdir -p $(OMNIJAR_DIR) && mv $(OMNIJAR_NAME) $(OMNIJAR_DIR) && ) \
     $(ZIP) -0 $(_ABS_DIST)/gecko.ap_ $(OMNIJAR_DIR)$(OMNIJAR_NAME)) && \
   rm -f $(_ABS_DIST)/gecko.apk && \
   cp $(_ABS_DIST)/gecko.ap_ $(_ABS_DIST)/gecko.apk && \
   $(ZIP) -j0 $(_ABS_DIST)/gecko.apk $(STAGEPATH)$(MOZ_PKG_DIR)$(_BINPATH)/classes.dex && \
   cp $(_ABS_DIST)/gecko.apk $(_ABS_DIST)/gecko-unsigned-unaligned.apk && \