Wed, 31 Dec 2014 06:55:50 +0100
Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2
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() |