| # 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/. |
| |
| import os |
| import re |
| import shutil |
| 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 |
| from mozpack.mozjar import JarReader |
| import mozpack.path |
| from collections import OrderedDict |
| |
| |
| 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 |
| |
| 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. |
| ''' |
| 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): |
| # 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. |
| if skip_if_older and int(os.path.getmtime(self.path) * 1000) \ |
| <= int(os.path.getmtime(dest.path) * 1000): |
| 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): |
| shutil.copy2(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') |
| |
| |
| class File(BaseFile): |
| ''' |
| File class for plain files. |
| ''' |
| def __init__(self, path): |
| self.path = path |
| |
| |
| 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): |
| 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 |
| 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 BaseFinder(object): |
| def __init__(self, base, minify=False): |
| ''' |
| Initializes the instance with a reference base directory. The |
| optional minify argument specifies whether file types supporting |
| minification (currently only "*.properties") should be minified. |
| ''' |
| self.base = base |
| self._minify = minify |
| |
| 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 __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. |
| Currently, only "*.properties" files are handled. |
| ''' |
| if self._minify and not isinstance(file, ExecutableFile): |
| if path.endswith('.properties'): |
| return MinifiedProperties(file) |
| return file |
| |
| |
| class FileFinder(BaseFinder): |
| ''' |
| Helper to get appropriate BaseFile instances from the file system. |
| ''' |
| def __init__(self, base, **kargs): |
| ''' |
| Create a FileFinder for files under the given base directory. |
| ''' |
| BaseFinder.__init__(self, base, **kargs) |
| |
| 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('', mozpack.path.split(pattern)) |
| elif os.path.isdir(os.path.join(self.base, pattern)): |
| return self._find_dir(pattern) |
| else: |
| return self._find_file(pattern) |
| |
| 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 os.listdir(os.path.join(self.base, path)): |
| if p.startswith('.'): |
| continue |
| for p_, f in self._find(mozpack.path.join(path, p)): |
| yield p_, f |
| |
| def _find_file(self, path): |
| ''' |
| Actual implementation of FileFinder.find() when the given pattern |
| corresponds to an existing file under the base directory. |
| ''' |
| srcpath = os.path.join(self.base, path) |
| if not os.path.exists(srcpath): |
| return |
| |
| if is_executable(srcpath): |
| yield path, ExecutableFile(srcpath) |
| else: |
| yield path, 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 mozpack.path.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 mozpack.path.match(p, mozpack.path.join(*pattern)): |
| yield p, f |
| elif '*' in pattern[0]: |
| if not os.path.exists(os.path.join(self.base, base)): |
| return |
| for p in os.listdir(os.path.join(self.base, base)): |
| if p.startswith('.') and not pattern[0].startswith('.'): |
| continue |
| if re.match(mozpack.path.translate(pattern[0]), p): |
| for p_, f in self._find_glob(mozpack.path.join(base, p), |
| pattern[1:]): |
| yield p_, f |
| else: |
| for p, f in self._find_glob(mozpack.path.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. |
| ''' |
| if '*' in pattern: |
| for p in self._files: |
| if mozpack.path.match(p, pattern): |
| yield p, DeflatedFile(self._files[p]) |
| elif pattern == '': |
| for p in self._files: |
| yield p, DeflatedFile(self._files[p]) |
| elif pattern in self._files: |
| yield pattern, DeflatedFile(self._files[pattern]) |
| else: |
| for p in self._files: |
| if mozpack.path.basedir(p, [pattern]) == pattern: |
| yield p, DeflatedFile(self._files[p]) |