python/mozbuild/mozpack/files.py

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/python/mozbuild/mozpack/files.py	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,868 @@
     1.4 +# This Source Code Form is subject to the terms of the Mozilla Public
     1.5 +# License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 +# file, You can obtain one at http://mozilla.org/MPL/2.0/.
     1.7 +
     1.8 +import errno
     1.9 +import os
    1.10 +import platform
    1.11 +import shutil
    1.12 +import stat
    1.13 +import subprocess
    1.14 +import uuid
    1.15 +import mozbuild.makeutil as makeutil
    1.16 +from mozbuild.preprocessor import Preprocessor
    1.17 +from mozbuild.util import FileAvoidWrite
    1.18 +from mozpack.executables import (
    1.19 +    is_executable,
    1.20 +    may_strip,
    1.21 +    strip,
    1.22 +    may_elfhack,
    1.23 +    elfhack,
    1.24 +)
    1.25 +from mozpack.chrome.manifest import ManifestEntry
    1.26 +from io import BytesIO
    1.27 +from mozpack.errors import (
    1.28 +    ErrorMessage,
    1.29 +    errors,
    1.30 +)
    1.31 +from mozpack.mozjar import JarReader
    1.32 +import mozpack.path
    1.33 +from collections import OrderedDict
    1.34 +from jsmin import JavascriptMinify
    1.35 +from tempfile import (
    1.36 +    mkstemp,
    1.37 +    NamedTemporaryFile,
    1.38 +)
    1.39 +
    1.40 +
    1.41 +class Dest(object):
    1.42 +    '''
    1.43 +    Helper interface for BaseFile.copy. The interface works as follows:
    1.44 +    - read() and write() can be used to sequentially read/write from the
    1.45 +      underlying file.
    1.46 +    - a call to read() after a write() will re-open the underlying file and
    1.47 +      read from it.
    1.48 +    - a call to write() after a read() will re-open the underlying file,
    1.49 +      emptying it, and write to it.
    1.50 +    '''
    1.51 +    def __init__(self, path):
    1.52 +        self.path = path
    1.53 +        self.mode = None
    1.54 +
    1.55 +    @property
    1.56 +    def name(self):
    1.57 +        return self.path
    1.58 +
    1.59 +    def read(self, length=-1):
    1.60 +        if self.mode != 'r':
    1.61 +            self.file = open(self.path, 'rb')
    1.62 +            self.mode = 'r'
    1.63 +        return self.file.read(length)
    1.64 +
    1.65 +    def write(self, data):
    1.66 +        if self.mode != 'w':
    1.67 +            self.file = open(self.path, 'wb')
    1.68 +            self.mode = 'w'
    1.69 +        return self.file.write(data)
    1.70 +
    1.71 +    def exists(self):
    1.72 +        return os.path.exists(self.path)
    1.73 +
    1.74 +    def close(self):
    1.75 +        if self.mode:
    1.76 +            self.mode = None
    1.77 +            self.file.close()
    1.78 +
    1.79 +
    1.80 +class BaseFile(object):
    1.81 +    '''
    1.82 +    Base interface and helper for file copying. Derived class may implement
    1.83 +    their own copy function, or rely on BaseFile.copy using the open() member
    1.84 +    function and/or the path property.
    1.85 +    '''
    1.86 +    @staticmethod
    1.87 +    def is_older(first, second):
    1.88 +        '''
    1.89 +        Compares the modification time of two files, and returns whether the
    1.90 +        ``first`` file is older than the ``second`` file.
    1.91 +        '''
    1.92 +        # os.path.getmtime returns a result in seconds with precision up to
    1.93 +        # the microsecond. But microsecond is too precise because
    1.94 +        # shutil.copystat only copies milliseconds, and seconds is not
    1.95 +        # enough precision.
    1.96 +        return int(os.path.getmtime(first) * 1000) \
    1.97 +                <= int(os.path.getmtime(second) * 1000)
    1.98 +
    1.99 +    @staticmethod
   1.100 +    def any_newer(dest, inputs):
   1.101 +        '''
   1.102 +        Compares the modification time of ``dest`` to multiple input files, and
   1.103 +        returns whether any of the ``inputs`` is newer (has a later mtime) than
   1.104 +        ``dest``.
   1.105 +        '''
   1.106 +        # os.path.getmtime returns a result in seconds with precision up to
   1.107 +        # the microsecond. But microsecond is too precise because
   1.108 +        # shutil.copystat only copies milliseconds, and seconds is not
   1.109 +        # enough precision.
   1.110 +        dest_mtime = int(os.path.getmtime(dest) * 1000)
   1.111 +        for input in inputs:
   1.112 +            if dest_mtime < int(os.path.getmtime(input) * 1000):
   1.113 +                return True
   1.114 +        return False
   1.115 +
   1.116 +    def copy(self, dest, skip_if_older=True):
   1.117 +        '''
   1.118 +        Copy the BaseFile content to the destination given as a string or a
   1.119 +        Dest instance. Avoids replacing existing files if the BaseFile content
   1.120 +        matches that of the destination, or in case of plain files, if the
   1.121 +        destination is newer than the original file. This latter behaviour is
   1.122 +        disabled when skip_if_older is False.
   1.123 +        Returns whether a copy was actually performed (True) or not (False).
   1.124 +        '''
   1.125 +        if isinstance(dest, basestring):
   1.126 +            dest = Dest(dest)
   1.127 +        else:
   1.128 +            assert isinstance(dest, Dest)
   1.129 +
   1.130 +        can_skip_content_check = False
   1.131 +        if not dest.exists():
   1.132 +            can_skip_content_check = True
   1.133 +        elif getattr(self, 'path', None) and getattr(dest, 'path', None):
   1.134 +            if skip_if_older and BaseFile.is_older(self.path, dest.path):
   1.135 +                return False
   1.136 +            elif os.path.getsize(self.path) != os.path.getsize(dest.path):
   1.137 +                can_skip_content_check = True
   1.138 +
   1.139 +        if can_skip_content_check:
   1.140 +            if getattr(self, 'path', None) and getattr(dest, 'path', None):
   1.141 +                shutil.copy2(self.path, dest.path)
   1.142 +            else:
   1.143 +                # Ensure the file is always created
   1.144 +                if not dest.exists():
   1.145 +                    dest.write('')
   1.146 +                shutil.copyfileobj(self.open(), dest)
   1.147 +            return True
   1.148 +
   1.149 +        src = self.open()
   1.150 +        copy_content = ''
   1.151 +        while True:
   1.152 +            dest_content = dest.read(32768)
   1.153 +            src_content = src.read(32768)
   1.154 +            copy_content += src_content
   1.155 +            if len(dest_content) == len(src_content) == 0:
   1.156 +                break
   1.157 +            # If the read content differs between origin and destination,
   1.158 +            # write what was read up to now, and copy the remainder.
   1.159 +            if dest_content != src_content:
   1.160 +                dest.write(copy_content)
   1.161 +                shutil.copyfileobj(src, dest)
   1.162 +                break
   1.163 +        if hasattr(self, 'path') and hasattr(dest, 'path'):
   1.164 +            shutil.copystat(self.path, dest.path)
   1.165 +        return True
   1.166 +
   1.167 +    def open(self):
   1.168 +        '''
   1.169 +        Return a file-like object allowing to read() the content of the
   1.170 +        associated file. This is meant to be overloaded in subclasses to return
   1.171 +        a custom file-like object.
   1.172 +        '''
   1.173 +        assert self.path is not None
   1.174 +        return open(self.path, 'rb')
   1.175 +
   1.176 +    @property
   1.177 +    def mode(self):
   1.178 +        '''
   1.179 +        Return the file's unix mode, or None if it has no meaning.
   1.180 +        '''
   1.181 +        return None
   1.182 +
   1.183 +
   1.184 +class File(BaseFile):
   1.185 +    '''
   1.186 +    File class for plain files.
   1.187 +    '''
   1.188 +    def __init__(self, path):
   1.189 +        self.path = path
   1.190 +
   1.191 +    @property
   1.192 +    def mode(self):
   1.193 +        '''
   1.194 +        Return the file's unix mode, as returned by os.stat().st_mode.
   1.195 +        '''
   1.196 +        if platform.system() == 'Windows':
   1.197 +            return None
   1.198 +        assert self.path is not None
   1.199 +        return os.stat(self.path).st_mode
   1.200 +
   1.201 +class ExecutableFile(File):
   1.202 +    '''
   1.203 +    File class for executable and library files on OS/2, OS/X and ELF systems.
   1.204 +    (see mozpack.executables.is_executable documentation).
   1.205 +    '''
   1.206 +    def copy(self, dest, skip_if_older=True):
   1.207 +        real_dest = dest
   1.208 +        if not isinstance(dest, basestring):
   1.209 +            fd, dest = mkstemp()
   1.210 +            os.close(fd)
   1.211 +            os.remove(dest)
   1.212 +        assert isinstance(dest, basestring)
   1.213 +        # If File.copy didn't actually copy because dest is newer, check the
   1.214 +        # file sizes. If dest is smaller, it means it is already stripped and
   1.215 +        # elfhacked, so we can skip.
   1.216 +        if not File.copy(self, dest, skip_if_older) and \
   1.217 +                os.path.getsize(self.path) > os.path.getsize(dest):
   1.218 +            return False
   1.219 +        try:
   1.220 +            if may_strip(dest):
   1.221 +                strip(dest)
   1.222 +            if may_elfhack(dest):
   1.223 +                elfhack(dest)
   1.224 +        except ErrorMessage:
   1.225 +            os.remove(dest)
   1.226 +            raise
   1.227 +
   1.228 +        if real_dest != dest:
   1.229 +            f = File(dest)
   1.230 +            ret = f.copy(real_dest, skip_if_older)
   1.231 +            os.remove(dest)
   1.232 +            return ret
   1.233 +        return True
   1.234 +
   1.235 +
   1.236 +class AbsoluteSymlinkFile(File):
   1.237 +    '''File class that is copied by symlinking (if available).
   1.238 +
   1.239 +    This class only works if the target path is absolute.
   1.240 +    '''
   1.241 +
   1.242 +    def __init__(self, path):
   1.243 +        if not os.path.isabs(path):
   1.244 +            raise ValueError('Symlink target not absolute: %s' % path)
   1.245 +
   1.246 +        File.__init__(self, path)
   1.247 +
   1.248 +    def copy(self, dest, skip_if_older=True):
   1.249 +        assert isinstance(dest, basestring)
   1.250 +
   1.251 +        # The logic in this function is complicated by the fact that symlinks
   1.252 +        # aren't universally supported. So, where symlinks aren't supported, we
   1.253 +        # fall back to file copying. Keep in mind that symlink support is
   1.254 +        # per-filesystem, not per-OS.
   1.255 +
   1.256 +        # Handle the simple case where symlinks are definitely not supported by
   1.257 +        # falling back to file copy.
   1.258 +        if not hasattr(os, 'symlink'):
   1.259 +            return File.copy(self, dest, skip_if_older=skip_if_older)
   1.260 +
   1.261 +        # Always verify the symlink target path exists.
   1.262 +        if not os.path.exists(self.path):
   1.263 +            raise ErrorMessage('Symlink target path does not exist: %s' % self.path)
   1.264 +
   1.265 +        st = None
   1.266 +
   1.267 +        try:
   1.268 +            st = os.lstat(dest)
   1.269 +        except OSError as ose:
   1.270 +            if ose.errno != errno.ENOENT:
   1.271 +                raise
   1.272 +
   1.273 +        # If the dest is a symlink pointing to us, we have nothing to do.
   1.274 +        # If it's the wrong symlink, the filesystem must support symlinks,
   1.275 +        # so we replace with a proper symlink.
   1.276 +        if st and stat.S_ISLNK(st.st_mode):
   1.277 +            link = os.readlink(dest)
   1.278 +            if link == self.path:
   1.279 +                return False
   1.280 +
   1.281 +            os.remove(dest)
   1.282 +            os.symlink(self.path, dest)
   1.283 +            return True
   1.284 +
   1.285 +        # If the destination doesn't exist, we try to create a symlink. If that
   1.286 +        # fails, we fall back to copy code.
   1.287 +        if not st:
   1.288 +            try:
   1.289 +                os.symlink(self.path, dest)
   1.290 +                return True
   1.291 +            except OSError:
   1.292 +                return File.copy(self, dest, skip_if_older=skip_if_older)
   1.293 +
   1.294 +        # Now the complicated part. If the destination exists, we could be
   1.295 +        # replacing a file with a symlink. Or, the filesystem may not support
   1.296 +        # symlinks. We want to minimize I/O overhead for performance reasons,
   1.297 +        # so we keep the existing destination file around as long as possible.
   1.298 +        # A lot of the system calls would be eliminated if we cached whether
   1.299 +        # symlinks are supported. However, even if we performed a single
   1.300 +        # up-front test of whether the root of the destination directory
   1.301 +        # supports symlinks, there's no guarantee that all operations for that
   1.302 +        # dest (or source) would be on the same filesystem and would support
   1.303 +        # symlinks.
   1.304 +        #
   1.305 +        # Our strategy is to attempt to create a new symlink with a random
   1.306 +        # name. If that fails, we fall back to copy mode. If that works, we
   1.307 +        # remove the old destination and move the newly-created symlink into
   1.308 +        # its place.
   1.309 +
   1.310 +        temp_dest = os.path.join(os.path.dirname(dest), str(uuid.uuid4()))
   1.311 +        try:
   1.312 +            os.symlink(self.path, temp_dest)
   1.313 +        # TODO Figure out exactly how symlink creation fails and only trap
   1.314 +        # that.
   1.315 +        except EnvironmentError:
   1.316 +            return File.copy(self, dest, skip_if_older=skip_if_older)
   1.317 +
   1.318 +        # If removing the original file fails, don't forget to clean up the
   1.319 +        # temporary symlink.
   1.320 +        try:
   1.321 +            os.remove(dest)
   1.322 +        except EnvironmentError:
   1.323 +            os.remove(temp_dest)
   1.324 +            raise
   1.325 +
   1.326 +        os.rename(temp_dest, dest)
   1.327 +        return True
   1.328 +
   1.329 +
   1.330 +class ExistingFile(BaseFile):
   1.331 +    '''
   1.332 +    File class that represents a file that may exist but whose content comes
   1.333 +    from elsewhere.
   1.334 +
   1.335 +    This purpose of this class is to account for files that are installed via
   1.336 +    external means. It is typically only used in manifests or in registries to
   1.337 +    account for files.
   1.338 +
   1.339 +    When asked to copy, this class does nothing because nothing is known about
   1.340 +    the source file/data.
   1.341 +
   1.342 +    Instances of this class come in two flavors: required and optional. If an
   1.343 +    existing file is required, it must exist during copy() or an error is
   1.344 +    raised.
   1.345 +    '''
   1.346 +    def __init__(self, required):
   1.347 +        self.required = required
   1.348 +
   1.349 +    def copy(self, dest, skip_if_older=True):
   1.350 +        if isinstance(dest, basestring):
   1.351 +            dest = Dest(dest)
   1.352 +        else:
   1.353 +            assert isinstance(dest, Dest)
   1.354 +
   1.355 +        if not self.required:
   1.356 +            return
   1.357 +
   1.358 +        if not dest.exists():
   1.359 +            errors.fatal("Required existing file doesn't exist: %s" %
   1.360 +                dest.path)
   1.361 +
   1.362 +
   1.363 +class PreprocessedFile(BaseFile):
   1.364 +    '''
   1.365 +    File class for a file that is preprocessed. PreprocessedFile.copy() runs
   1.366 +    the preprocessor on the file to create the output.
   1.367 +    '''
   1.368 +    def __init__(self, path, depfile_path, marker, defines, extra_depends=None):
   1.369 +        self.path = path
   1.370 +        self.depfile = depfile_path
   1.371 +        self.marker = marker
   1.372 +        self.defines = defines
   1.373 +        self.extra_depends = list(extra_depends or [])
   1.374 +
   1.375 +    def copy(self, dest, skip_if_older=True):
   1.376 +        '''
   1.377 +        Invokes the preprocessor to create the destination file.
   1.378 +        '''
   1.379 +        if isinstance(dest, basestring):
   1.380 +            dest = Dest(dest)
   1.381 +        else:
   1.382 +            assert isinstance(dest, Dest)
   1.383 +
   1.384 +        # We have to account for the case where the destination exists and is a
   1.385 +        # symlink to something. Since we know the preprocessor is certainly not
   1.386 +        # going to create a symlink, we can just remove the existing one. If the
   1.387 +        # destination is not a symlink, we leave it alone, since we're going to
   1.388 +        # overwrite its contents anyway.
   1.389 +        # If symlinks aren't supported at all, we can skip this step.
   1.390 +        if hasattr(os, 'symlink'):
   1.391 +            if os.path.islink(dest.path):
   1.392 +                os.remove(dest.path)
   1.393 +
   1.394 +        pp_deps = set(self.extra_depends)
   1.395 +
   1.396 +        # If a dependency file was specified, and it exists, add any
   1.397 +        # dependencies from that file to our list.
   1.398 +        if self.depfile and os.path.exists(self.depfile):
   1.399 +            target = mozpack.path.normpath(dest.name)
   1.400 +            with open(self.depfile, 'rb') as fileobj:
   1.401 +                for rule in makeutil.read_dep_makefile(fileobj):
   1.402 +                    if target in rule.targets():
   1.403 +                        pp_deps.update(rule.dependencies())
   1.404 +
   1.405 +        skip = False
   1.406 +        if dest.exists() and skip_if_older:
   1.407 +            # If a dependency file was specified, and it doesn't exist,
   1.408 +            # assume that the preprocessor needs to be rerun. That will
   1.409 +            # regenerate the dependency file.
   1.410 +            if self.depfile and not os.path.exists(self.depfile):
   1.411 +                skip = False
   1.412 +            else:
   1.413 +                skip = not BaseFile.any_newer(dest.path, pp_deps)
   1.414 +
   1.415 +        if skip:
   1.416 +            return False
   1.417 +
   1.418 +        deps_out = None
   1.419 +        if self.depfile:
   1.420 +            deps_out = FileAvoidWrite(self.depfile)
   1.421 +        pp = Preprocessor(defines=self.defines, marker=self.marker)
   1.422 +
   1.423 +        with open(self.path, 'rU') as input:
   1.424 +            pp.processFile(input=input, output=dest, depfile=deps_out)
   1.425 +
   1.426 +        dest.close()
   1.427 +        if self.depfile:
   1.428 +            deps_out.close()
   1.429 +
   1.430 +        return True
   1.431 +
   1.432 +
   1.433 +class GeneratedFile(BaseFile):
   1.434 +    '''
   1.435 +    File class for content with no previous existence on the filesystem.
   1.436 +    '''
   1.437 +    def __init__(self, content):
   1.438 +        self.content = content
   1.439 +
   1.440 +    def open(self):
   1.441 +        return BytesIO(self.content)
   1.442 +
   1.443 +
   1.444 +class DeflatedFile(BaseFile):
   1.445 +    '''
   1.446 +    File class for members of a jar archive. DeflatedFile.copy() effectively
   1.447 +    extracts the file from the jar archive.
   1.448 +    '''
   1.449 +    def __init__(self, file):
   1.450 +        from mozpack.mozjar import JarFileReader
   1.451 +        assert isinstance(file, JarFileReader)
   1.452 +        self.file = file
   1.453 +
   1.454 +    def open(self):
   1.455 +        self.file.seek(0)
   1.456 +        return self.file
   1.457 +
   1.458 +
   1.459 +class XPTFile(GeneratedFile):
   1.460 +    '''
   1.461 +    File class for a linked XPT file. It takes several XPT files as input
   1.462 +    (using the add() and remove() member functions), and links them at copy()
   1.463 +    time.
   1.464 +    '''
   1.465 +    def __init__(self):
   1.466 +        self._files = set()
   1.467 +
   1.468 +    def add(self, xpt):
   1.469 +        '''
   1.470 +        Add the given XPT file (as a BaseFile instance) to the list of XPTs
   1.471 +        to link.
   1.472 +        '''
   1.473 +        assert isinstance(xpt, BaseFile)
   1.474 +        self._files.add(xpt)
   1.475 +
   1.476 +    def remove(self, xpt):
   1.477 +        '''
   1.478 +        Remove the given XPT file (as a BaseFile instance) from the list of
   1.479 +        XPTs to link.
   1.480 +        '''
   1.481 +        assert isinstance(xpt, BaseFile)
   1.482 +        self._files.remove(xpt)
   1.483 +
   1.484 +    def copy(self, dest, skip_if_older=True):
   1.485 +        '''
   1.486 +        Link the registered XPTs and place the resulting linked XPT at the
   1.487 +        destination given as a string or a Dest instance. Avoids an expensive
   1.488 +        XPT linking if the interfaces in an existing destination match those of
   1.489 +        the individual XPTs to link.
   1.490 +        skip_if_older is ignored.
   1.491 +        '''
   1.492 +        if isinstance(dest, basestring):
   1.493 +            dest = Dest(dest)
   1.494 +        assert isinstance(dest, Dest)
   1.495 +
   1.496 +        from xpt import xpt_link, Typelib, Interface
   1.497 +        all_typelibs = [Typelib.read(f.open()) for f in self._files]
   1.498 +        if dest.exists():
   1.499 +            # Typelib.read() needs to seek(), so use a BytesIO for dest
   1.500 +            # content.
   1.501 +            dest_interfaces = \
   1.502 +                dict((i.name, i)
   1.503 +                     for i in Typelib.read(BytesIO(dest.read())).interfaces
   1.504 +                     if i.iid != Interface.UNRESOLVED_IID)
   1.505 +            identical = True
   1.506 +            for f in self._files:
   1.507 +                typelib = Typelib.read(f.open())
   1.508 +                for i in typelib.interfaces:
   1.509 +                    if i.iid != Interface.UNRESOLVED_IID and \
   1.510 +                            not (i.name in dest_interfaces and
   1.511 +                                 i == dest_interfaces[i.name]):
   1.512 +                        identical = False
   1.513 +                        break
   1.514 +            if identical:
   1.515 +                return False
   1.516 +        s = BytesIO()
   1.517 +        xpt_link(all_typelibs).write(s)
   1.518 +        dest.write(s.getvalue())
   1.519 +        return True
   1.520 +
   1.521 +    def open(self):
   1.522 +        raise RuntimeError("Unsupported")
   1.523 +
   1.524 +    def isempty(self):
   1.525 +        '''
   1.526 +        Return whether there are XPT files to link.
   1.527 +        '''
   1.528 +        return len(self._files) == 0
   1.529 +
   1.530 +
   1.531 +class ManifestFile(BaseFile):
   1.532 +    '''
   1.533 +    File class for a manifest file. It takes individual manifest entries (using
   1.534 +    the add() and remove() member functions), and adjusts them to be relative
   1.535 +    to the base path for the manifest, given at creation.
   1.536 +    Example:
   1.537 +        There is a manifest entry "content webapprt webapprt/content/" relative
   1.538 +        to "webapprt/chrome". When packaging, the entry will be stored in
   1.539 +        jar:webapprt/omni.ja!/chrome/chrome.manifest, which means the entry
   1.540 +        will have to be relative to "chrome" instead of "webapprt/chrome". This
   1.541 +        doesn't really matter when serializing the entry, since this base path
   1.542 +        is not written out, but it matters when moving the entry at the same
   1.543 +        time, e.g. to jar:webapprt/omni.ja!/chrome.manifest, which we don't do
   1.544 +        currently but could in the future.
   1.545 +    '''
   1.546 +    def __init__(self, base, entries=None):
   1.547 +        self._entries = entries if entries else []
   1.548 +        self._base = base
   1.549 +
   1.550 +    def add(self, entry):
   1.551 +        '''
   1.552 +        Add the given entry to the manifest. Entries are rebased at open() time
   1.553 +        instead of add() time so that they can be more easily remove()d.
   1.554 +        '''
   1.555 +        assert isinstance(entry, ManifestEntry)
   1.556 +        self._entries.append(entry)
   1.557 +
   1.558 +    def remove(self, entry):
   1.559 +        '''
   1.560 +        Remove the given entry from the manifest.
   1.561 +        '''
   1.562 +        assert isinstance(entry, ManifestEntry)
   1.563 +        self._entries.remove(entry)
   1.564 +
   1.565 +    def open(self):
   1.566 +        '''
   1.567 +        Return a file-like object allowing to read() the serialized content of
   1.568 +        the manifest.
   1.569 +        '''
   1.570 +        return BytesIO(''.join('%s\n' % e.rebase(self._base)
   1.571 +                               for e in self._entries))
   1.572 +
   1.573 +    def __iter__(self):
   1.574 +        '''
   1.575 +        Iterate over entries in the manifest file.
   1.576 +        '''
   1.577 +        return iter(self._entries)
   1.578 +
   1.579 +    def isempty(self):
   1.580 +        '''
   1.581 +        Return whether there are manifest entries to write
   1.582 +        '''
   1.583 +        return len(self._entries) == 0
   1.584 +
   1.585 +
   1.586 +class MinifiedProperties(BaseFile):
   1.587 +    '''
   1.588 +    File class for minified properties. This wraps around a BaseFile instance,
   1.589 +    and removes lines starting with a # from its content.
   1.590 +    '''
   1.591 +    def __init__(self, file):
   1.592 +        assert isinstance(file, BaseFile)
   1.593 +        self._file = file
   1.594 +
   1.595 +    def open(self):
   1.596 +        '''
   1.597 +        Return a file-like object allowing to read() the minified content of
   1.598 +        the properties file.
   1.599 +        '''
   1.600 +        return BytesIO(''.join(l for l in self._file.open().readlines()
   1.601 +                               if not l.startswith('#')))
   1.602 +
   1.603 +
   1.604 +class MinifiedJavaScript(BaseFile):
   1.605 +    '''
   1.606 +    File class for minifying JavaScript files.
   1.607 +    '''
   1.608 +    def __init__(self, file, verify_command=None):
   1.609 +        assert isinstance(file, BaseFile)
   1.610 +        self._file = file
   1.611 +        self._verify_command = verify_command
   1.612 +
   1.613 +    def open(self):
   1.614 +        output = BytesIO()
   1.615 +        minify = JavascriptMinify(self._file.open(), output)
   1.616 +        minify.minify()
   1.617 +        output.seek(0)
   1.618 +
   1.619 +        if not self._verify_command:
   1.620 +            return output
   1.621 +
   1.622 +        input_source = self._file.open().read()
   1.623 +        output_source = output.getvalue()
   1.624 +
   1.625 +        with NamedTemporaryFile() as fh1, NamedTemporaryFile() as fh2:
   1.626 +            fh1.write(input_source)
   1.627 +            fh2.write(output_source)
   1.628 +            fh1.flush()
   1.629 +            fh2.flush()
   1.630 +
   1.631 +            try:
   1.632 +                args = list(self._verify_command)
   1.633 +                args.extend([fh1.name, fh2.name])
   1.634 +                subprocess.check_output(args, stderr=subprocess.STDOUT)
   1.635 +            except subprocess.CalledProcessError as e:
   1.636 +                errors.warn('JS minification verification failed for %s:' %
   1.637 +                    (getattr(self._file, 'path', '<unknown>')))
   1.638 +                # Prefix each line with "Warning:" so mozharness doesn't
   1.639 +                # think these error messages are real errors.
   1.640 +                for line in e.output.splitlines():
   1.641 +                    errors.warn(line)
   1.642 +
   1.643 +                return self._file.open()
   1.644 +
   1.645 +        return output
   1.646 +
   1.647 +
   1.648 +class BaseFinder(object):
   1.649 +    def __init__(self, base, minify=False, minify_js=False,
   1.650 +        minify_js_verify_command=None):
   1.651 +        '''
   1.652 +        Initializes the instance with a reference base directory.
   1.653 +
   1.654 +        The optional minify argument specifies whether minification of code
   1.655 +        should occur. minify_js is an additional option to control minification
   1.656 +        of JavaScript. It requires minify to be True.
   1.657 +
   1.658 +        minify_js_verify_command can be used to optionally verify the results
   1.659 +        of JavaScript minification. If defined, it is expected to be an iterable
   1.660 +        that will constitute the first arguments to a called process which will
   1.661 +        receive the filenames of the original and minified JavaScript files.
   1.662 +        The invoked process can then verify the results. If minification is
   1.663 +        rejected, the process exits with a non-0 exit code and the original
   1.664 +        JavaScript source is used. An example value for this argument is
   1.665 +        ('/path/to/js', '/path/to/verify/script.js').
   1.666 +        '''
   1.667 +        if minify_js and not minify:
   1.668 +            raise ValueError('minify_js requires minify.')
   1.669 +
   1.670 +        self.base = base
   1.671 +        self._minify = minify
   1.672 +        self._minify_js = minify_js
   1.673 +        self._minify_js_verify_command = minify_js_verify_command
   1.674 +
   1.675 +    def find(self, pattern):
   1.676 +        '''
   1.677 +        Yield path, BaseFile_instance pairs for all files under the base
   1.678 +        directory and its subdirectories that match the given pattern. See the
   1.679 +        mozpack.path.match documentation for a description of the handled
   1.680 +        patterns.
   1.681 +        '''
   1.682 +        while pattern.startswith('/'):
   1.683 +            pattern = pattern[1:]
   1.684 +        for p, f in self._find(pattern):
   1.685 +            yield p, self._minify_file(p, f)
   1.686 +
   1.687 +    def __iter__(self):
   1.688 +        '''
   1.689 +        Iterates over all files under the base directory (excluding files
   1.690 +        starting with a '.' and files at any level under a directory starting
   1.691 +        with a '.').
   1.692 +            for path, file in finder:
   1.693 +                ...
   1.694 +        '''
   1.695 +        return self.find('')
   1.696 +
   1.697 +    def __contains__(self, pattern):
   1.698 +        raise RuntimeError("'in' operator forbidden for %s. Use contains()." %
   1.699 +                           self.__class__.__name__)
   1.700 +
   1.701 +    def contains(self, pattern):
   1.702 +        '''
   1.703 +        Return whether some files under the base directory match the given
   1.704 +        pattern. See the mozpack.path.match documentation for a description of
   1.705 +        the handled patterns.
   1.706 +        '''
   1.707 +        return any(self.find(pattern))
   1.708 +
   1.709 +    def _minify_file(self, path, file):
   1.710 +        '''
   1.711 +        Return an appropriate MinifiedSomething wrapper for the given BaseFile
   1.712 +        instance (file), according to the file type (determined by the given
   1.713 +        path), if the FileFinder was created with minification enabled.
   1.714 +        Otherwise, just return the given BaseFile instance.
   1.715 +        '''
   1.716 +        if not self._minify or isinstance(file, ExecutableFile):
   1.717 +            return file
   1.718 +
   1.719 +        if path.endswith('.properties'):
   1.720 +            return MinifiedProperties(file)
   1.721 +
   1.722 +        if self._minify_js and path.endswith(('.js', '.jsm')):
   1.723 +            return MinifiedJavaScript(file, self._minify_js_verify_command)
   1.724 +
   1.725 +        return file
   1.726 +
   1.727 +
   1.728 +class FileFinder(BaseFinder):
   1.729 +    '''
   1.730 +    Helper to get appropriate BaseFile instances from the file system.
   1.731 +    '''
   1.732 +    def __init__(self, base, find_executables=True, ignore=(), **kargs):
   1.733 +        '''
   1.734 +        Create a FileFinder for files under the given base directory.
   1.735 +
   1.736 +        The find_executables argument determines whether the finder needs to
   1.737 +        try to guess whether files are executables. Disabling this guessing
   1.738 +        when not necessary can speed up the finder significantly.
   1.739 +
   1.740 +        ``ignore`` accepts an iterable of patterns to ignore. Entries are
   1.741 +        strings that match paths relative to ``base`` using
   1.742 +        ``mozpack.path.match()``. This means if an entry corresponds
   1.743 +        to a directory, all files under that directory will be ignored. If
   1.744 +        an entry corresponds to a file, that particular file will be ignored.
   1.745 +        '''
   1.746 +        BaseFinder.__init__(self, base, **kargs)
   1.747 +        self.find_executables = find_executables
   1.748 +        self.ignore = ignore
   1.749 +
   1.750 +    def _find(self, pattern):
   1.751 +        '''
   1.752 +        Actual implementation of FileFinder.find(), dispatching to specialized
   1.753 +        member functions depending on what kind of pattern was given.
   1.754 +        Note all files with a name starting with a '.' are ignored when
   1.755 +        scanning directories, but are not ignored when explicitely requested.
   1.756 +        '''
   1.757 +        if '*' in pattern:
   1.758 +            return self._find_glob('', mozpack.path.split(pattern))
   1.759 +        elif os.path.isdir(os.path.join(self.base, pattern)):
   1.760 +            return self._find_dir(pattern)
   1.761 +        else:
   1.762 +            return self._find_file(pattern)
   1.763 +
   1.764 +    def _find_dir(self, path):
   1.765 +        '''
   1.766 +        Actual implementation of FileFinder.find() when the given pattern
   1.767 +        corresponds to an existing directory under the base directory.
   1.768 +        Ignores file names starting with a '.' under the given path. If the
   1.769 +        path itself has leafs starting with a '.', they are not ignored.
   1.770 +        '''
   1.771 +        for p in self.ignore:
   1.772 +            if mozpack.path.match(path, p):
   1.773 +                return
   1.774 +
   1.775 +        # The sorted makes the output idempotent. Otherwise, we are
   1.776 +        # likely dependent on filesystem implementation details, such as
   1.777 +        # inode ordering.
   1.778 +        for p in sorted(os.listdir(os.path.join(self.base, path))):
   1.779 +            if p.startswith('.'):
   1.780 +                continue
   1.781 +            for p_, f in self._find(mozpack.path.join(path, p)):
   1.782 +                yield p_, f
   1.783 +
   1.784 +    def _find_file(self, path):
   1.785 +        '''
   1.786 +        Actual implementation of FileFinder.find() when the given pattern
   1.787 +        corresponds to an existing file under the base directory.
   1.788 +        '''
   1.789 +        srcpath = os.path.join(self.base, path)
   1.790 +        if not os.path.exists(srcpath):
   1.791 +            return
   1.792 +
   1.793 +        for p in self.ignore:
   1.794 +            if mozpack.path.match(path, p):
   1.795 +                return
   1.796 +
   1.797 +        if self.find_executables and is_executable(srcpath):
   1.798 +            yield path, ExecutableFile(srcpath)
   1.799 +        else:
   1.800 +            yield path, File(srcpath)
   1.801 +
   1.802 +    def _find_glob(self, base, pattern):
   1.803 +        '''
   1.804 +        Actual implementation of FileFinder.find() when the given pattern
   1.805 +        contains globbing patterns ('*' or '**'). This is meant to be an
   1.806 +        equivalent of:
   1.807 +            for p, f in self:
   1.808 +                if mozpack.path.match(p, pattern):
   1.809 +                    yield p, f
   1.810 +        but avoids scanning the entire tree.
   1.811 +        '''
   1.812 +        if not pattern:
   1.813 +            for p, f in self._find(base):
   1.814 +                yield p, f
   1.815 +        elif pattern[0] == '**':
   1.816 +            for p, f in self._find(base):
   1.817 +                if mozpack.path.match(p, mozpack.path.join(*pattern)):
   1.818 +                    yield p, f
   1.819 +        elif '*' in pattern[0]:
   1.820 +            if not os.path.exists(os.path.join(self.base, base)):
   1.821 +                return
   1.822 +
   1.823 +            for p in self.ignore:
   1.824 +                if mozpack.path.match(base, p):
   1.825 +                    return
   1.826 +
   1.827 +            # See above comment w.r.t. sorted() and idempotent behavior.
   1.828 +            for p in sorted(os.listdir(os.path.join(self.base, base))):
   1.829 +                if p.startswith('.') and not pattern[0].startswith('.'):
   1.830 +                    continue
   1.831 +                if mozpack.path.match(p, pattern[0]):
   1.832 +                    for p_, f in self._find_glob(mozpack.path.join(base, p),
   1.833 +                                                 pattern[1:]):
   1.834 +                        yield p_, f
   1.835 +        else:
   1.836 +            for p, f in self._find_glob(mozpack.path.join(base, pattern[0]),
   1.837 +                                        pattern[1:]):
   1.838 +                yield p, f
   1.839 +
   1.840 +
   1.841 +class JarFinder(BaseFinder):
   1.842 +    '''
   1.843 +    Helper to get appropriate DeflatedFile instances from a JarReader.
   1.844 +    '''
   1.845 +    def __init__(self, base, reader, **kargs):
   1.846 +        '''
   1.847 +        Create a JarFinder for files in the given JarReader. The base argument
   1.848 +        is used as an indication of the Jar file location.
   1.849 +        '''
   1.850 +        assert isinstance(reader, JarReader)
   1.851 +        BaseFinder.__init__(self, base, **kargs)
   1.852 +        self._files = OrderedDict((f.filename, f) for f in reader)
   1.853 +
   1.854 +    def _find(self, pattern):
   1.855 +        '''
   1.856 +        Actual implementation of JarFinder.find(), dispatching to specialized
   1.857 +        member functions depending on what kind of pattern was given.
   1.858 +        '''
   1.859 +        if '*' in pattern:
   1.860 +            for p in self._files:
   1.861 +                if mozpack.path.match(p, pattern):
   1.862 +                    yield p, DeflatedFile(self._files[p])
   1.863 +        elif pattern == '':
   1.864 +            for p in self._files:
   1.865 +                yield p, DeflatedFile(self._files[p])
   1.866 +        elif pattern in self._files:
   1.867 +            yield pattern, DeflatedFile(self._files[pattern])
   1.868 +        else:
   1.869 +            for p in self._files:
   1.870 +                if mozpack.path.basedir(p, [pattern]) == pattern:
   1.871 +                    yield p, DeflatedFile(self._files[p])

mercurial