| # 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, unicode_literals |
| |
| from contextlib import contextmanager |
| import json |
| |
| from .files import ( |
| AbsoluteSymlinkFile, |
| ExistingFile, |
| File, |
| FileFinder, |
| PreprocessedFile, |
| ) |
| import mozpack.path as mozpath |
| |
| |
| # This probably belongs in a more generic module. Where? |
| @contextmanager |
| def _auto_fileobj(path, fileobj, mode='r'): |
| if path and fileobj: |
| raise AssertionError('Only 1 of path or fileobj may be defined.') |
| |
| if not path and not fileobj: |
| raise AssertionError('Must specified 1 of path or fileobj.') |
| |
| if path: |
| fileobj = open(path, mode) |
| |
| try: |
| yield fileobj |
| finally: |
| if path: |
| fileobj.close() |
| |
| |
| class UnreadableInstallManifest(Exception): |
| """Raised when an invalid install manifest is parsed.""" |
| |
| |
| class InstallManifest(object): |
| """Describes actions to be used with a copier.FileCopier instance. |
| |
| This class facilitates serialization and deserialization of data used to |
| construct a copier.FileCopier and to perform copy operations. |
| |
| The manifest defines source paths, destination paths, and a mechanism by |
| which the destination file should come into existence. |
| |
| Entries in the manifest correspond to the following types: |
| |
| copy -- The file specified as the source path will be copied to the |
| destination path. |
| |
| symlink -- The destination path will be a symlink to the source path. |
| If symlinks are not supported, a copy will be performed. |
| |
| exists -- The destination path is accounted for and won't be deleted by |
| the FileCopier. If the destination path doesn't exist, an error is |
| raised. |
| |
| optional -- The destination path is accounted for and won't be deleted by |
| the FileCopier. No error is raised if the destination path does not |
| exist. |
| |
| patternsymlink -- Paths matched by the expression in the source path |
| will be symlinked to the destination directory. |
| |
| patterncopy -- Similar to patternsymlink except files are copied, not |
| symlinked. |
| |
| preprocess -- The file specified at the source path will be run through |
| the preprocessor, and the output will be written to the destination |
| path. |
| |
| Version 1 of the manifest was the initial version. |
| Version 2 added optional path support |
| Version 3 added support for pattern entries. |
| Version 4 added preprocessed file support. |
| """ |
| |
| CURRENT_VERSION = 4 |
| |
| FIELD_SEPARATOR = '\x1f' |
| |
| # Negative values are reserved for non-actionable items, that is, metadata |
| # that doesn't describe files in the destination. |
| SYMLINK = 1 |
| COPY = 2 |
| REQUIRED_EXISTS = 3 |
| OPTIONAL_EXISTS = 4 |
| PATTERN_SYMLINK = 5 |
| PATTERN_COPY = 6 |
| PREPROCESS = 7 |
| |
| def __init__(self, path=None, fileobj=None): |
| """Create a new InstallManifest entry. |
| |
| If path is defined, the manifest will be populated with data from the |
| file path. |
| |
| If fileobj is defined, the manifest will be populated with data read |
| from the specified file object. |
| |
| Both path and fileobj cannot be defined. |
| """ |
| self._dests = {} |
| self._source_files = set() |
| |
| if path or fileobj: |
| with _auto_fileobj(path, fileobj, 'rb') as fh: |
| self._source_files.add(fh.name) |
| self._load_from_fileobj(fh) |
| |
| def _load_from_fileobj(self, fileobj): |
| version = fileobj.readline().rstrip() |
| if version not in ('1', '2', '3', '4'): |
| raise UnreadableInstallManifest('Unknown manifest version: %s' % |
| version) |
| |
| for line in fileobj: |
| line = line.rstrip() |
| |
| fields = line.split(self.FIELD_SEPARATOR) |
| |
| record_type = int(fields[0]) |
| |
| if record_type == self.SYMLINK: |
| dest, source = fields[1:] |
| self.add_symlink(source, dest) |
| continue |
| |
| if record_type == self.COPY: |
| dest, source = fields[1:] |
| self.add_copy(source, dest) |
| continue |
| |
| if record_type == self.REQUIRED_EXISTS: |
| _, path = fields |
| self.add_required_exists(path) |
| continue |
| |
| if record_type == self.OPTIONAL_EXISTS: |
| _, path = fields |
| self.add_optional_exists(path) |
| continue |
| |
| if record_type == self.PATTERN_SYMLINK: |
| _, base, pattern, dest = fields[1:] |
| self.add_pattern_symlink(base, pattern, dest) |
| continue |
| |
| if record_type == self.PATTERN_COPY: |
| _, base, pattern, dest = fields[1:] |
| self.add_pattern_copy(base, pattern, dest) |
| continue |
| |
| if record_type == self.PREPROCESS: |
| dest, source, deps, marker, defines, warnings = fields[1:] |
| |
| self.add_preprocess(source, dest, deps, marker, |
| self._decode_field_entry(defines), |
| silence_missing_directive_warnings=bool(int(warnings))) |
| continue |
| |
| # Don't fail for non-actionable items, allowing |
| # forward-compatibility with those we will add in the future. |
| if record_type >= 0: |
| raise UnreadableInstallManifest('Unknown record type: %d' % |
| record_type) |
| |
| def __len__(self): |
| return len(self._dests) |
| |
| def __contains__(self, item): |
| return item in self._dests |
| |
| def __eq__(self, other): |
| return isinstance(other, InstallManifest) and self._dests == other._dests |
| |
| def __neq__(self, other): |
| return not self.__eq__(other) |
| |
| def __ior__(self, other): |
| if not isinstance(other, InstallManifest): |
| raise ValueError('Can only | with another instance of InstallManifest.') |
| |
| # We must copy source files to ourselves so extra dependencies from |
| # the preprocessor are taken into account. Ideally, we would track |
| # which source file each entry came from. However, this is more |
| # complicated and not yet implemented. The current implementation |
| # will result in over invalidation, possibly leading to performance |
| # loss. |
| self._source_files |= other._source_files |
| |
| for dest in sorted(other._dests): |
| self._add_entry(dest, other._dests[dest]) |
| |
| return self |
| |
| def _encode_field_entry(self, data): |
| """Converts an object into a format that can be stored in the manifest file. |
| |
| Complex data types, such as ``dict``, need to be converted into a text |
| representation before they can be written to a file. |
| """ |
| return json.dumps(data, sort_keys=True) |
| |
| def _decode_field_entry(self, data): |
| """Restores an object from a format that can be stored in the manifest file. |
| |
| Complex data types, such as ``dict``, need to be converted into a text |
| representation before they can be written to a file. |
| """ |
| return json.loads(data) |
| |
| def write(self, path=None, fileobj=None): |
| """Serialize this manifest to a file or file object. |
| |
| If path is specified, that file will be written to. If fileobj is specified, |
| the serialized content will be written to that file object. |
| |
| It is an error if both are specified. |
| """ |
| with _auto_fileobj(path, fileobj, 'wb') as fh: |
| fh.write('%d\n' % self.CURRENT_VERSION) |
| |
| for dest in sorted(self._dests): |
| entry = self._dests[dest] |
| |
| parts = ['%d' % entry[0], dest] |
| parts.extend(entry[1:]) |
| fh.write('%s\n' % self.FIELD_SEPARATOR.join( |
| p.encode('utf-8') for p in parts)) |
| |
| def add_symlink(self, source, dest): |
| """Add a symlink to this manifest. |
| |
| dest will be a symlink to source. |
| """ |
| self._add_entry(dest, (self.SYMLINK, source)) |
| |
| def add_copy(self, source, dest): |
| """Add a copy to this manifest. |
| |
| source will be copied to dest. |
| """ |
| self._add_entry(dest, (self.COPY, source)) |
| |
| def add_required_exists(self, dest): |
| """Record that a destination file must exist. |
| |
| This effectively prevents the listed file from being deleted. |
| """ |
| self._add_entry(dest, (self.REQUIRED_EXISTS,)) |
| |
| def add_optional_exists(self, dest): |
| """Record that a destination file may exist. |
| |
| This effectively prevents the listed file from being deleted. Unlike a |
| "required exists" file, files of this type do not raise errors if the |
| destination file does not exist. |
| """ |
| self._add_entry(dest, (self.OPTIONAL_EXISTS,)) |
| |
| def add_pattern_symlink(self, base, pattern, dest): |
| """Add a pattern match that results in symlinks being created. |
| |
| A ``FileFinder`` will be created with its base set to ``base`` |
| and ``FileFinder.find()`` will be called with ``pattern`` to discover |
| source files. Each source file will be symlinked under ``dest``. |
| |
| Filenames under ``dest`` are constructed by taking the path fragment |
| after ``base`` and concatenating it with ``dest``. e.g. |
| |
| <base>/foo/bar.h -> <dest>/foo/bar.h |
| """ |
| self._add_entry(mozpath.join(base, pattern, dest), |
| (self.PATTERN_SYMLINK, base, pattern, dest)) |
| |
| def add_pattern_copy(self, base, pattern, dest): |
| """Add a pattern match that results in copies. |
| |
| See ``add_pattern_symlink()`` for usage. |
| """ |
| self._add_entry(mozpath.join(base, pattern, dest), |
| (self.PATTERN_COPY, base, pattern, dest)) |
| |
| def add_preprocess(self, source, dest, deps, marker='#', defines={}, |
| silence_missing_directive_warnings=False): |
| """Add a preprocessed file to this manifest. |
| |
| ``source`` will be passed through preprocessor.py, and the output will be |
| written to ``dest``. |
| """ |
| self._add_entry(dest, ( |
| self.PREPROCESS, |
| source, |
| deps, |
| marker, |
| self._encode_field_entry(defines), |
| '1' if silence_missing_directive_warnings else '0', |
| )) |
| |
| def _add_entry(self, dest, entry): |
| if dest in self._dests: |
| raise ValueError('Item already in manifest: %s' % dest) |
| |
| self._dests[dest] = entry |
| |
| def populate_registry(self, registry, defines_override={}): |
| """Populate a mozpack.copier.FileRegistry instance with data from us. |
| |
| The caller supplied a FileRegistry instance (or at least something that |
| conforms to its interface) and that instance is populated with data |
| from this manifest. |
| |
| Defines can be given to override the ones in the manifest for |
| preprocessing. |
| """ |
| for dest in sorted(self._dests): |
| entry = self._dests[dest] |
| install_type = entry[0] |
| |
| if install_type == self.SYMLINK: |
| registry.add(dest, AbsoluteSymlinkFile(entry[1])) |
| continue |
| |
| if install_type == self.COPY: |
| registry.add(dest, File(entry[1])) |
| continue |
| |
| if install_type == self.REQUIRED_EXISTS: |
| registry.add(dest, ExistingFile(required=True)) |
| continue |
| |
| if install_type == self.OPTIONAL_EXISTS: |
| registry.add(dest, ExistingFile(required=False)) |
| continue |
| |
| if install_type in (self.PATTERN_SYMLINK, self.PATTERN_COPY): |
| _, base, pattern, dest = entry |
| finder = FileFinder(base, find_executables=False) |
| paths = [f[0] for f in finder.find(pattern)] |
| |
| if install_type == self.PATTERN_SYMLINK: |
| cls = AbsoluteSymlinkFile |
| else: |
| cls = File |
| |
| for path in paths: |
| source = mozpath.join(base, path) |
| registry.add(mozpath.join(dest, path), cls(source)) |
| |
| continue |
| |
| if install_type == self.PREPROCESS: |
| defines = self._decode_field_entry(entry[4]) |
| if defines_override: |
| defines.update(defines_override) |
| registry.add(dest, PreprocessedFile(entry[1], |
| depfile_path=entry[2], |
| marker=entry[3], |
| defines=defines, |
| extra_depends=self._source_files, |
| silence_missing_directive_warnings=bool(int(entry[5])))) |
| |
| continue |
| |
| raise Exception('Unknown install type defined in manifest: %d' % |
| install_type) |