python/mozbuild/mozpack/packager/__init__.py

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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 mozbuild.preprocessor import Preprocessor
michael@0 6 import re
michael@0 7 import os
michael@0 8 from mozpack.errors import errors
michael@0 9 from mozpack.chrome.manifest import (
michael@0 10 Manifest,
michael@0 11 ManifestChrome,
michael@0 12 ManifestInterfaces,
michael@0 13 is_manifest,
michael@0 14 parse_manifest,
michael@0 15 )
michael@0 16 import mozpack.path
michael@0 17 from collections import deque
michael@0 18
michael@0 19
michael@0 20 class Component(object):
michael@0 21 '''
michael@0 22 Class that represents a component in a package manifest.
michael@0 23 '''
michael@0 24 def __init__(self, name, destdir=''):
michael@0 25 if name.find(' ') > 0:
michael@0 26 errors.fatal('Malformed manifest: space in component name "%s"'
michael@0 27 % component)
michael@0 28 self._name = name
michael@0 29 self._destdir = destdir
michael@0 30
michael@0 31 def __repr__(self):
michael@0 32 s = self.name
michael@0 33 if self.destdir:
michael@0 34 s += ' destdir="%s"' % self.destdir
michael@0 35 return s
michael@0 36
michael@0 37 @property
michael@0 38 def name(self):
michael@0 39 return self._name
michael@0 40
michael@0 41 @property
michael@0 42 def destdir(self):
michael@0 43 return self._destdir
michael@0 44
michael@0 45 @staticmethod
michael@0 46 def _triples(lst):
michael@0 47 '''
michael@0 48 Split [1, 2, 3, 4, 5, 6, 7] into [(1, 2, 3), (4, 5, 6)].
michael@0 49 '''
michael@0 50 return zip(*[iter(lst)] * 3)
michael@0 51
michael@0 52 KEY_VALUE_RE = re.compile(r'''
michael@0 53 \s* # optional whitespace.
michael@0 54 ([a-zA-Z0-9_]+) # key.
michael@0 55 \s*=\s* # optional space around =.
michael@0 56 "([^"]*)" # value without surrounding quotes.
michael@0 57 (?:\s+|$)
michael@0 58 ''', re.VERBOSE)
michael@0 59
michael@0 60 @staticmethod
michael@0 61 def _split_options(string):
michael@0 62 '''
michael@0 63 Split 'key1="value1" key2="value2"' into
michael@0 64 {'key1':'value1', 'key2':'value2'}.
michael@0 65
michael@0 66 Returned keys and values are all strings.
michael@0 67
michael@0 68 Throws ValueError if the input is malformed.
michael@0 69 '''
michael@0 70 options = {}
michael@0 71 splits = Component.KEY_VALUE_RE.split(string)
michael@0 72 if len(splits) % 3 != 1:
michael@0 73 # This should never happen -- we expect to always split
michael@0 74 # into ['', ('key', 'val', '')*].
michael@0 75 raise ValueError("Bad input")
michael@0 76 if splits[0]:
michael@0 77 raise ValueError('Unrecognized input ' + splits[0])
michael@0 78 for key, val, no_match in Component._triples(splits[1:]):
michael@0 79 if no_match:
michael@0 80 raise ValueError('Unrecognized input ' + no_match)
michael@0 81 options[key] = val
michael@0 82 return options
michael@0 83
michael@0 84 @staticmethod
michael@0 85 def _split_component_and_options(string):
michael@0 86 '''
michael@0 87 Split 'name key1="value1" key2="value2"' into
michael@0 88 ('name', {'key1':'value1', 'key2':'value2'}).
michael@0 89
michael@0 90 Returned name, keys and values are all strings.
michael@0 91
michael@0 92 Raises ValueError if the input is malformed.
michael@0 93 '''
michael@0 94 splits = string.strip().split(None, 1)
michael@0 95 if not splits:
michael@0 96 raise ValueError('No component found')
michael@0 97 component = splits[0].strip()
michael@0 98 if not component:
michael@0 99 raise ValueError('No component found')
michael@0 100 if not re.match('[a-zA-Z0-9_\-]+$', component):
michael@0 101 raise ValueError('Bad component name ' + component)
michael@0 102 options = Component._split_options(splits[1]) if len(splits) > 1 else {}
michael@0 103 return component, options
michael@0 104
michael@0 105 @staticmethod
michael@0 106 def from_string(string):
michael@0 107 '''
michael@0 108 Create a component from a string.
michael@0 109 '''
michael@0 110 try:
michael@0 111 name, options = Component._split_component_and_options(string)
michael@0 112 except ValueError as e:
michael@0 113 errors.fatal('Malformed manifest: %s' % e)
michael@0 114 return
michael@0 115 destdir = options.pop('destdir', '')
michael@0 116 if options:
michael@0 117 errors.fatal('Malformed manifest: options %s not recognized'
michael@0 118 % options.keys())
michael@0 119 return Component(name, destdir=destdir)
michael@0 120
michael@0 121
michael@0 122 class PackageManifestParser(object):
michael@0 123 '''
michael@0 124 Class for parsing of a package manifest, after preprocessing.
michael@0 125
michael@0 126 A package manifest is a list of file paths, with some syntaxic sugar:
michael@0 127 [] designates a toplevel component. Example: [xpcom]
michael@0 128 - in front of a file specifies it to be removed
michael@0 129 * wildcard support
michael@0 130 ** expands to all files and zero or more directories
michael@0 131 ; file comment
michael@0 132
michael@0 133 The parser takes input from the preprocessor line by line, and pushes
michael@0 134 parsed information to a sink object.
michael@0 135
michael@0 136 The add and remove methods of the sink object are called with the
michael@0 137 current Component instance and a path.
michael@0 138 '''
michael@0 139 def __init__(self, sink):
michael@0 140 '''
michael@0 141 Initialize the package manifest parser with the given sink.
michael@0 142 '''
michael@0 143 self._component = Component('')
michael@0 144 self._sink = sink
michael@0 145
michael@0 146 def handle_line(self, str):
michael@0 147 '''
michael@0 148 Handle a line of input and push the parsed information to the sink
michael@0 149 object.
michael@0 150 '''
michael@0 151 # Remove comments.
michael@0 152 str = str.strip()
michael@0 153 if not str or str.startswith(';'):
michael@0 154 return
michael@0 155 if str.startswith('[') and str.endswith(']'):
michael@0 156 self._component = Component.from_string(str[1:-1])
michael@0 157 elif str.startswith('-'):
michael@0 158 str = str[1:]
michael@0 159 self._sink.remove(self._component, str)
michael@0 160 elif ',' in str:
michael@0 161 errors.fatal('Incompatible syntax')
michael@0 162 else:
michael@0 163 self._sink.add(self._component, str)
michael@0 164
michael@0 165
michael@0 166 class PreprocessorOutputWrapper(object):
michael@0 167 '''
michael@0 168 File-like helper to handle the preprocessor output and send it to a parser.
michael@0 169 The parser's handle_line method is called in the relevant errors.context.
michael@0 170 '''
michael@0 171 def __init__(self, preprocessor, parser):
michael@0 172 self._parser = parser
michael@0 173 self._pp = preprocessor
michael@0 174
michael@0 175 def write(self, str):
michael@0 176 file = os.path.normpath(os.path.abspath(self._pp.context['FILE']))
michael@0 177 with errors.context(file, self._pp.context['LINE']):
michael@0 178 self._parser.handle_line(str)
michael@0 179
michael@0 180
michael@0 181 def preprocess(input, parser, defines={}):
michael@0 182 '''
michael@0 183 Preprocess the file-like input with the given defines, and send the
michael@0 184 preprocessed output line by line to the given parser.
michael@0 185 '''
michael@0 186 pp = Preprocessor()
michael@0 187 pp.context.update(defines)
michael@0 188 pp.do_filter('substitution')
michael@0 189 pp.out = PreprocessorOutputWrapper(pp, parser)
michael@0 190 pp.do_include(input)
michael@0 191
michael@0 192
michael@0 193 def preprocess_manifest(sink, manifest, defines={}):
michael@0 194 '''
michael@0 195 Preprocess the given file-like manifest with the given defines, and push
michael@0 196 the parsed information to a sink. See PackageManifestParser documentation
michael@0 197 for more details on the sink.
michael@0 198 '''
michael@0 199 preprocess(manifest, PackageManifestParser(sink), defines)
michael@0 200
michael@0 201
michael@0 202 class CallDeque(deque):
michael@0 203 '''
michael@0 204 Queue of function calls to make.
michael@0 205 '''
michael@0 206 def append(self, function, *args):
michael@0 207 deque.append(self, (errors.get_context(), function, args))
michael@0 208
michael@0 209 def execute(self):
michael@0 210 while True:
michael@0 211 try:
michael@0 212 context, function, args = self.popleft()
michael@0 213 except IndexError:
michael@0 214 return
michael@0 215 if context:
michael@0 216 with errors.context(context[0], context[1]):
michael@0 217 function(*args)
michael@0 218 else:
michael@0 219 function(*args)
michael@0 220
michael@0 221
michael@0 222 class SimplePackager(object):
michael@0 223 '''
michael@0 224 Helper used to translate and buffer instructions from the
michael@0 225 SimpleManifestSink to a formatter. Formatters expect some information to be
michael@0 226 given first that the simple manifest contents can't guarantee before the
michael@0 227 end of the input.
michael@0 228 '''
michael@0 229 def __init__(self, formatter):
michael@0 230 self.formatter = formatter
michael@0 231 # Queue for formatter.add_interfaces()/add_manifest() calls.
michael@0 232 self._queue = CallDeque()
michael@0 233 # Queue for formatter.add_manifest() calls for ManifestChrome.
michael@0 234 self._chrome_queue = CallDeque()
michael@0 235 # Queue for formatter.add() calls.
michael@0 236 self._file_queue = CallDeque()
michael@0 237 # All manifest paths imported.
michael@0 238 self._manifests = set()
michael@0 239 # All manifest paths included from some other manifest.
michael@0 240 self._included_manifests = set()
michael@0 241 self._closed = False
michael@0 242
michael@0 243 def add(self, path, file):
michael@0 244 '''
michael@0 245 Add the given BaseFile instance with the given path.
michael@0 246 '''
michael@0 247 assert not self._closed
michael@0 248 if is_manifest(path):
michael@0 249 self._add_manifest_file(path, file)
michael@0 250 elif path.endswith('.xpt'):
michael@0 251 self._queue.append(self.formatter.add_interfaces, path, file)
michael@0 252 else:
michael@0 253 self._file_queue.append(self.formatter.add, path, file)
michael@0 254
michael@0 255 def _add_manifest_file(self, path, file):
michael@0 256 '''
michael@0 257 Add the given BaseFile with manifest file contents with the given path.
michael@0 258 '''
michael@0 259 self._manifests.add(path)
michael@0 260 base = ''
michael@0 261 if hasattr(file, 'path'):
michael@0 262 # Find the directory the given path is relative to.
michael@0 263 b = mozpack.path.normsep(file.path)
michael@0 264 if b.endswith('/' + path) or b == path:
michael@0 265 base = os.path.normpath(b[:-len(path)])
michael@0 266 for e in parse_manifest(base, path, file.open()):
michael@0 267 # ManifestResources need to be given after ManifestChrome, so just
michael@0 268 # put all ManifestChrome in a separate queue to make them first.
michael@0 269 if isinstance(e, ManifestChrome):
michael@0 270 # e.move(e.base) just returns a clone of the entry.
michael@0 271 self._chrome_queue.append(self.formatter.add_manifest,
michael@0 272 e.move(e.base))
michael@0 273 elif not isinstance(e, (Manifest, ManifestInterfaces)):
michael@0 274 self._queue.append(self.formatter.add_manifest, e.move(e.base))
michael@0 275 if isinstance(e, Manifest):
michael@0 276 if e.flags:
michael@0 277 errors.fatal('Flags are not supported on ' +
michael@0 278 '"manifest" entries')
michael@0 279 self._included_manifests.add(e.path)
michael@0 280
michael@0 281 def get_bases(self):
michael@0 282 '''
michael@0 283 Return all paths under which root manifests have been found. Root
michael@0 284 manifests are manifests that are included in no other manifest.
michael@0 285 '''
michael@0 286 return set(mozpack.path.dirname(m)
michael@0 287 for m in self._manifests - self._included_manifests)
michael@0 288
michael@0 289 def close(self):
michael@0 290 '''
michael@0 291 Push all instructions to the formatter.
michael@0 292 '''
michael@0 293 self._closed = True
michael@0 294 for base in self.get_bases():
michael@0 295 if base:
michael@0 296 self.formatter.add_base(base)
michael@0 297 self._chrome_queue.execute()
michael@0 298 self._queue.execute()
michael@0 299 self._file_queue.execute()
michael@0 300
michael@0 301
michael@0 302 class SimpleManifestSink(object):
michael@0 303 '''
michael@0 304 Parser sink for "simple" package manifests. Simple package manifests use
michael@0 305 the format described in the PackageManifestParser documentation, but don't
michael@0 306 support file removals, and require manifests, interfaces and chrome data to
michael@0 307 be explicitely listed.
michael@0 308 Entries starting with bin/ are searched under bin/ in the FileFinder, but
michael@0 309 are packaged without the bin/ prefix.
michael@0 310 '''
michael@0 311 def __init__(self, finder, formatter):
michael@0 312 '''
michael@0 313 Initialize the SimpleManifestSink. The given FileFinder is used to
michael@0 314 get files matching the patterns given in the manifest. The given
michael@0 315 formatter does the packaging job.
michael@0 316 '''
michael@0 317 self._finder = finder
michael@0 318 self.packager = SimplePackager(formatter)
michael@0 319 self._closed = False
michael@0 320 self._manifests = set()
michael@0 321
michael@0 322 @staticmethod
michael@0 323 def normalize_path(path):
michael@0 324 '''
michael@0 325 Remove any bin/ prefix.
michael@0 326 '''
michael@0 327 if mozpack.path.basedir(path, ['bin']) == 'bin':
michael@0 328 return mozpack.path.relpath(path, 'bin')
michael@0 329 return path
michael@0 330
michael@0 331 def add(self, component, pattern):
michael@0 332 '''
michael@0 333 Add files with the given pattern in the given component.
michael@0 334 '''
michael@0 335 assert not self._closed
michael@0 336 added = False
michael@0 337 for p, f in self._finder.find(pattern):
michael@0 338 added = True
michael@0 339 if is_manifest(p):
michael@0 340 self._manifests.add(p)
michael@0 341 dest = mozpack.path.join(component.destdir, SimpleManifestSink.normalize_path(p))
michael@0 342 self.packager.add(dest, f)
michael@0 343 if not added:
michael@0 344 errors.error('Missing file(s): %s' % pattern)
michael@0 345
michael@0 346 def remove(self, component, pattern):
michael@0 347 '''
michael@0 348 Remove files with the given pattern in the given component.
michael@0 349 '''
michael@0 350 assert not self._closed
michael@0 351 errors.fatal('Removal is unsupported')
michael@0 352
michael@0 353 def close(self, auto_root_manifest=True):
michael@0 354 '''
michael@0 355 Add possibly missing bits and push all instructions to the formatter.
michael@0 356 '''
michael@0 357 if auto_root_manifest:
michael@0 358 # Simple package manifests don't contain the root manifests, so
michael@0 359 # find and add them.
michael@0 360 paths = [mozpack.path.dirname(m) for m in self._manifests]
michael@0 361 path = mozpack.path.dirname(mozpack.path.commonprefix(paths))
michael@0 362 for p, f in self._finder.find(mozpack.path.join(path,
michael@0 363 'chrome.manifest')):
michael@0 364 if not p in self._manifests:
michael@0 365 self.packager.add(SimpleManifestSink.normalize_path(p), f)
michael@0 366 self.packager.close()

mercurial