|
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/. |
|
4 |
|
5 # This module contains code for managing WebIDL files and bindings for |
|
6 # the build system. |
|
7 |
|
8 from __future__ import unicode_literals |
|
9 |
|
10 import errno |
|
11 import hashlib |
|
12 import json |
|
13 import logging |
|
14 import os |
|
15 |
|
16 from copy import deepcopy |
|
17 |
|
18 from mach.mixin.logging import LoggingMixin |
|
19 |
|
20 from mozbuild.base import MozbuildObject |
|
21 from mozbuild.makeutil import Makefile |
|
22 from mozbuild.pythonutil import iter_modules_in_path |
|
23 from mozbuild.util import FileAvoidWrite |
|
24 |
|
25 import mozpack.path as mozpath |
|
26 |
|
27 # There are various imports in this file in functions to avoid adding |
|
28 # dependencies to config.status. See bug 949875. |
|
29 |
|
30 |
|
31 class BuildResult(object): |
|
32 """Represents the result of processing WebIDL files. |
|
33 |
|
34 This holds a summary of output file generation during code generation. |
|
35 """ |
|
36 |
|
37 def __init__(self): |
|
38 # The .webidl files that had their outputs regenerated. |
|
39 self.inputs = set() |
|
40 |
|
41 # The output files that were created. |
|
42 self.created = set() |
|
43 |
|
44 # The output files that changed. |
|
45 self.updated = set() |
|
46 |
|
47 # The output files that didn't change. |
|
48 self.unchanged = set() |
|
49 |
|
50 |
|
51 class WebIDLCodegenManagerState(dict): |
|
52 """Holds state for the WebIDL code generation manager. |
|
53 |
|
54 State is currently just an extended dict. The internal implementation of |
|
55 state should be considered a black box to everyone except |
|
56 WebIDLCodegenManager. But we'll still document it. |
|
57 |
|
58 Fields: |
|
59 |
|
60 version |
|
61 The integer version of the format. This is to detect incompatible |
|
62 changes between state. It should be bumped whenever the format |
|
63 changes or semantics change. |
|
64 |
|
65 webidls |
|
66 A dictionary holding information about every known WebIDL input. |
|
67 Keys are the basenames of input WebIDL files. Values are dicts of |
|
68 metadata. Keys in those dicts are: |
|
69 |
|
70 * filename - The full path to the input filename. |
|
71 * inputs - A set of full paths to other webidl files this webidl |
|
72 depends on. |
|
73 * outputs - Set of full output paths that are created/derived from |
|
74 this file. |
|
75 * sha1 - The hexidecimal SHA-1 of the input filename from the last |
|
76 processing time. |
|
77 |
|
78 global_inputs |
|
79 A dictionary defining files that influence all processing. Keys |
|
80 are full filenames. Values are hexidecimal SHA-1 from the last |
|
81 processing time. |
|
82 """ |
|
83 |
|
84 VERSION = 1 |
|
85 |
|
86 def __init__(self, fh=None): |
|
87 self['version'] = self.VERSION |
|
88 self['webidls'] = {} |
|
89 self['global_depends'] = {} |
|
90 |
|
91 if not fh: |
|
92 return |
|
93 |
|
94 state = json.load(fh) |
|
95 if state['version'] != self.VERSION: |
|
96 raise Exception('Unknown state version: %s' % state['version']) |
|
97 |
|
98 self['version'] = state['version'] |
|
99 self['global_depends'] = state['global_depends'] |
|
100 |
|
101 for k, v in state['webidls'].items(): |
|
102 self['webidls'][k] = v |
|
103 |
|
104 # Sets are converted to lists for serialization because JSON |
|
105 # doesn't support sets. |
|
106 self['webidls'][k]['inputs'] = set(v['inputs']) |
|
107 self['webidls'][k]['outputs'] = set(v['outputs']) |
|
108 |
|
109 def dump(self, fh): |
|
110 """Dump serialized state to a file handle.""" |
|
111 normalized = deepcopy(self) |
|
112 |
|
113 for k, v in self['webidls'].items(): |
|
114 # Convert sets to lists because JSON doesn't support sets. |
|
115 normalized['webidls'][k]['outputs'] = sorted(v['outputs']) |
|
116 normalized['webidls'][k]['inputs'] = sorted(v['inputs']) |
|
117 |
|
118 json.dump(normalized, fh, sort_keys=True) |
|
119 |
|
120 |
|
121 class WebIDLCodegenManager(LoggingMixin): |
|
122 """Manages all code generation around WebIDL. |
|
123 |
|
124 To facilitate testing, this object is meant to be generic and reusable. |
|
125 Paths, etc should be parameters and not hardcoded. |
|
126 """ |
|
127 |
|
128 # Global parser derived declaration files. |
|
129 GLOBAL_DECLARE_FILES = { |
|
130 'GeneratedAtomList.h', |
|
131 'PrototypeList.h', |
|
132 'RegisterBindings.h', |
|
133 'UnionConversions.h', |
|
134 'UnionTypes.h', |
|
135 } |
|
136 |
|
137 # Global parser derived definition files. |
|
138 GLOBAL_DEFINE_FILES = { |
|
139 'RegisterBindings.cpp', |
|
140 'UnionTypes.cpp', |
|
141 'PrototypeList.cpp', |
|
142 } |
|
143 |
|
144 def __init__(self, config_path, inputs, exported_header_dir, |
|
145 codegen_dir, state_path, cache_dir=None, make_deps_path=None, |
|
146 make_deps_target=None): |
|
147 """Create an instance that manages WebIDLs in the build system. |
|
148 |
|
149 config_path refers to a WebIDL config file (e.g. Bindings.conf). |
|
150 inputs is a 4-tuple describing the input .webidl files and how to |
|
151 process them. Members are: |
|
152 (set(.webidl files), set(basenames of exported files), |
|
153 set(basenames of generated events files), |
|
154 set(example interface names)) |
|
155 |
|
156 exported_header_dir and codegen_dir are directories where generated |
|
157 files will be written to. |
|
158 state_path is the path to a file that will receive JSON state from our |
|
159 actions. |
|
160 make_deps_path is the path to a make dependency file that we can |
|
161 optionally write. |
|
162 make_deps_target is the target that receives the make dependencies. It |
|
163 must be defined if using make_deps_path. |
|
164 """ |
|
165 self.populate_logger() |
|
166 |
|
167 input_paths, exported_stems, generated_events_stems, example_interfaces = inputs |
|
168 |
|
169 self._config_path = config_path |
|
170 self._input_paths = set(input_paths) |
|
171 self._exported_stems = set(exported_stems) |
|
172 self._generated_events_stems = set(generated_events_stems) |
|
173 self._example_interfaces = set(example_interfaces) |
|
174 self._exported_header_dir = exported_header_dir |
|
175 self._codegen_dir = codegen_dir |
|
176 self._state_path = state_path |
|
177 self._cache_dir = cache_dir |
|
178 self._make_deps_path = make_deps_path |
|
179 self._make_deps_target = make_deps_target |
|
180 |
|
181 if (make_deps_path and not make_deps_target) or (not make_deps_path and |
|
182 make_deps_target): |
|
183 raise Exception('Must define both make_deps_path and make_deps_target ' |
|
184 'if one is defined.') |
|
185 |
|
186 self._parser_results = None |
|
187 self._config = None |
|
188 self._state = WebIDLCodegenManagerState() |
|
189 |
|
190 if os.path.exists(state_path): |
|
191 with open(state_path, 'rb') as fh: |
|
192 try: |
|
193 self._state = WebIDLCodegenManagerState(fh=fh) |
|
194 except Exception as e: |
|
195 self.log(logging.WARN, 'webidl_bad_state', {'msg': str(e)}, |
|
196 'Bad WebIDL state: {msg}') |
|
197 |
|
198 @property |
|
199 def config(self): |
|
200 if not self._config: |
|
201 self._parse_webidl() |
|
202 |
|
203 return self._config |
|
204 |
|
205 def generate_build_files(self): |
|
206 """Generate files required for the build. |
|
207 |
|
208 This function is in charge of generating all the .h/.cpp files derived |
|
209 from input .webidl files. Please note that there are build actions |
|
210 required to produce .webidl files and these build actions are |
|
211 explicitly not captured here: this function assumes all .webidl files |
|
212 are present and up to date. |
|
213 |
|
214 This routine is called as part of the build to ensure files that need |
|
215 to exist are present and up to date. This routine may not be called if |
|
216 the build dependencies (generated as a result of calling this the first |
|
217 time) say everything is up to date. |
|
218 |
|
219 Because reprocessing outputs for every .webidl on every invocation |
|
220 is expensive, we only regenerate the minimal set of files on every |
|
221 invocation. The rules for deciding what needs done are roughly as |
|
222 follows: |
|
223 |
|
224 1. If any .webidl changes, reparse all .webidl files and regenerate |
|
225 the global derived files. Only regenerate output files (.h/.cpp) |
|
226 impacted by the modified .webidl files. |
|
227 2. If an non-.webidl dependency (Python files, config file) changes, |
|
228 assume everything is out of date and regenerate the world. This |
|
229 is because changes in those could globally impact every output |
|
230 file. |
|
231 3. If an output file is missing, ensure it is present by performing |
|
232 necessary regeneration. |
|
233 """ |
|
234 # Despite #1 above, we assume the build system is smart enough to not |
|
235 # invoke us if nothing has changed. Therefore, any invocation means |
|
236 # something has changed. And, if anything has changed, we need to |
|
237 # parse the WebIDL. |
|
238 self._parse_webidl() |
|
239 |
|
240 result = BuildResult() |
|
241 |
|
242 # If we parse, we always update globals - they are cheap and it is |
|
243 # easier that way. |
|
244 created, updated, unchanged = self._write_global_derived() |
|
245 result.created |= created |
|
246 result.updated |= updated |
|
247 result.unchanged |= unchanged |
|
248 |
|
249 # If any of the extra dependencies changed, regenerate the world. |
|
250 global_changed, global_hashes = self._global_dependencies_changed() |
|
251 if global_changed: |
|
252 # Make a copy because we may modify. |
|
253 changed_inputs = set(self._input_paths) |
|
254 else: |
|
255 changed_inputs = self._compute_changed_inputs() |
|
256 |
|
257 self._state['global_depends'] = global_hashes |
|
258 |
|
259 # Generate bindings from .webidl files. |
|
260 for filename in sorted(changed_inputs): |
|
261 basename = mozpath.basename(filename) |
|
262 result.inputs.add(filename) |
|
263 written, deps = self._generate_build_files_for_webidl(filename) |
|
264 result.created |= written[0] |
|
265 result.updated |= written[1] |
|
266 result.unchanged |= written[2] |
|
267 |
|
268 self._state['webidls'][basename] = dict( |
|
269 filename=filename, |
|
270 outputs=written[0] | written[1] | written[2], |
|
271 inputs=set(deps), |
|
272 sha1=self._input_hashes[filename], |
|
273 ) |
|
274 |
|
275 # Process some special interfaces required for testing. |
|
276 for interface in self._example_interfaces: |
|
277 written = self.generate_example_files(interface) |
|
278 result.created |= written[0] |
|
279 result.updated |= written[1] |
|
280 result.unchanged |= written[2] |
|
281 |
|
282 # Generate a make dependency file. |
|
283 if self._make_deps_path: |
|
284 mk = Makefile() |
|
285 codegen_rule = mk.create_rule([self._make_deps_target]) |
|
286 codegen_rule.add_dependencies(global_hashes.keys()) |
|
287 codegen_rule.add_dependencies(self._input_paths) |
|
288 |
|
289 with FileAvoidWrite(self._make_deps_path) as fh: |
|
290 mk.dump(fh) |
|
291 |
|
292 self._save_state() |
|
293 |
|
294 return result |
|
295 |
|
296 def generate_example_files(self, interface): |
|
297 """Generates example files for a given interface.""" |
|
298 from Codegen import CGExampleRoot |
|
299 |
|
300 root = CGExampleRoot(self.config, interface) |
|
301 |
|
302 return self._maybe_write_codegen(root, *self._example_paths(interface)) |
|
303 |
|
304 def _parse_webidl(self): |
|
305 import WebIDL |
|
306 from Configuration import Configuration |
|
307 |
|
308 self.log(logging.INFO, 'webidl_parse', |
|
309 {'count': len(self._input_paths)}, |
|
310 'Parsing {count} WebIDL files.') |
|
311 |
|
312 hashes = {} |
|
313 parser = WebIDL.Parser(self._cache_dir) |
|
314 |
|
315 for path in sorted(self._input_paths): |
|
316 with open(path, 'rb') as fh: |
|
317 data = fh.read() |
|
318 hashes[path] = hashlib.sha1(data).hexdigest() |
|
319 parser.parse(data, path) |
|
320 |
|
321 self._parser_results = parser.finish() |
|
322 self._config = Configuration(self._config_path, self._parser_results) |
|
323 self._input_hashes = hashes |
|
324 |
|
325 def _write_global_derived(self): |
|
326 from Codegen import GlobalGenRoots |
|
327 |
|
328 things = [('declare', f) for f in self.GLOBAL_DECLARE_FILES] |
|
329 things.extend(('define', f) for f in self.GLOBAL_DEFINE_FILES) |
|
330 |
|
331 result = (set(), set(), set()) |
|
332 |
|
333 for what, filename in things: |
|
334 stem = mozpath.splitext(filename)[0] |
|
335 root = getattr(GlobalGenRoots, stem)(self._config) |
|
336 |
|
337 if what == 'declare': |
|
338 code = root.declare() |
|
339 output_root = self._exported_header_dir |
|
340 elif what == 'define': |
|
341 code = root.define() |
|
342 output_root = self._codegen_dir |
|
343 else: |
|
344 raise Exception('Unknown global gen type: %s' % what) |
|
345 |
|
346 output_path = mozpath.join(output_root, filename) |
|
347 self._maybe_write_file(output_path, code, result) |
|
348 |
|
349 return result |
|
350 |
|
351 def _compute_changed_inputs(self): |
|
352 """Compute the set of input files that need to be regenerated.""" |
|
353 changed_inputs = set() |
|
354 expected_outputs = self.expected_build_output_files() |
|
355 |
|
356 # Look for missing output files. |
|
357 if any(not os.path.exists(f) for f in expected_outputs): |
|
358 # FUTURE Bug 940469 Only regenerate minimum set. |
|
359 changed_inputs |= self._input_paths |
|
360 |
|
361 # That's it for examining output files. We /could/ examine SHA-1's of |
|
362 # output files from a previous run to detect modifications. But that's |
|
363 # a lot of extra work and most build systems don't do that anyway. |
|
364 |
|
365 # Now we move on to the input files. |
|
366 old_hashes = {v['filename']: v['sha1'] |
|
367 for v in self._state['webidls'].values()} |
|
368 |
|
369 old_filenames = set(old_hashes.keys()) |
|
370 new_filenames = self._input_paths |
|
371 |
|
372 # If an old file has disappeared or a new file has arrived, mark |
|
373 # it. |
|
374 changed_inputs |= old_filenames ^ new_filenames |
|
375 |
|
376 # For the files in common between runs, compare content. If the file |
|
377 # has changed, mark it. We don't need to perform mtime comparisons |
|
378 # because content is a stronger validator. |
|
379 for filename in old_filenames & new_filenames: |
|
380 if old_hashes[filename] != self._input_hashes[filename]: |
|
381 changed_inputs.add(filename) |
|
382 |
|
383 # We've now populated the base set of inputs that have changed. |
|
384 |
|
385 # Inherit dependencies from previous run. The full set of dependencies |
|
386 # is associated with each record, so we don't need to perform any fancy |
|
387 # graph traversal. |
|
388 for v in self._state['webidls'].values(): |
|
389 if any(dep for dep in v['inputs'] if dep in changed_inputs): |
|
390 changed_inputs.add(v['filename']) |
|
391 |
|
392 # Only use paths that are known to our current state. |
|
393 # This filters out files that were deleted or changed type (e.g. from |
|
394 # static to preprocessed). |
|
395 return changed_inputs & self._input_paths |
|
396 |
|
397 def _binding_info(self, p): |
|
398 """Compute binding metadata for an input path. |
|
399 |
|
400 Returns a tuple of: |
|
401 |
|
402 (stem, binding_stem, is_event, output_files) |
|
403 |
|
404 output_files is itself a tuple. The first two items are the binding |
|
405 header and C++ paths, respectively. The 2nd pair are the event header |
|
406 and C++ paths or None if this isn't an event binding. |
|
407 """ |
|
408 basename = mozpath.basename(p) |
|
409 stem = mozpath.splitext(basename)[0] |
|
410 binding_stem = '%sBinding' % stem |
|
411 |
|
412 if stem in self._exported_stems: |
|
413 header_dir = self._exported_header_dir |
|
414 else: |
|
415 header_dir = self._codegen_dir |
|
416 |
|
417 is_event = stem in self._generated_events_stems |
|
418 |
|
419 files = ( |
|
420 mozpath.join(header_dir, '%s.h' % binding_stem), |
|
421 mozpath.join(self._codegen_dir, '%s.cpp' % binding_stem), |
|
422 mozpath.join(header_dir, '%s.h' % stem) if is_event else None, |
|
423 mozpath.join(self._codegen_dir, '%s.cpp' % stem) if is_event else None, |
|
424 ) |
|
425 |
|
426 return stem, binding_stem, is_event, header_dir, files |
|
427 |
|
428 def _example_paths(self, interface): |
|
429 return ( |
|
430 mozpath.join(self._codegen_dir, '%s-example.h' % interface), |
|
431 mozpath.join(self._codegen_dir, '%s-example.cpp' % interface)) |
|
432 |
|
433 def expected_build_output_files(self): |
|
434 """Obtain the set of files generate_build_files() should write.""" |
|
435 paths = set() |
|
436 |
|
437 # Account for global generation. |
|
438 for p in self.GLOBAL_DECLARE_FILES: |
|
439 paths.add(mozpath.join(self._exported_header_dir, p)) |
|
440 for p in self.GLOBAL_DEFINE_FILES: |
|
441 paths.add(mozpath.join(self._codegen_dir, p)) |
|
442 |
|
443 for p in self._input_paths: |
|
444 stem, binding_stem, is_event, header_dir, files = self._binding_info(p) |
|
445 paths |= {f for f in files if f} |
|
446 |
|
447 for interface in self._example_interfaces: |
|
448 for p in self._example_paths(interface): |
|
449 paths.add(p) |
|
450 |
|
451 return paths |
|
452 |
|
453 def _generate_build_files_for_webidl(self, filename): |
|
454 from Codegen import ( |
|
455 CGBindingRoot, |
|
456 CGEventRoot, |
|
457 ) |
|
458 |
|
459 self.log(logging.INFO, 'webidl_generate_build_for_input', |
|
460 {'filename': filename}, |
|
461 'Generating WebIDL files derived from {filename}') |
|
462 |
|
463 stem, binding_stem, is_event, header_dir, files = self._binding_info(filename) |
|
464 root = CGBindingRoot(self._config, binding_stem, filename) |
|
465 |
|
466 result = self._maybe_write_codegen(root, files[0], files[1]) |
|
467 |
|
468 if is_event: |
|
469 generated_event = CGEventRoot(self._config, stem) |
|
470 result = self._maybe_write_codegen(generated_event, files[2], |
|
471 files[3], result) |
|
472 |
|
473 return result, root.deps() |
|
474 |
|
475 def _global_dependencies_changed(self): |
|
476 """Determine whether the global dependencies have changed.""" |
|
477 current_files = set(iter_modules_in_path(mozpath.dirname(__file__))) |
|
478 |
|
479 # We need to catch other .py files from /dom/bindings. We assume these |
|
480 # are in the same directory as the config file. |
|
481 current_files |= set(iter_modules_in_path(mozpath.dirname(self._config_path))) |
|
482 |
|
483 current_files.add(self._config_path) |
|
484 |
|
485 current_hashes = {} |
|
486 for f in current_files: |
|
487 # This will fail if the file doesn't exist. If a current global |
|
488 # dependency doesn't exist, something else is wrong. |
|
489 with open(f, 'rb') as fh: |
|
490 current_hashes[f] = hashlib.sha1(fh.read()).hexdigest() |
|
491 |
|
492 # The set of files has changed. |
|
493 if current_files ^ set(self._state['global_depends'].keys()): |
|
494 return True, current_hashes |
|
495 |
|
496 # Compare hashes. |
|
497 for f, sha1 in current_hashes.items(): |
|
498 if sha1 != self._state['global_depends'][f]: |
|
499 return True, current_hashes |
|
500 |
|
501 return False, current_hashes |
|
502 |
|
503 def _save_state(self): |
|
504 with open(self._state_path, 'wb') as fh: |
|
505 self._state.dump(fh) |
|
506 |
|
507 def _maybe_write_codegen(self, obj, declare_path, define_path, result=None): |
|
508 assert declare_path and define_path |
|
509 if not result: |
|
510 result = (set(), set(), set()) |
|
511 |
|
512 self._maybe_write_file(declare_path, obj.declare(), result) |
|
513 self._maybe_write_file(define_path, obj.define(), result) |
|
514 |
|
515 return result |
|
516 |
|
517 def _maybe_write_file(self, path, content, result): |
|
518 fh = FileAvoidWrite(path) |
|
519 fh.write(content) |
|
520 existed, updated = fh.close() |
|
521 |
|
522 if not existed: |
|
523 result[0].add(path) |
|
524 elif updated: |
|
525 result[1].add(path) |
|
526 else: |
|
527 result[2].add(path) |
|
528 |
|
529 |
|
530 def create_build_system_manager(topsrcdir, topobjdir, dist_dir): |
|
531 """Create a WebIDLCodegenManager for use by the build system.""" |
|
532 src_dir = os.path.join(topsrcdir, 'dom', 'bindings') |
|
533 obj_dir = os.path.join(topobjdir, 'dom', 'bindings') |
|
534 |
|
535 with open(os.path.join(obj_dir, 'file-lists.json'), 'rb') as fh: |
|
536 files = json.load(fh) |
|
537 |
|
538 inputs = (files['webidls'], files['exported_stems'], |
|
539 files['generated_events_stems'], files['example_interfaces']) |
|
540 |
|
541 cache_dir = os.path.join(obj_dir, '_cache') |
|
542 try: |
|
543 os.makedirs(cache_dir) |
|
544 except OSError as e: |
|
545 if e.errno != errno.EEXIST: |
|
546 raise |
|
547 |
|
548 return WebIDLCodegenManager( |
|
549 os.path.join(src_dir, 'Bindings.conf'), |
|
550 inputs, |
|
551 os.path.join(dist_dir, 'include', 'mozilla', 'dom'), |
|
552 obj_dir, |
|
553 os.path.join(obj_dir, 'codegen.json'), |
|
554 cache_dir=cache_dir, |
|
555 # The make rules include a codegen.pp file containing dependencies. |
|
556 make_deps_path=os.path.join(obj_dir, 'codegen.pp'), |
|
557 make_deps_target='codegen.pp', |
|
558 ) |
|
559 |
|
560 |
|
561 class BuildSystemWebIDL(MozbuildObject): |
|
562 @property |
|
563 def manager(self): |
|
564 if not hasattr(self, '_webidl_manager'): |
|
565 self._webidl_manager = create_build_system_manager( |
|
566 self.topsrcdir, self.topobjdir, self.distdir) |
|
567 |
|
568 return self._webidl_manager |