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