python/mozbuild/mozpack/manifests.py

Thu, 15 Jan 2015 15:55:04 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 15 Jan 2015 15:55:04 +0100
branch
TOR_BUG_9701
changeset 9
a63d609f5ebe
permissions
-rw-r--r--

Back out 97036ab72558 which inappropriately compared turds to third parties.

michael@0 1 # This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 # License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
michael@0 4
michael@0 5 from __future__ import unicode_literals
michael@0 6
michael@0 7 from contextlib import contextmanager
michael@0 8 import json
michael@0 9
michael@0 10 from .files import (
michael@0 11 AbsoluteSymlinkFile,
michael@0 12 ExistingFile,
michael@0 13 File,
michael@0 14 FileFinder,
michael@0 15 PreprocessedFile,
michael@0 16 )
michael@0 17 import mozpack.path as mozpath
michael@0 18
michael@0 19
michael@0 20 # This probably belongs in a more generic module. Where?
michael@0 21 @contextmanager
michael@0 22 def _auto_fileobj(path, fileobj, mode='r'):
michael@0 23 if path and fileobj:
michael@0 24 raise AssertionError('Only 1 of path or fileobj may be defined.')
michael@0 25
michael@0 26 if not path and not fileobj:
michael@0 27 raise AssertionError('Must specified 1 of path or fileobj.')
michael@0 28
michael@0 29 if path:
michael@0 30 fileobj = open(path, mode)
michael@0 31
michael@0 32 try:
michael@0 33 yield fileobj
michael@0 34 finally:
michael@0 35 if path:
michael@0 36 fileobj.close()
michael@0 37
michael@0 38
michael@0 39 class UnreadableInstallManifest(Exception):
michael@0 40 """Raised when an invalid install manifest is parsed."""
michael@0 41
michael@0 42
michael@0 43 class InstallManifest(object):
michael@0 44 """Describes actions to be used with a copier.FileCopier instance.
michael@0 45
michael@0 46 This class facilitates serialization and deserialization of data used to
michael@0 47 construct a copier.FileCopier and to perform copy operations.
michael@0 48
michael@0 49 The manifest defines source paths, destination paths, and a mechanism by
michael@0 50 which the destination file should come into existence.
michael@0 51
michael@0 52 Entries in the manifest correspond to the following types:
michael@0 53
michael@0 54 copy -- The file specified as the source path will be copied to the
michael@0 55 destination path.
michael@0 56
michael@0 57 symlink -- The destination path will be a symlink to the source path.
michael@0 58 If symlinks are not supported, a copy will be performed.
michael@0 59
michael@0 60 exists -- The destination path is accounted for and won't be deleted by
michael@0 61 the FileCopier. If the destination path doesn't exist, an error is
michael@0 62 raised.
michael@0 63
michael@0 64 optional -- The destination path is accounted for and won't be deleted by
michael@0 65 the FileCopier. No error is raised if the destination path does not
michael@0 66 exist.
michael@0 67
michael@0 68 patternsymlink -- Paths matched by the expression in the source path
michael@0 69 will be symlinked to the destination directory.
michael@0 70
michael@0 71 patterncopy -- Similar to patternsymlink except files are copied, not
michael@0 72 symlinked.
michael@0 73
michael@0 74 preprocess -- The file specified at the source path will be run through
michael@0 75 the preprocessor, and the output will be written to the destination
michael@0 76 path.
michael@0 77
michael@0 78 Version 1 of the manifest was the initial version.
michael@0 79 Version 2 added optional path support
michael@0 80 Version 3 added support for pattern entries.
michael@0 81 Version 4 added preprocessed file support.
michael@0 82 """
michael@0 83
michael@0 84 CURRENT_VERSION = 4
michael@0 85
michael@0 86 FIELD_SEPARATOR = '\x1f'
michael@0 87
michael@0 88 SYMLINK = 1
michael@0 89 COPY = 2
michael@0 90 REQUIRED_EXISTS = 3
michael@0 91 OPTIONAL_EXISTS = 4
michael@0 92 PATTERN_SYMLINK = 5
michael@0 93 PATTERN_COPY = 6
michael@0 94 PREPROCESS = 7
michael@0 95
michael@0 96 def __init__(self, path=None, fileobj=None):
michael@0 97 """Create a new InstallManifest entry.
michael@0 98
michael@0 99 If path is defined, the manifest will be populated with data from the
michael@0 100 file path.
michael@0 101
michael@0 102 If fileobj is defined, the manifest will be populated with data read
michael@0 103 from the specified file object.
michael@0 104
michael@0 105 Both path and fileobj cannot be defined.
michael@0 106 """
michael@0 107 self._dests = {}
michael@0 108 self._source_file = None
michael@0 109
michael@0 110 if path or fileobj:
michael@0 111 with _auto_fileobj(path, fileobj, 'rb') as fh:
michael@0 112 self._source_file = fh.name
michael@0 113 self._load_from_fileobj(fh)
michael@0 114
michael@0 115 def _load_from_fileobj(self, fileobj):
michael@0 116 version = fileobj.readline().rstrip()
michael@0 117 if version not in ('1', '2', '3', '4'):
michael@0 118 raise UnreadableInstallManifest('Unknown manifest version: ' %
michael@0 119 version)
michael@0 120
michael@0 121 for line in fileobj:
michael@0 122 line = line.rstrip()
michael@0 123
michael@0 124 fields = line.split(self.FIELD_SEPARATOR)
michael@0 125
michael@0 126 record_type = int(fields[0])
michael@0 127
michael@0 128 if record_type == self.SYMLINK:
michael@0 129 dest, source = fields[1:]
michael@0 130 self.add_symlink(source, dest)
michael@0 131 continue
michael@0 132
michael@0 133 if record_type == self.COPY:
michael@0 134 dest, source = fields[1:]
michael@0 135 self.add_copy(source, dest)
michael@0 136 continue
michael@0 137
michael@0 138 if record_type == self.REQUIRED_EXISTS:
michael@0 139 _, path = fields
michael@0 140 self.add_required_exists(path)
michael@0 141 continue
michael@0 142
michael@0 143 if record_type == self.OPTIONAL_EXISTS:
michael@0 144 _, path = fields
michael@0 145 self.add_optional_exists(path)
michael@0 146 continue
michael@0 147
michael@0 148 if record_type == self.PATTERN_SYMLINK:
michael@0 149 _, base, pattern, dest = fields[1:]
michael@0 150 self.add_pattern_symlink(base, pattern, dest)
michael@0 151 continue
michael@0 152
michael@0 153 if record_type == self.PATTERN_COPY:
michael@0 154 _, base, pattern, dest = fields[1:]
michael@0 155 self.add_pattern_copy(base, pattern, dest)
michael@0 156 continue
michael@0 157
michael@0 158 if record_type == self.PREPROCESS:
michael@0 159 dest, source, deps, marker, defines = fields[1:]
michael@0 160 self.add_preprocess(source, dest, deps, marker,
michael@0 161 self._decode_field_entry(defines))
michael@0 162 continue
michael@0 163
michael@0 164 raise UnreadableInstallManifest('Unknown record type: %d' %
michael@0 165 record_type)
michael@0 166
michael@0 167 def __len__(self):
michael@0 168 return len(self._dests)
michael@0 169
michael@0 170 def __contains__(self, item):
michael@0 171 return item in self._dests
michael@0 172
michael@0 173 def __eq__(self, other):
michael@0 174 return isinstance(other, InstallManifest) and self._dests == other._dests
michael@0 175
michael@0 176 def __neq__(self, other):
michael@0 177 return not self.__eq__(other)
michael@0 178
michael@0 179 def __ior__(self, other):
michael@0 180 if not isinstance(other, InstallManifest):
michael@0 181 raise ValueError('Can only | with another instance of InstallManifest.')
michael@0 182
michael@0 183 for dest in sorted(other._dests):
michael@0 184 self._add_entry(dest, other._dests[dest])
michael@0 185
michael@0 186 return self
michael@0 187
michael@0 188 def _encode_field_entry(self, data):
michael@0 189 """Converts an object into a format that can be stored in the manifest file.
michael@0 190
michael@0 191 Complex data types, such as ``dict``, need to be converted into a text
michael@0 192 representation before they can be written to a file.
michael@0 193 """
michael@0 194 return json.dumps(data, sort_keys=True)
michael@0 195
michael@0 196 def _decode_field_entry(self, data):
michael@0 197 """Restores an object from a format that can be stored in the manifest file.
michael@0 198
michael@0 199 Complex data types, such as ``dict``, need to be converted into a text
michael@0 200 representation before they can be written to a file.
michael@0 201 """
michael@0 202 return json.loads(data)
michael@0 203
michael@0 204 def write(self, path=None, fileobj=None):
michael@0 205 """Serialize this manifest to a file or file object.
michael@0 206
michael@0 207 If path is specified, that file will be written to. If fileobj is specified,
michael@0 208 the serialized content will be written to that file object.
michael@0 209
michael@0 210 It is an error if both are specified.
michael@0 211 """
michael@0 212 with _auto_fileobj(path, fileobj, 'wb') as fh:
michael@0 213 fh.write('%d\n' % self.CURRENT_VERSION)
michael@0 214
michael@0 215 for dest in sorted(self._dests):
michael@0 216 entry = self._dests[dest]
michael@0 217
michael@0 218 parts = ['%d' % entry[0], dest]
michael@0 219 parts.extend(entry[1:])
michael@0 220 fh.write('%s\n' % self.FIELD_SEPARATOR.join(
michael@0 221 p.encode('utf-8') for p in parts))
michael@0 222
michael@0 223 def add_symlink(self, source, dest):
michael@0 224 """Add a symlink to this manifest.
michael@0 225
michael@0 226 dest will be a symlink to source.
michael@0 227 """
michael@0 228 self._add_entry(dest, (self.SYMLINK, source))
michael@0 229
michael@0 230 def add_copy(self, source, dest):
michael@0 231 """Add a copy to this manifest.
michael@0 232
michael@0 233 source will be copied to dest.
michael@0 234 """
michael@0 235 self._add_entry(dest, (self.COPY, source))
michael@0 236
michael@0 237 def add_required_exists(self, dest):
michael@0 238 """Record that a destination file must exist.
michael@0 239
michael@0 240 This effectively prevents the listed file from being deleted.
michael@0 241 """
michael@0 242 self._add_entry(dest, (self.REQUIRED_EXISTS,))
michael@0 243
michael@0 244 def add_optional_exists(self, dest):
michael@0 245 """Record that a destination file may exist.
michael@0 246
michael@0 247 This effectively prevents the listed file from being deleted. Unlike a
michael@0 248 "required exists" file, files of this type do not raise errors if the
michael@0 249 destination file does not exist.
michael@0 250 """
michael@0 251 self._add_entry(dest, (self.OPTIONAL_EXISTS,))
michael@0 252
michael@0 253 def add_pattern_symlink(self, base, pattern, dest):
michael@0 254 """Add a pattern match that results in symlinks being created.
michael@0 255
michael@0 256 A ``FileFinder`` will be created with its base set to ``base``
michael@0 257 and ``FileFinder.find()`` will be called with ``pattern`` to discover
michael@0 258 source files. Each source file will be symlinked under ``dest``.
michael@0 259
michael@0 260 Filenames under ``dest`` are constructed by taking the path fragment
michael@0 261 after ``base`` and concatenating it with ``dest``. e.g.
michael@0 262
michael@0 263 <base>/foo/bar.h -> <dest>/foo/bar.h
michael@0 264 """
michael@0 265 self._add_entry(mozpath.join(base, pattern, dest),
michael@0 266 (self.PATTERN_SYMLINK, base, pattern, dest))
michael@0 267
michael@0 268 def add_pattern_copy(self, base, pattern, dest):
michael@0 269 """Add a pattern match that results in copies.
michael@0 270
michael@0 271 See ``add_pattern_symlink()`` for usage.
michael@0 272 """
michael@0 273 self._add_entry(mozpath.join(base, pattern, dest),
michael@0 274 (self.PATTERN_COPY, base, pattern, dest))
michael@0 275
michael@0 276 def add_preprocess(self, source, dest, deps, marker='#', defines={}):
michael@0 277 """Add a preprocessed file to this manifest.
michael@0 278
michael@0 279 ``source`` will be passed through preprocessor.py, and the output will be
michael@0 280 written to ``dest``.
michael@0 281 """
michael@0 282 self._add_entry(dest,
michael@0 283 (self.PREPROCESS, source, deps, marker, self._encode_field_entry(defines)))
michael@0 284
michael@0 285 def _add_entry(self, dest, entry):
michael@0 286 if dest in self._dests:
michael@0 287 raise ValueError('Item already in manifest: %s' % dest)
michael@0 288
michael@0 289 self._dests[dest] = entry
michael@0 290
michael@0 291 def _get_deps(self, dest):
michael@0 292 return {self._source_file} if self._source_file else set()
michael@0 293
michael@0 294 def populate_registry(self, registry):
michael@0 295 """Populate a mozpack.copier.FileRegistry instance with data from us.
michael@0 296
michael@0 297 The caller supplied a FileRegistry instance (or at least something that
michael@0 298 conforms to its interface) and that instance is populated with data
michael@0 299 from this manifest.
michael@0 300 """
michael@0 301 for dest in sorted(self._dests):
michael@0 302 entry = self._dests[dest]
michael@0 303 install_type = entry[0]
michael@0 304
michael@0 305 if install_type == self.SYMLINK:
michael@0 306 registry.add(dest, AbsoluteSymlinkFile(entry[1]))
michael@0 307 continue
michael@0 308
michael@0 309 if install_type == self.COPY:
michael@0 310 registry.add(dest, File(entry[1]))
michael@0 311 continue
michael@0 312
michael@0 313 if install_type == self.REQUIRED_EXISTS:
michael@0 314 registry.add(dest, ExistingFile(required=True))
michael@0 315 continue
michael@0 316
michael@0 317 if install_type == self.OPTIONAL_EXISTS:
michael@0 318 registry.add(dest, ExistingFile(required=False))
michael@0 319 continue
michael@0 320
michael@0 321 if install_type in (self.PATTERN_SYMLINK, self.PATTERN_COPY):
michael@0 322 _, base, pattern, dest = entry
michael@0 323 finder = FileFinder(base, find_executables=False)
michael@0 324 paths = [f[0] for f in finder.find(pattern)]
michael@0 325
michael@0 326 if install_type == self.PATTERN_SYMLINK:
michael@0 327 cls = AbsoluteSymlinkFile
michael@0 328 else:
michael@0 329 cls = File
michael@0 330
michael@0 331 for path in paths:
michael@0 332 source = mozpath.join(base, path)
michael@0 333 registry.add(mozpath.join(dest, path), cls(source))
michael@0 334
michael@0 335 continue
michael@0 336
michael@0 337 if install_type == self.PREPROCESS:
michael@0 338 registry.add(dest, PreprocessedFile(entry[1],
michael@0 339 depfile_path=entry[2],
michael@0 340 marker=entry[3],
michael@0 341 defines=self._decode_field_entry(entry[4]),
michael@0 342 extra_depends=self._get_deps(dest)))
michael@0 343
michael@0 344 continue
michael@0 345
michael@0 346 raise Exception('Unknown install type defined in manifest: %d' %
michael@0 347 install_type)

mercurial