зеркало из https://github.com/mozilla/gecko-dev.git
577 строки
21 KiB
Python
577 строки
21 KiB
Python
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
# This module contains code for managing WebIDL files and bindings for
|
|
# the build system.
|
|
|
|
from __future__ import unicode_literals
|
|
|
|
import errno
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import os
|
|
|
|
from copy import deepcopy
|
|
|
|
from mach.mixin.logging import LoggingMixin
|
|
|
|
from mozbuild.base import MozbuildObject
|
|
from mozbuild.makeutil import Makefile
|
|
from mozbuild.pythonutil import iter_modules_in_path
|
|
from mozbuild.util import FileAvoidWrite
|
|
|
|
import mozpack.path as mozpath
|
|
|
|
# There are various imports in this file in functions to avoid adding
|
|
# dependencies to config.status. See bug 949875.
|
|
|
|
|
|
class BuildResult(object):
|
|
"""Represents the result of processing WebIDL files.
|
|
|
|
This holds a summary of output file generation during code generation.
|
|
"""
|
|
|
|
def __init__(self):
|
|
# The .webidl files that had their outputs regenerated.
|
|
self.inputs = set()
|
|
|
|
# The output files that were created.
|
|
self.created = set()
|
|
|
|
# The output files that changed.
|
|
self.updated = set()
|
|
|
|
# The output files that didn't change.
|
|
self.unchanged = set()
|
|
|
|
|
|
class WebIDLCodegenManagerState(dict):
|
|
"""Holds state for the WebIDL code generation manager.
|
|
|
|
State is currently just an extended dict. The internal implementation of
|
|
state should be considered a black box to everyone except
|
|
WebIDLCodegenManager. But we'll still document it.
|
|
|
|
Fields:
|
|
|
|
version
|
|
The integer version of the format. This is to detect incompatible
|
|
changes between state. It should be bumped whenever the format
|
|
changes or semantics change.
|
|
|
|
webidls
|
|
A dictionary holding information about every known WebIDL input.
|
|
Keys are the basenames of input WebIDL files. Values are dicts of
|
|
metadata. Keys in those dicts are:
|
|
|
|
* filename - The full path to the input filename.
|
|
* inputs - A set of full paths to other webidl files this webidl
|
|
depends on.
|
|
* outputs - Set of full output paths that are created/derived from
|
|
this file.
|
|
* sha1 - The hexidecimal SHA-1 of the input filename from the last
|
|
processing time.
|
|
|
|
global_inputs
|
|
A dictionary defining files that influence all processing. Keys
|
|
are full filenames. Values are hexidecimal SHA-1 from the last
|
|
processing time.
|
|
"""
|
|
|
|
VERSION = 1
|
|
|
|
def __init__(self, fh=None):
|
|
self['version'] = self.VERSION
|
|
self['webidls'] = {}
|
|
self['global_depends'] = {}
|
|
|
|
if not fh:
|
|
return
|
|
|
|
state = json.load(fh)
|
|
if state['version'] != self.VERSION:
|
|
raise Exception('Unknown state version: %s' % state['version'])
|
|
|
|
self['version'] = state['version']
|
|
self['global_depends'] = state['global_depends']
|
|
|
|
for k, v in state['webidls'].items():
|
|
self['webidls'][k] = v
|
|
|
|
# Sets are converted to lists for serialization because JSON
|
|
# doesn't support sets.
|
|
self['webidls'][k]['inputs'] = set(v['inputs'])
|
|
self['webidls'][k]['outputs'] = set(v['outputs'])
|
|
|
|
def dump(self, fh):
|
|
"""Dump serialized state to a file handle."""
|
|
normalized = deepcopy(self)
|
|
|
|
for k, v in self['webidls'].items():
|
|
# Convert sets to lists because JSON doesn't support sets.
|
|
normalized['webidls'][k]['outputs'] = sorted(v['outputs'])
|
|
normalized['webidls'][k]['inputs'] = sorted(v['inputs'])
|
|
|
|
json.dump(normalized, fh, sort_keys=True)
|
|
|
|
|
|
class WebIDLCodegenManager(LoggingMixin):
|
|
"""Manages all code generation around WebIDL.
|
|
|
|
To facilitate testing, this object is meant to be generic and reusable.
|
|
Paths, etc should be parameters and not hardcoded.
|
|
"""
|
|
|
|
# Global parser derived declaration files.
|
|
GLOBAL_DECLARE_FILES = {
|
|
'FeatureList.h',
|
|
'GeneratedAtomList.h',
|
|
'GeneratedEventList.h',
|
|
'PrototypeList.h',
|
|
'RegisterBindings.h',
|
|
'RegisterWorkerBindings.h',
|
|
'ResolveSystemBinding.h',
|
|
'UnionConversions.h',
|
|
'UnionTypes.h',
|
|
}
|
|
|
|
# Global parser derived definition files.
|
|
GLOBAL_DEFINE_FILES = {
|
|
'RegisterBindings.cpp',
|
|
'RegisterWorkerBindings.cpp',
|
|
'ResolveSystemBinding.cpp',
|
|
'UnionTypes.cpp',
|
|
'PrototypeList.cpp',
|
|
}
|
|
|
|
def __init__(self, config_path, inputs, exported_header_dir,
|
|
codegen_dir, state_path, cache_dir=None, make_deps_path=None,
|
|
make_deps_target=None):
|
|
"""Create an instance that manages WebIDLs in the build system.
|
|
|
|
config_path refers to a WebIDL config file (e.g. Bindings.conf).
|
|
inputs is a 4-tuple describing the input .webidl files and how to
|
|
process them. Members are:
|
|
(set(.webidl files), set(basenames of exported files),
|
|
set(basenames of generated events files),
|
|
set(example interface names))
|
|
|
|
exported_header_dir and codegen_dir are directories where generated
|
|
files will be written to.
|
|
state_path is the path to a file that will receive JSON state from our
|
|
actions.
|
|
make_deps_path is the path to a make dependency file that we can
|
|
optionally write.
|
|
make_deps_target is the target that receives the make dependencies. It
|
|
must be defined if using make_deps_path.
|
|
"""
|
|
self.populate_logger()
|
|
|
|
input_paths, exported_stems, generated_events_stems, example_interfaces = inputs
|
|
|
|
self._config_path = config_path
|
|
self._input_paths = set(input_paths)
|
|
self._exported_stems = set(exported_stems)
|
|
self._generated_events_stems = set(generated_events_stems)
|
|
self._generated_events_stems_as_array = generated_events_stems
|
|
self._example_interfaces = set(example_interfaces)
|
|
self._exported_header_dir = exported_header_dir
|
|
self._codegen_dir = codegen_dir
|
|
self._state_path = state_path
|
|
self._cache_dir = cache_dir
|
|
self._make_deps_path = make_deps_path
|
|
self._make_deps_target = make_deps_target
|
|
|
|
if ((make_deps_path and not make_deps_target) or
|
|
(not make_deps_path and make_deps_target)):
|
|
raise Exception('Must define both make_deps_path and make_deps_target '
|
|
'if one is defined.')
|
|
|
|
self._parser_results = None
|
|
self._config = None
|
|
self._state = WebIDLCodegenManagerState()
|
|
|
|
if os.path.exists(state_path):
|
|
with open(state_path, 'rb') as fh:
|
|
try:
|
|
self._state = WebIDLCodegenManagerState(fh=fh)
|
|
except Exception as e:
|
|
self.log(logging.WARN, 'webidl_bad_state', {'msg': str(e)},
|
|
'Bad WebIDL state: {msg}')
|
|
|
|
@property
|
|
def config(self):
|
|
if not self._config:
|
|
self._parse_webidl()
|
|
|
|
return self._config
|
|
|
|
def generate_build_files(self):
|
|
"""Generate files required for the build.
|
|
|
|
This function is in charge of generating all the .h/.cpp files derived
|
|
from input .webidl files. Please note that there are build actions
|
|
required to produce .webidl files and these build actions are
|
|
explicitly not captured here: this function assumes all .webidl files
|
|
are present and up to date.
|
|
|
|
This routine is called as part of the build to ensure files that need
|
|
to exist are present and up to date. This routine may not be called if
|
|
the build dependencies (generated as a result of calling this the first
|
|
time) say everything is up to date.
|
|
|
|
Because reprocessing outputs for every .webidl on every invocation
|
|
is expensive, we only regenerate the minimal set of files on every
|
|
invocation. The rules for deciding what needs done are roughly as
|
|
follows:
|
|
|
|
1. If any .webidl changes, reparse all .webidl files and regenerate
|
|
the global derived files. Only regenerate output files (.h/.cpp)
|
|
impacted by the modified .webidl files.
|
|
2. If an non-.webidl dependency (Python files, config file) changes,
|
|
assume everything is out of date and regenerate the world. This
|
|
is because changes in those could globally impact every output
|
|
file.
|
|
3. If an output file is missing, ensure it is present by performing
|
|
necessary regeneration.
|
|
"""
|
|
# Despite #1 above, we assume the build system is smart enough to not
|
|
# invoke us if nothing has changed. Therefore, any invocation means
|
|
# something has changed. And, if anything has changed, we need to
|
|
# parse the WebIDL.
|
|
self._parse_webidl()
|
|
|
|
result = BuildResult()
|
|
|
|
# If we parse, we always update globals - they are cheap and it is
|
|
# easier that way.
|
|
created, updated, unchanged = self._write_global_derived()
|
|
result.created |= created
|
|
result.updated |= updated
|
|
result.unchanged |= unchanged
|
|
|
|
# If any of the extra dependencies changed, regenerate the world.
|
|
global_changed, global_hashes = self._global_dependencies_changed()
|
|
if global_changed:
|
|
# Make a copy because we may modify.
|
|
changed_inputs = set(self._input_paths)
|
|
else:
|
|
changed_inputs = self._compute_changed_inputs()
|
|
|
|
self._state['global_depends'] = global_hashes
|
|
|
|
# Generate bindings from .webidl files.
|
|
for filename in sorted(changed_inputs):
|
|
basename = mozpath.basename(filename)
|
|
result.inputs.add(filename)
|
|
written, deps = self._generate_build_files_for_webidl(filename)
|
|
result.created |= written[0]
|
|
result.updated |= written[1]
|
|
result.unchanged |= written[2]
|
|
|
|
self._state['webidls'][basename] = dict(
|
|
filename=filename,
|
|
outputs=written[0] | written[1] | written[2],
|
|
inputs=set(deps),
|
|
sha1=self._input_hashes[filename],
|
|
)
|
|
|
|
# Process some special interfaces required for testing.
|
|
for interface in self._example_interfaces:
|
|
written = self.generate_example_files(interface)
|
|
result.created |= written[0]
|
|
result.updated |= written[1]
|
|
result.unchanged |= written[2]
|
|
|
|
# Generate a make dependency file.
|
|
if self._make_deps_path:
|
|
mk = Makefile()
|
|
codegen_rule = mk.create_rule([self._make_deps_target])
|
|
codegen_rule.add_dependencies(global_hashes.keys())
|
|
codegen_rule.add_dependencies(self._input_paths)
|
|
|
|
with FileAvoidWrite(self._make_deps_path) as fh:
|
|
mk.dump(fh)
|
|
|
|
self._save_state()
|
|
|
|
return result
|
|
|
|
def generate_example_files(self, interface):
|
|
"""Generates example files for a given interface."""
|
|
from Codegen import CGExampleRoot
|
|
|
|
root = CGExampleRoot(self.config, interface)
|
|
|
|
return self._maybe_write_codegen(root, *self._example_paths(interface))
|
|
|
|
def _parse_webidl(self):
|
|
import WebIDL
|
|
from Configuration import Configuration
|
|
|
|
self.log(logging.INFO, 'webidl_parse',
|
|
{'count': len(self._input_paths)},
|
|
'Parsing {count} WebIDL files.')
|
|
|
|
hashes = {}
|
|
parser = WebIDL.Parser(self._cache_dir)
|
|
|
|
for path in sorted(self._input_paths):
|
|
with open(path, 'rb') as fh:
|
|
data = fh.read()
|
|
hashes[path] = hashlib.sha1(data).hexdigest()
|
|
parser.parse(data, path)
|
|
|
|
self._parser_results = parser.finish()
|
|
self._config = Configuration(self._config_path, self._parser_results,
|
|
self._generated_events_stems_as_array)
|
|
self._input_hashes = hashes
|
|
|
|
def _write_global_derived(self):
|
|
from Codegen import GlobalGenRoots
|
|
|
|
things = [('declare', f) for f in self.GLOBAL_DECLARE_FILES]
|
|
things.extend(('define', f) for f in self.GLOBAL_DEFINE_FILES)
|
|
|
|
result = (set(), set(), set())
|
|
|
|
for what, filename in things:
|
|
stem = mozpath.splitext(filename)[0]
|
|
root = getattr(GlobalGenRoots, stem)(self._config)
|
|
|
|
if what == 'declare':
|
|
code = root.declare()
|
|
output_root = self._exported_header_dir
|
|
elif what == 'define':
|
|
code = root.define()
|
|
output_root = self._codegen_dir
|
|
else:
|
|
raise Exception('Unknown global gen type: %s' % what)
|
|
|
|
output_path = mozpath.join(output_root, filename)
|
|
self._maybe_write_file(output_path, code, result)
|
|
|
|
return result
|
|
|
|
def _compute_changed_inputs(self):
|
|
"""Compute the set of input files that need to be regenerated."""
|
|
changed_inputs = set()
|
|
expected_outputs = self.expected_build_output_files()
|
|
|
|
# Look for missing output files.
|
|
if any(not os.path.exists(f) for f in expected_outputs):
|
|
# FUTURE Bug 940469 Only regenerate minimum set.
|
|
changed_inputs |= self._input_paths
|
|
|
|
# That's it for examining output files. We /could/ examine SHA-1's of
|
|
# output files from a previous run to detect modifications. But that's
|
|
# a lot of extra work and most build systems don't do that anyway.
|
|
|
|
# Now we move on to the input files.
|
|
old_hashes = {v['filename']: v['sha1']
|
|
for v in self._state['webidls'].values()}
|
|
|
|
old_filenames = set(old_hashes.keys())
|
|
new_filenames = self._input_paths
|
|
|
|
# If an old file has disappeared or a new file has arrived, mark
|
|
# it.
|
|
changed_inputs |= old_filenames ^ new_filenames
|
|
|
|
# For the files in common between runs, compare content. If the file
|
|
# has changed, mark it. We don't need to perform mtime comparisons
|
|
# because content is a stronger validator.
|
|
for filename in old_filenames & new_filenames:
|
|
if old_hashes[filename] != self._input_hashes[filename]:
|
|
changed_inputs.add(filename)
|
|
|
|
# We've now populated the base set of inputs that have changed.
|
|
|
|
# Inherit dependencies from previous run. The full set of dependencies
|
|
# is associated with each record, so we don't need to perform any fancy
|
|
# graph traversal.
|
|
for v in self._state['webidls'].values():
|
|
if any(dep for dep in v['inputs'] if dep in changed_inputs):
|
|
changed_inputs.add(v['filename'])
|
|
|
|
# Only use paths that are known to our current state.
|
|
# This filters out files that were deleted or changed type (e.g. from
|
|
# static to preprocessed).
|
|
return changed_inputs & self._input_paths
|
|
|
|
def _binding_info(self, p):
|
|
"""Compute binding metadata for an input path.
|
|
|
|
Returns a tuple of:
|
|
|
|
(stem, binding_stem, is_event, output_files)
|
|
|
|
output_files is itself a tuple. The first two items are the binding
|
|
header and C++ paths, respectively. The 2nd pair are the event header
|
|
and C++ paths or None if this isn't an event binding.
|
|
"""
|
|
basename = mozpath.basename(p)
|
|
stem = mozpath.splitext(basename)[0]
|
|
binding_stem = '%sBinding' % stem
|
|
|
|
if stem in self._exported_stems:
|
|
header_dir = self._exported_header_dir
|
|
else:
|
|
header_dir = self._codegen_dir
|
|
|
|
is_event = stem in self._generated_events_stems
|
|
|
|
files = (
|
|
mozpath.join(header_dir, '%s.h' % binding_stem),
|
|
mozpath.join(self._codegen_dir, '%s.cpp' % binding_stem),
|
|
mozpath.join(header_dir, '%s.h' % stem) if is_event else None,
|
|
mozpath.join(self._codegen_dir, '%s.cpp' % stem) if is_event else None,
|
|
)
|
|
|
|
return stem, binding_stem, is_event, header_dir, files
|
|
|
|
def _example_paths(self, interface):
|
|
return (
|
|
mozpath.join(self._codegen_dir, '%s-example.h' % interface),
|
|
mozpath.join(self._codegen_dir, '%s-example.cpp' % interface))
|
|
|
|
def expected_build_output_files(self):
|
|
"""Obtain the set of files generate_build_files() should write."""
|
|
paths = set()
|
|
|
|
# Account for global generation.
|
|
for p in self.GLOBAL_DECLARE_FILES:
|
|
paths.add(mozpath.join(self._exported_header_dir, p))
|
|
for p in self.GLOBAL_DEFINE_FILES:
|
|
paths.add(mozpath.join(self._codegen_dir, p))
|
|
|
|
for p in self._input_paths:
|
|
stem, binding_stem, is_event, header_dir, files = self._binding_info(p)
|
|
paths |= {f for f in files if f}
|
|
|
|
for interface in self._example_interfaces:
|
|
for p in self._example_paths(interface):
|
|
paths.add(p)
|
|
|
|
return paths
|
|
|
|
def _generate_build_files_for_webidl(self, filename):
|
|
from Codegen import (
|
|
CGBindingRoot,
|
|
CGEventRoot,
|
|
)
|
|
|
|
self.log(logging.INFO, 'webidl_generate_build_for_input',
|
|
{'filename': filename},
|
|
'Generating WebIDL files derived from {filename}')
|
|
|
|
stem, binding_stem, is_event, header_dir, files = self._binding_info(filename)
|
|
root = CGBindingRoot(self._config, binding_stem, filename)
|
|
|
|
result = self._maybe_write_codegen(root, files[0], files[1])
|
|
|
|
if is_event:
|
|
generated_event = CGEventRoot(self._config, stem)
|
|
result = self._maybe_write_codegen(generated_event, files[2],
|
|
files[3], result)
|
|
|
|
return result, root.deps()
|
|
|
|
def _global_dependencies_changed(self):
|
|
"""Determine whether the global dependencies have changed."""
|
|
current_files = set(iter_modules_in_path(mozpath.dirname(__file__)))
|
|
|
|
# We need to catch other .py files from /dom/bindings. We assume these
|
|
# are in the same directory as the config file.
|
|
current_files |= set(iter_modules_in_path(mozpath.dirname(self._config_path)))
|
|
|
|
current_files.add(self._config_path)
|
|
|
|
current_hashes = {}
|
|
for f in current_files:
|
|
# This will fail if the file doesn't exist. If a current global
|
|
# dependency doesn't exist, something else is wrong.
|
|
with open(f, 'rb') as fh:
|
|
current_hashes[f] = hashlib.sha1(fh.read()).hexdigest()
|
|
|
|
# The set of files has changed.
|
|
if current_files ^ set(self._state['global_depends'].keys()):
|
|
return True, current_hashes
|
|
|
|
# Compare hashes.
|
|
for f, sha1 in current_hashes.items():
|
|
if sha1 != self._state['global_depends'][f]:
|
|
return True, current_hashes
|
|
|
|
return False, current_hashes
|
|
|
|
def _save_state(self):
|
|
with open(self._state_path, 'wb') as fh:
|
|
self._state.dump(fh)
|
|
|
|
def _maybe_write_codegen(self, obj, declare_path, define_path, result=None):
|
|
assert declare_path and define_path
|
|
if not result:
|
|
result = (set(), set(), set())
|
|
|
|
self._maybe_write_file(declare_path, obj.declare(), result)
|
|
self._maybe_write_file(define_path, obj.define(), result)
|
|
|
|
return result
|
|
|
|
def _maybe_write_file(self, path, content, result):
|
|
fh = FileAvoidWrite(path)
|
|
fh.write(content)
|
|
existed, updated = fh.close()
|
|
|
|
if not existed:
|
|
result[0].add(path)
|
|
elif updated:
|
|
result[1].add(path)
|
|
else:
|
|
result[2].add(path)
|
|
|
|
|
|
def create_build_system_manager(topsrcdir, topobjdir, dist_dir):
|
|
"""Create a WebIDLCodegenManager for use by the build system."""
|
|
src_dir = os.path.join(topsrcdir, 'dom', 'bindings')
|
|
obj_dir = os.path.join(topobjdir, 'dom', 'bindings')
|
|
|
|
with open(os.path.join(obj_dir, 'file-lists.json'), 'rb') as fh:
|
|
files = json.load(fh)
|
|
|
|
inputs = (files['webidls'], files['exported_stems'],
|
|
files['generated_events_stems'], files['example_interfaces'])
|
|
|
|
cache_dir = os.path.join(obj_dir, '_cache')
|
|
try:
|
|
os.makedirs(cache_dir)
|
|
except OSError as e:
|
|
if e.errno != errno.EEXIST:
|
|
raise
|
|
|
|
return WebIDLCodegenManager(
|
|
os.path.join(src_dir, 'Bindings.conf'),
|
|
inputs,
|
|
os.path.join(dist_dir, 'include', 'mozilla', 'dom'),
|
|
obj_dir,
|
|
os.path.join(obj_dir, 'codegen.json'),
|
|
cache_dir=cache_dir,
|
|
# The make rules include a codegen.pp file containing dependencies.
|
|
make_deps_path=os.path.join(obj_dir, 'codegen.pp'),
|
|
make_deps_target='codegen.pp',
|
|
)
|
|
|
|
|
|
class BuildSystemWebIDL(MozbuildObject):
|
|
@property
|
|
def manager(self):
|
|
if not hasattr(self, '_webidl_manager'):
|
|
self._webidl_manager = create_build_system_manager(
|
|
self.topsrcdir, self.topobjdir, self.distdir)
|
|
|
|
return self._webidl_manager
|