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