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)