| # 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/. |
| |
| from __future__ import absolute_import |
| |
| import errno |
| import os |
| import platform |
| import shutil |
| import stat |
| import subprocess |
| import uuid |
| import mozbuild.makeutil as makeutil |
| from mozbuild.preprocessor import Preprocessor |
| from mozbuild.util import FileAvoidWrite |
| from mozpack.executables import ( |
| is_executable, |
| may_strip, |
| strip, |
| may_elfhack, |
| elfhack, |
| ) |
| from mozpack.chrome.manifest import ManifestEntry |
| from io import BytesIO |
| from mozpack.errors import ( |
| ErrorMessage, |
| errors, |
| ) |
| from mozpack.mozjar import JarReader |
| import mozpack.path as mozpath |
| from collections import OrderedDict |
| from jsmin import JavascriptMinify |
| from tempfile import ( |
| mkstemp, |
| NamedTemporaryFile, |
| ) |
| |
| try: |
| import hglib |
| except ImportError: |
| hglib = None |
| |
| |
| # For clean builds, copying files on win32 using CopyFile through ctypes is |
| # ~2x as fast as using shutil.copyfile. |
| if platform.system() != 'Windows': |
| _copyfile = shutil.copyfile |
| else: |
| import ctypes |
| _kernel32 = ctypes.windll.kernel32 |
| _CopyFileA = _kernel32.CopyFileA |
| _CopyFileW = _kernel32.CopyFileW |
| |
| def _copyfile(src, dest): |
| # False indicates `dest` should be overwritten if it exists already. |
| if isinstance(src, unicode) and isinstance(dest, unicode): |
| _CopyFileW(src, dest, False) |
| elif isinstance(src, str) and isinstance(dest, str): |
| _CopyFileA(src, dest, False) |
| else: |
| raise TypeError('mismatched path types!') |
| |
| class Dest(object): |
| ''' |
| Helper interface for BaseFile.copy. The interface works as follows: |
| - read() and write() can be used to sequentially read/write from the |
| underlying file. |
| - a call to read() after a write() will re-open the underlying file and |
| read from it. |
| - a call to write() after a read() will re-open the underlying file, |
| emptying it, and write to it. |
| ''' |
| def __init__(self, path): |
| self.path = path |
| self.mode = None |
| |
| @property |
| def name(self): |
| return self.path |
| |
| def read(self, length=-1): |
| if self.mode != 'r': |
| self.file = open(self.path, 'rb') |
| self.mode = 'r' |
| return self.file.read(length) |
| |
| def write(self, data): |
| if self.mode != 'w': |
| self.file = open(self.path, 'wb') |
| self.mode = 'w' |
| return self.file.write(data) |
| |
| def exists(self): |
| return os.path.exists(self.path) |
| |
| def close(self): |
| if self.mode: |
| self.mode = None |
| self.file.close() |
| |
| |
| class BaseFile(object): |
| ''' |
| Base interface and helper for file copying. Derived class may implement |
| their own copy function, or rely on BaseFile.copy using the open() member |
| function and/or the path property. |
| ''' |
| @staticmethod |
| def is_older(first, second): |
| ''' |
| Compares the modification time of two files, and returns whether the |
| ``first`` file is older than the ``second`` file. |
| ''' |
| # os.path.getmtime returns a result in seconds with precision up to |
| # the microsecond. But microsecond is too precise because |
| # shutil.copystat only copies milliseconds, and seconds is not |
| # enough precision. |
| return int(os.path.getmtime(first) * 1000) \ |
| <= int(os.path.getmtime(second) * 1000) |
| |
| @staticmethod |
| def any_newer(dest, inputs): |
| ''' |
| Compares the modification time of ``dest`` to multiple input files, and |
| returns whether any of the ``inputs`` is newer (has a later mtime) than |
| ``dest``. |
| ''' |
| # os.path.getmtime returns a result in seconds with precision up to |
| # the microsecond. But microsecond is too precise because |
| # shutil.copystat only copies milliseconds, and seconds is not |
| # enough precision. |
| dest_mtime = int(os.path.getmtime(dest) * 1000) |
| for input in inputs: |
| if dest_mtime < int(os.path.getmtime(input) * 1000): |
| return True |
| return False |
| |
| def copy(self, dest, skip_if_older=True): |
| ''' |
| Copy the BaseFile content to the destination given as a string or a |
| Dest instance. Avoids replacing existing files if the BaseFile content |
| matches that of the destination, or in case of plain files, if the |
| destination is newer than the original file. This latter behaviour is |
| disabled when skip_if_older is False. |
| Returns whether a copy was actually performed (True) or not (False). |
| ''' |
| if isinstance(dest, basestring): |
| dest = Dest(dest) |
| else: |
| assert isinstance(dest, Dest) |
| |
| can_skip_content_check = False |
| if not dest.exists(): |
| can_skip_content_check = True |
| elif getattr(self, 'path', None) and getattr(dest, 'path', None): |
| if skip_if_older and BaseFile.is_older(self.path, dest.path): |
| return False |
| elif os.path.getsize(self.path) != os.path.getsize(dest.path): |
| can_skip_content_check = True |
| |
| if can_skip_content_check: |
| if getattr(self, 'path', None) and getattr(dest, 'path', None): |
| _copyfile(self.path, dest.path) |
| shutil.copystat(self.path, dest.path) |
| else: |
| # Ensure the file is always created |
| if not dest.exists(): |
| dest.write('') |
| shutil.copyfileobj(self.open(), dest) |
| return True |
| |
| src = self.open() |
| copy_content = '' |
| while True: |
| dest_content = dest.read(32768) |
| src_content = src.read(32768) |
| copy_content += src_content |
| if len(dest_content) == len(src_content) == 0: |
| break |
| # If the read content differs between origin and destination, |
| # write what was read up to now, and copy the remainder. |
| if dest_content != src_content: |
| dest.write(copy_content) |
| shutil.copyfileobj(src, dest) |
| break |
| if hasattr(self, 'path') and hasattr(dest, 'path'): |
| shutil.copystat(self.path, dest.path) |
| return True |
| |
| def open(self): |
| ''' |
| Return a file-like object allowing to read() the content of the |
| associated file. This is meant to be overloaded in subclasses to return |
| a custom file-like object. |
| ''' |
| assert self.path is not None |
| return open(self.path, 'rb') |
| |
| def read(self): |
| raise NotImplementedError('BaseFile.read() not implemented. Bug 1170329.') |
| |
| @property |
| def mode(self): |
| ''' |
| Return the file's unix mode, or None if it has no meaning. |
| ''' |
| return None |
| |
| |
| class File(BaseFile): |
| ''' |
| File class for plain files. |
| ''' |
| def __init__(self, path): |
| self.path = path |
| |
| @property |
| def mode(self): |
| ''' |
| Return the file's unix mode, as returned by os.stat().st_mode. |
| ''' |
| if platform.system() == 'Windows': |
| return None |
| assert self.path is not None |
| mode = os.stat(self.path).st_mode |
| # Normalize file mode: |
| # - keep file type (e.g. S_IFREG) |
| ret = stat.S_IFMT(mode) |
| # - expand user read and execute permissions to everyone |
| if mode & 0400: |
| ret |= 0444 |
| if mode & 0100: |
| ret |= 0111 |
| # - keep user write permissions |
| if mode & 0200: |
| ret |= 0200 |
| # - leave away sticky bit, setuid, setgid |
| return ret |
| |
| def read(self): |
| '''Return the contents of the file.''' |
| with open(self.path, 'rb') as fh: |
| return fh.read() |
| |
| |
| class ExecutableFile(File): |
| ''' |
| File class for executable and library files on OS/2, OS/X and ELF systems. |
| (see mozpack.executables.is_executable documentation). |
| ''' |
| def copy(self, dest, skip_if_older=True): |
| real_dest = dest |
| if not isinstance(dest, basestring): |
| fd, dest = mkstemp() |
| os.close(fd) |
| os.remove(dest) |
| assert isinstance(dest, basestring) |
| # If File.copy didn't actually copy because dest is newer, check the |
| # file sizes. If dest is smaller, it means it is already stripped and |
| # elfhacked, so we can skip. |
| if not File.copy(self, dest, skip_if_older) and \ |
| os.path.getsize(self.path) > os.path.getsize(dest): |
| return False |
| try: |
| if may_strip(dest): |
| strip(dest) |
| if may_elfhack(dest): |
| elfhack(dest) |
| except ErrorMessage: |
| os.remove(dest) |
| raise |
| |
| if real_dest != dest: |
| f = File(dest) |
| ret = f.copy(real_dest, skip_if_older) |
| os.remove(dest) |
| return ret |
| return True |
| |
| |
| class AbsoluteSymlinkFile(File): |
| '''File class that is copied by symlinking (if available). |
| |
| This class only works if the target path is absolute. |
| ''' |
| |
| def __init__(self, path): |
| if not os.path.isabs(path): |
| raise ValueError('Symlink target not absolute: %s' % path) |
| |
| File.__init__(self, path) |
| |
| def copy(self, dest, skip_if_older=True): |
| assert isinstance(dest, basestring) |
| |
| # The logic in this function is complicated by the fact that symlinks |
| # aren't universally supported. So, where symlinks aren't supported, we |
| # fall back to file copying. Keep in mind that symlink support is |
| # per-filesystem, not per-OS. |
| |
| # Handle the simple case where symlinks are definitely not supported by |
| # falling back to file copy. |
| if not hasattr(os, 'symlink'): |
| return File.copy(self, dest, skip_if_older=skip_if_older) |
| |
| # Always verify the symlink target path exists. |
| if not os.path.exists(self.path): |
| raise ErrorMessage('Symlink target path does not exist: %s' % self.path) |
| |
| st = None |
| |
| try: |
| st = os.lstat(dest) |
| except OSError as ose: |
| if ose.errno != errno.ENOENT: |
| raise |
| |
| # If the dest is a symlink pointing to us, we have nothing to do. |
| # If it's the wrong symlink, the filesystem must support symlinks, |
| # so we replace with a proper symlink. |
| if st and stat.S_ISLNK(st.st_mode): |
| link = os.readlink(dest) |
| if link == self.path: |
| return False |
| |
| os.remove(dest) |
| os.symlink(self.path, dest) |
| return True |
| |
| # If the destination doesn't exist, we try to create a symlink. If that |
| # fails, we fall back to copy code. |
| if not st: |
| try: |
| os.symlink(self.path, dest) |
| return True |
| except OSError: |
| return File.copy(self, dest, skip_if_older=skip_if_older) |
| |
| # Now the complicated part. If the destination exists, we could be |
| # replacing a file with a symlink. Or, the filesystem may not support |
| # symlinks. We want to minimize I/O overhead for performance reasons, |
| # so we keep the existing destination file around as long as possible. |
| # A lot of the system calls would be eliminated if we cached whether |
| # symlinks are supported. However, even if we performed a single |
| # up-front test of whether the root of the destination directory |
| # supports symlinks, there's no guarantee that all operations for that |
| # dest (or source) would be on the same filesystem and would support |
| # symlinks. |
| # |
| # Our strategy is to attempt to create a new symlink with a random |
| # name. If that fails, we fall back to copy mode. If that works, we |
| # remove the old destination and move the newly-created symlink into |
| # its place. |
| |
| temp_dest = os.path.join(os.path.dirname(dest), str(uuid.uuid4())) |
| try: |
| os.symlink(self.path, temp_dest) |
| # TODO Figure out exactly how symlink creation fails and only trap |
| # that. |
| except EnvironmentError: |
| return File.copy(self, dest, skip_if_older=skip_if_older) |
| |
| # If removing the original file fails, don't forget to clean up the |
| # temporary symlink. |
| try: |
| os.remove(dest) |
| except EnvironmentError: |
| os.remove(temp_dest) |
| raise |
| |
| os.rename(temp_dest, dest) |
| return True |
| |
| |
| class ExistingFile(BaseFile): |
| ''' |
| File class that represents a file that may exist but whose content comes |
| from elsewhere. |
| |
| This purpose of this class is to account for files that are installed via |
| external means. It is typically only used in manifests or in registries to |
| account for files. |
| |
| When asked to copy, this class does nothing because nothing is known about |
| the source file/data. |
| |
| Instances of this class come in two flavors: required and optional. If an |
| existing file is required, it must exist during copy() or an error is |
| raised. |
| ''' |
| def __init__(self, required): |
| self.required = required |
| |
| def copy(self, dest, skip_if_older=True): |
| if isinstance(dest, basestring): |
| dest = Dest(dest) |
| else: |
| assert isinstance(dest, Dest) |
| |
| if not self.required: |
| return |
| |
| if not dest.exists(): |
| errors.fatal("Required existing file doesn't exist: %s" % |
| dest.path) |
| |
| |
| class PreprocessedFile(BaseFile): |
| ''' |
| File class for a file that is preprocessed. PreprocessedFile.copy() runs |
| the preprocessor on the file to create the output. |
| ''' |
| def __init__(self, path, depfile_path, marker, defines, extra_depends=None, |
| silence_missing_directive_warnings=False): |
| self.path = path |
| self.depfile = depfile_path |
| self.marker = marker |
| self.defines = defines |
| self.extra_depends = list(extra_depends or []) |
| self.silence_missing_directive_warnings = \ |
| silence_missing_directive_warnings |
| |
| def copy(self, dest, skip_if_older=True): |
| ''' |
| Invokes the preprocessor to create the destination file. |
| ''' |
| if isinstance(dest, basestring): |
| dest = Dest(dest) |
| else: |
| assert isinstance(dest, Dest) |
| |
| # We have to account for the case where the destination exists and is a |
| # symlink to something. Since we know the preprocessor is certainly not |
| # going to create a symlink, we can just remove the existing one. If the |
| # destination is not a symlink, we leave it alone, since we're going to |
| # overwrite its contents anyway. |
| # If symlinks aren't supported at all, we can skip this step. |
| if hasattr(os, 'symlink'): |
| if os.path.islink(dest.path): |
| os.remove(dest.path) |
| |
| pp_deps = set(self.extra_depends) |
| |
| # If a dependency file was specified, and it exists, add any |
| # dependencies from that file to our list. |
| if self.depfile and os.path.exists(self.depfile): |
| target = mozpath.normpath(dest.name) |
| with open(self.depfile, 'rb') as fileobj: |
| for rule in makeutil.read_dep_makefile(fileobj): |
| if target in rule.targets(): |
| pp_deps.update(rule.dependencies()) |
| |
| skip = False |
| if dest.exists() and skip_if_older: |
| # If a dependency file was specified, and it doesn't exist, |
| # assume that the preprocessor needs to be rerun. That will |
| # regenerate the dependency file. |
| if self.depfile and not os.path.exists(self.depfile): |
| skip = False |
| else: |
| skip = not BaseFile.any_newer(dest.path, pp_deps) |
| |
| if skip: |
| return False |
| |
| deps_out = None |
| if self.depfile: |
| deps_out = FileAvoidWrite(self.depfile) |
| pp = Preprocessor(defines=self.defines, marker=self.marker) |
| pp.setSilenceDirectiveWarnings(self.silence_missing_directive_warnings) |
| |
| with open(self.path, 'rU') as input: |
| pp.processFile(input=input, output=dest, depfile=deps_out) |
| |
| dest.close() |
| if self.depfile: |
| deps_out.close() |
| |
| return True |
| |
| |
| class GeneratedFile(BaseFile): |
| ''' |
| File class for content with no previous existence on the filesystem. |
| ''' |
| def __init__(self, content): |
| self.content = content |
| |
| def open(self): |
| return BytesIO(self.content) |
| |
| |
| class DeflatedFile(BaseFile): |
| ''' |
| File class for members of a jar archive. DeflatedFile.copy() effectively |
| extracts the file from the jar archive. |
| ''' |
| def __init__(self, file): |
| from mozpack.mozjar import JarFileReader |
| assert isinstance(file, JarFileReader) |
| self.file = file |
| |
| def open(self): |
| self.file.seek(0) |
| return self.file |
| |
| |
| class XPTFile(GeneratedFile): |
| ''' |
| File class for a linked XPT file. It takes several XPT files as input |
| (using the add() and remove() member functions), and links them at copy() |
| time. |
| ''' |
| def __init__(self): |
| self._files = set() |
| |
| def add(self, xpt): |
| ''' |
| Add the given XPT file (as a BaseFile instance) to the list of XPTs |
| to link. |
| ''' |
| assert isinstance(xpt, BaseFile) |
| self._files.add(xpt) |
| |
| def remove(self, xpt): |
| ''' |
| Remove the given XPT file (as a BaseFile instance) from the list of |
| XPTs to link. |
| ''' |
| assert isinstance(xpt, BaseFile) |
| self._files.remove(xpt) |
| |
| def copy(self, dest, skip_if_older=True): |
| ''' |
| Link the registered XPTs and place the resulting linked XPT at the |
| destination given as a string or a Dest instance. Avoids an expensive |
| XPT linking if the interfaces in an existing destination match those of |
| the individual XPTs to link. |
| skip_if_older is ignored. |
| ''' |
| if isinstance(dest, basestring): |
| dest = Dest(dest) |
| assert isinstance(dest, Dest) |
| |
| from xpt import xpt_link, Typelib, Interface |
| all_typelibs = [Typelib.read(f.open()) for f in self._files] |
| if dest.exists(): |
| # Typelib.read() needs to seek(), so use a BytesIO for dest |
| # content. |
| dest_interfaces = \ |
| dict((i.name, i) |
| for i in Typelib.read(BytesIO(dest.read())).interfaces |
| if i.iid != Interface.UNRESOLVED_IID) |
| identical = True |
| for f in self._files: |
| typelib = Typelib.read(f.open()) |
| for i in typelib.interfaces: |
| if i.iid != Interface.UNRESOLVED_IID and \ |
| not (i.name in dest_interfaces and |
| i == dest_interfaces[i.name]): |
| identical = False |
| break |
| if identical: |
| return False |
| s = BytesIO() |
| xpt_link(all_typelibs).write(s) |
| dest.write(s.getvalue()) |
| return True |
| |
| def open(self): |
| raise RuntimeError("Unsupported") |
| |
| def isempty(self): |
| ''' |
| Return whether there are XPT files to link. |
| ''' |
| return len(self._files) == 0 |
| |
| |
| class ManifestFile(BaseFile): |
| ''' |
| File class for a manifest file. It takes individual manifest entries (using |
| the add() and remove() member functions), and adjusts them to be relative |
| to the base path for the manifest, given at creation. |
| Example: |
| There is a manifest entry "content webapprt webapprt/content/" relative |
| to "webapprt/chrome". When packaging, the entry will be stored in |
| jar:webapprt/omni.ja!/chrome/chrome.manifest, which means the entry |
| will have to be relative to "chrome" instead of "webapprt/chrome". This |
| doesn't really matter when serializing the entry, since this base path |
| is not written out, but it matters when moving the entry at the same |
| time, e.g. to jar:webapprt/omni.ja!/chrome.manifest, which we don't do |
| currently but could in the future. |
| ''' |
| def __init__(self, base, entries=None): |
| self._entries = entries if entries else [] |
| self._base = base |
| |
| def add(self, entry): |
| ''' |
| Add the given entry to the manifest. Entries are rebased at open() time |
| instead of add() time so that they can be more easily remove()d. |
| ''' |
| assert isinstance(entry, ManifestEntry) |
| self._entries.append(entry) |
| |
| def remove(self, entry): |
| ''' |
| Remove the given entry from the manifest. |
| ''' |
| assert isinstance(entry, ManifestEntry) |
| self._entries.remove(entry) |
| |
| def open(self): |
| ''' |
| Return a file-like object allowing to read() the serialized content of |
| the manifest. |
| ''' |
| return BytesIO(''.join('%s\n' % e.rebase(self._base) |
| for e in self._entries)) |
| |
| def __iter__(self): |
| ''' |
| Iterate over entries in the manifest file. |
| ''' |
| return iter(self._entries) |
| |
| def isempty(self): |
| ''' |
| Return whether there are manifest entries to write |
| ''' |
| return len(self._entries) == 0 |
| |
| |
| class MinifiedProperties(BaseFile): |
| ''' |
| File class for minified properties. This wraps around a BaseFile instance, |
| and removes lines starting with a # from its content. |
| ''' |
| def __init__(self, file): |
| assert isinstance(file, BaseFile) |
| self._file = file |
| |
| def open(self): |
| ''' |
| Return a file-like object allowing to read() the minified content of |
| the properties file. |
| ''' |
| return BytesIO(''.join(l for l in self._file.open().readlines() |
| if not l.startswith('#'))) |
| |
| |
| class MinifiedJavaScript(BaseFile): |
| ''' |
| File class for minifying JavaScript files. |
| ''' |
| def __init__(self, file, verify_command=None): |
| assert isinstance(file, BaseFile) |
| self._file = file |
| self._verify_command = verify_command |
| |
| def open(self): |
| output = BytesIO() |
| minify = JavascriptMinify(self._file.open(), output, quote_chars="'\"`") |
| minify.minify() |
| output.seek(0) |
| |
| if not self._verify_command: |
| return output |
| |
| input_source = self._file.open().read() |
| output_source = output.getvalue() |
| |
| with NamedTemporaryFile() as fh1, NamedTemporaryFile() as fh2: |
| fh1.write(input_source) |
| fh2.write(output_source) |
| fh1.flush() |
| fh2.flush() |
| |
| try: |
| args = list(self._verify_command) |
| args.extend([fh1.name, fh2.name]) |
| subprocess.check_output(args, stderr=subprocess.STDOUT) |
| except subprocess.CalledProcessError as e: |
| errors.warn('JS minification verification failed for %s:' % |
| (getattr(self._file, 'path', '<unknown>'))) |
| # Prefix each line with "Warning:" so mozharness doesn't |
| # think these error messages are real errors. |
| for line in e.output.splitlines(): |
| errors.warn(line) |
| |
| return self._file.open() |
| |
| return output |
| |
| |
| class BaseFinder(object): |
| def __init__(self, base, minify=False, minify_js=False, |
| minify_js_verify_command=None): |
| ''' |
| Initializes the instance with a reference base directory. |
| |
| The optional minify argument specifies whether minification of code |
| should occur. minify_js is an additional option to control minification |
| of JavaScript. It requires minify to be True. |
| |
| minify_js_verify_command can be used to optionally verify the results |
| of JavaScript minification. If defined, it is expected to be an iterable |
| that will constitute the first arguments to a called process which will |
| receive the filenames of the original and minified JavaScript files. |
| The invoked process can then verify the results. If minification is |
| rejected, the process exits with a non-0 exit code and the original |
| JavaScript source is used. An example value for this argument is |
| ('/path/to/js', '/path/to/verify/script.js'). |
| ''' |
| if minify_js and not minify: |
| raise ValueError('minify_js requires minify.') |
| |
| self.base = base |
| self._minify = minify |
| self._minify_js = minify_js |
| self._minify_js_verify_command = minify_js_verify_command |
| |
| def find(self, pattern): |
| ''' |
| Yield path, BaseFile_instance pairs for all files under the base |
| directory and its subdirectories that match the given pattern. See the |
| mozpack.path.match documentation for a description of the handled |
| patterns. |
| ''' |
| while pattern.startswith('/'): |
| pattern = pattern[1:] |
| for p, f in self._find(pattern): |
| yield p, self._minify_file(p, f) |
| |
| def get(self, path): |
| """Obtain a single file. |
| |
| Where ``find`` is tailored towards matching multiple files, this method |
| is used for retrieving a single file. Use this method when performance |
| is critical. |
| |
| Returns a ``BaseFile`` if at most one file exists or ``None`` otherwise. |
| """ |
| files = list(self.find(path)) |
| if len(files) != 1: |
| return None |
| return files[0][1] |
| |
| def __iter__(self): |
| ''' |
| Iterates over all files under the base directory (excluding files |
| starting with a '.' and files at any level under a directory starting |
| with a '.'). |
| for path, file in finder: |
| ... |
| ''' |
| return self.find('') |
| |
| def __contains__(self, pattern): |
| raise RuntimeError("'in' operator forbidden for %s. Use contains()." % |
| self.__class__.__name__) |
| |
| def contains(self, pattern): |
| ''' |
| Return whether some files under the base directory match the given |
| pattern. See the mozpack.path.match documentation for a description of |
| the handled patterns. |
| ''' |
| return any(self.find(pattern)) |
| |
| def _minify_file(self, path, file): |
| ''' |
| Return an appropriate MinifiedSomething wrapper for the given BaseFile |
| instance (file), according to the file type (determined by the given |
| path), if the FileFinder was created with minification enabled. |
| Otherwise, just return the given BaseFile instance. |
| ''' |
| if not self._minify or isinstance(file, ExecutableFile): |
| return file |
| |
| if path.endswith('.properties'): |
| return MinifiedProperties(file) |
| |
| if self._minify_js and path.endswith(('.js', '.jsm')): |
| return MinifiedJavaScript(file, self._minify_js_verify_command) |
| |
| return file |
| |
| def _find_helper(self, pattern, files, file_getter): |
| """Generic implementation of _find. |
| |
| A few *Finder implementations share logic for returning results. |
| This function implements the custom logic. |
| |
| The ``file_getter`` argument is a callable that receives a path |
| that is known to exist. The callable should return a ``BaseFile`` |
| instance. |
| """ |
| if '*' in pattern: |
| for p in files: |
| if mozpath.match(p, pattern): |
| yield p, file_getter(p) |
| elif pattern == '': |
| for p in files: |
| yield p, file_getter(p) |
| elif pattern in files: |
| yield pattern, file_getter(pattern) |
| else: |
| for p in files: |
| if mozpath.basedir(p, [pattern]) == pattern: |
| yield p, file_getter(p) |
| |
| |
| class FileFinder(BaseFinder): |
| ''' |
| Helper to get appropriate BaseFile instances from the file system. |
| ''' |
| def __init__(self, base, find_executables=True, ignore=(), |
| find_dotfiles=False, **kargs): |
| ''' |
| Create a FileFinder for files under the given base directory. |
| |
| The find_executables argument determines whether the finder needs to |
| try to guess whether files are executables. Disabling this guessing |
| when not necessary can speed up the finder significantly. |
| |
| ``ignore`` accepts an iterable of patterns to ignore. Entries are |
| strings that match paths relative to ``base`` using |
| ``mozpath.match()``. This means if an entry corresponds |
| to a directory, all files under that directory will be ignored. If |
| an entry corresponds to a file, that particular file will be ignored. |
| ''' |
| BaseFinder.__init__(self, base, **kargs) |
| self.find_dotfiles = find_dotfiles |
| self.find_executables = find_executables |
| self.ignore = ignore |
| |
| def _find(self, pattern): |
| ''' |
| Actual implementation of FileFinder.find(), dispatching to specialized |
| member functions depending on what kind of pattern was given. |
| Note all files with a name starting with a '.' are ignored when |
| scanning directories, but are not ignored when explicitely requested. |
| ''' |
| if '*' in pattern: |
| return self._find_glob('', mozpath.split(pattern)) |
| elif os.path.isdir(os.path.join(self.base, pattern)): |
| return self._find_dir(pattern) |
| else: |
| f = self.get(pattern) |
| return ((pattern, f),) if f else () |
| |
| def _find_dir(self, path): |
| ''' |
| Actual implementation of FileFinder.find() when the given pattern |
| corresponds to an existing directory under the base directory. |
| Ignores file names starting with a '.' under the given path. If the |
| path itself has leafs starting with a '.', they are not ignored. |
| ''' |
| for p in self.ignore: |
| if mozpath.match(path, p): |
| return |
| |
| # The sorted makes the output idempotent. Otherwise, we are |
| # likely dependent on filesystem implementation details, such as |
| # inode ordering. |
| for p in sorted(os.listdir(os.path.join(self.base, path))): |
| if p.startswith('.'): |
| if p in ('.', '..'): |
| continue |
| if not self.find_dotfiles: |
| continue |
| for p_, f in self._find(mozpath.join(path, p)): |
| yield p_, f |
| |
| def get(self, path): |
| srcpath = os.path.join(self.base, path) |
| if not os.path.exists(srcpath): |
| return None |
| |
| for p in self.ignore: |
| if mozpath.match(path, p): |
| return None |
| |
| if self.find_executables and is_executable(srcpath): |
| return ExecutableFile(srcpath) |
| else: |
| return File(srcpath) |
| |
| def _find_glob(self, base, pattern): |
| ''' |
| Actual implementation of FileFinder.find() when the given pattern |
| contains globbing patterns ('*' or '**'). This is meant to be an |
| equivalent of: |
| for p, f in self: |
| if mozpath.match(p, pattern): |
| yield p, f |
| but avoids scanning the entire tree. |
| ''' |
| if not pattern: |
| for p, f in self._find(base): |
| yield p, f |
| elif pattern[0] == '**': |
| for p, f in self._find(base): |
| if mozpath.match(p, mozpath.join(*pattern)): |
| yield p, f |
| elif '*' in pattern[0]: |
| if not os.path.exists(os.path.join(self.base, base)): |
| return |
| |
| for p in self.ignore: |
| if mozpath.match(base, p): |
| return |
| |
| # See above comment w.r.t. sorted() and idempotent behavior. |
| for p in sorted(os.listdir(os.path.join(self.base, base))): |
| if p.startswith('.') and not pattern[0].startswith('.'): |
| continue |
| if mozpath.match(p, pattern[0]): |
| for p_, f in self._find_glob(mozpath.join(base, p), |
| pattern[1:]): |
| yield p_, f |
| else: |
| for p, f in self._find_glob(mozpath.join(base, pattern[0]), |
| pattern[1:]): |
| yield p, f |
| |
| |
| class JarFinder(BaseFinder): |
| ''' |
| Helper to get appropriate DeflatedFile instances from a JarReader. |
| ''' |
| def __init__(self, base, reader, **kargs): |
| ''' |
| Create a JarFinder for files in the given JarReader. The base argument |
| is used as an indication of the Jar file location. |
| ''' |
| assert isinstance(reader, JarReader) |
| BaseFinder.__init__(self, base, **kargs) |
| self._files = OrderedDict((f.filename, f) for f in reader) |
| |
| def _find(self, pattern): |
| ''' |
| Actual implementation of JarFinder.find(), dispatching to specialized |
| member functions depending on what kind of pattern was given. |
| ''' |
| return self._find_helper(pattern, self._files, |
| lambda x: DeflatedFile(self._files[x])) |
| |
| |
| class ComposedFinder(BaseFinder): |
| ''' |
| Composes multiple File Finders in some sort of virtual file system. |
| |
| A ComposedFinder is initialized from a dictionary associating paths to |
| *Finder instances. |
| |
| Note this could be optimized to be smarter than getting all the files |
| in advance. |
| ''' |
| def __init__(self, finders): |
| # Can't import globally, because of the dependency of mozpack.copier |
| # on this module. |
| from mozpack.copier import FileRegistry |
| self.files = FileRegistry() |
| |
| for base, finder in sorted(finders.iteritems()): |
| if self.files.contains(base): |
| self.files.remove(base) |
| for p, f in finder.find(''): |
| self.files.add(mozpath.join(base, p), f) |
| |
| def find(self, pattern): |
| for p in self.files.match(pattern): |
| yield p, self.files[p] |
| |
| |
| class MercurialFile(BaseFile): |
| """File class for holding data from Mercurial.""" |
| def __init__(self, client, rev, path): |
| self._content = client.cat([path], rev=rev) |
| |
| def read(self): |
| return self._content |
| |
| |
| class MercurialRevisionFinder(BaseFinder): |
| """A finder that operates on a specific Mercurial revision.""" |
| |
| def __init__(self, repo, rev='.', recognize_repo_paths=False, **kwargs): |
| """Create a finder attached to a specific revision in a repository. |
| |
| If no revision is given, open the parent of the working directory. |
| |
| ``recognize_repo_paths`` will enable a mode where ``.get()`` will |
| recognize full paths that include the repo's path. Typically Finder |
| instances are "bound" to a base directory and paths are relative to |
| that directory. This mode changes that. When this mode is activated, |
| ``.find()`` will not work! This mode exists to support the moz.build |
| reader, which uses absolute paths instead of relative paths. The reader |
| should eventually be rewritten to use relative paths and this hack |
| should be removed (TODO bug 1171069). |
| """ |
| if not hglib: |
| raise Exception('hglib package not found') |
| |
| super(MercurialRevisionFinder, self).__init__(base=repo, **kwargs) |
| |
| self._root = mozpath.normpath(repo).rstrip('/') |
| self._recognize_repo_paths = recognize_repo_paths |
| |
| # We change directories here otherwise we have to deal with relative |
| # paths. |
| oldcwd = os.getcwd() |
| os.chdir(self._root) |
| try: |
| self._client = hglib.open(path=repo, encoding=b'utf-8') |
| finally: |
| os.chdir(oldcwd) |
| self._rev = rev if rev is not None else b'.' |
| self._files = OrderedDict() |
| |
| # Immediately populate the list of files in the repo since nearly every |
| # operation requires this list. |
| out = self._client.rawcommand([b'files', b'--rev', str(self._rev)]) |
| for relpath in out.splitlines(): |
| self._files[relpath] = None |
| |
| def _find(self, pattern): |
| if self._recognize_repo_paths: |
| raise NotImplementedError('cannot use find with recognize_repo_path') |
| |
| return self._find_helper(pattern, self._files, self._get) |
| |
| def get(self, path): |
| if self._recognize_repo_paths: |
| if not path.startswith(self._root): |
| raise ValueError('lookups in recognize_repo_paths mode must be ' |
| 'prefixed with repo path: %s' % path) |
| path = path[len(self._root) + 1:] |
| |
| try: |
| return self._get(path) |
| except KeyError: |
| return None |
| |
| def _get(self, path): |
| # We lazy populate self._files because potentially creating tens of |
| # thousands of MercurialFile instances for every file in the repo is |
| # inefficient. |
| f = self._files[path] |
| if not f: |
| f = MercurialFile(self._client, self._rev, path) |
| self._files[path] = f |
| |
| return f |