chromium/third_party/blink/renderer/build/scripts/make_runtime_features.py

#!/usr/bin/env python
# Copyright (C) 2013 Google Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#     * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
#     * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import copy
import os
import sys

if sys.version_info.major == 2:
    import cPickle as pickle
else:
    import pickle

from blinkbuild.name_style_converter import NameStyleConverter
import make_runtime_features_utilities as util
import json5_generator
import template_expander


class BaseRuntimeFeatureWriter(json5_generator.Writer):
    # |class_name| should be passed as a template input to generate the target
    # class. Set this variable if the template generates a class.
    class_name = None
    # |file_basename| must be set by subclasses since it is used to generate
    # the header guard.
    file_basename = None

    def __init__(self, json5_file_path, output_dir):
        super(BaseRuntimeFeatureWriter, self).__init__(json5_file_path,
                                                       output_dir)
        # Subclasses should add generated output files and their contents to this dict.
        self._outputs = {}
        assert self.file_basename

        self._features = self.json5_file.name_dictionaries
        origin_trial_set = util.origin_trials(self._features)

        # Make sure the resulting dictionaries have all the keys we expect.
        for feature in self._features:
            feature['in_origin_trial'] = str(
                feature['name']) in origin_trial_set
            feature['data_member_name'] = self._data_member_name(
                feature['name'])
            # If 'status' is a dict, add the values for all the not-mentioned platforms too.
            if isinstance(feature['status'], dict):
                feature['status'] = self._status_with_all_platforms(
                    feature['status'])
            # Specify the type of status
            feature['status_type'] = "dict" if isinstance(
                feature['status'], dict) else "str"
            if feature['base_feature'] == 'none':
                feature['base_feature'] = ''
            elif feature['base_feature'] == '':
                feature['base_feature'] = feature['name']

        self._origin_trial_features = [
            feature for feature in self._features if feature['in_origin_trial']
        ]
        self._header_guard = self.make_header_guard(self._relative_output_dir +
                                                    self.file_basename + '.h')

    @staticmethod
    def _data_member_name(str_or_converter):
        converter = NameStyleConverter(str_or_converter) if type(
            str_or_converter) is str else str_or_converter
        return converter.to_class_data_member(prefix='is', suffix='enabled')

    def _feature_sets(self):
        # Another way to think of the status levels is as "sets of features"
        # which is how we're referring to them in this generator.
        return self.json5_file.parameters['status']['valid_values']

    def _status_with_all_platforms(self, status):
        new_status = copy.deepcopy(status)
        default = new_status['default'] if 'default' in new_status else ''
        new_status['default'] = default
        for platform in self._platforms():
            if platform not in new_status:
                new_status[platform] = default
        return new_status

    def _platforms(self):
        # Remove all occurrences of 'default' from 'valid_keys'
        platforms = self.json5_file.parameters['status']['valid_keys']
        return [platform for platform in platforms if platform != 'default']


class RuntimeFeatureWriter(BaseRuntimeFeatureWriter):
    class_name = 'RuntimeEnabledFeatures'
    file_basename = 'runtime_enabled_features'

    def __init__(self, json5_file_path, output_dir):
        super(RuntimeFeatureWriter, self).__init__(json5_file_path, output_dir)
        self._outputs = {
            (self.file_basename + '.h'):
            self.generate_header,
            (self.file_basename + '.cc'):
            self.generate_implementation,
            ('exported/web_runtime_features_base.cc'):
            self.generate_web_implementation,
        }

        # Write features to file for bindings generation
        self._write_features_to_pickle_file(output_dir)
        self._overridable_features = util.overridable_features(self._features)

        overridable_set = set()
        for feature in self._overridable_features:
            overridable_set.add(str(feature['name']))

        for feature in self._features:
            feature['is_overridable_feature'] = str(
                feature['name']) in overridable_set

    def _write_features_to_pickle_file(self, platform_output_dir):
        # TODO(yashard): Get the file path from args instead of hardcoding it.
        file_name = os.path.join(platform_output_dir, '..', 'build', 'scripts',
                                 'runtime_enabled_features.pickle')
        features_map = {}
        for feature in self._features:
            features_map[str(feature['name'])] = {
                'in_origin_trial': feature['in_origin_trial']
            }
        if os.path.isfile(file_name):
            with open(os.path.abspath(file_name)) as pickle_file:
                # pylint: disable=broad-except
                try:
                    if pickle.load(pickle_file) == features_map:
                        return
                except Exception:
                    # If trouble unpickling, overwrite
                    pass
        with open(os.path.abspath(file_name), 'wb') as pickle_file:
            pickle.dump(features_map, pickle_file)

    def _template_inputs(self):
        return {
            'features': self._features,
            'feature_sets': self._feature_sets(),
            'platforms': self._platforms(),
            'input_files': self._input_files,
            'origin_trial_controlled_features': self._origin_trial_features,
            'header_guard': self._header_guard,
        }

    @template_expander.use_jinja('templates/' + file_basename + '.h.tmpl')
    def generate_header(self):
        return self._template_inputs()

    @template_expander.use_jinja('templates/' + file_basename + '.cc.tmpl')
    def generate_implementation(self):
        return self._template_inputs()

    @template_expander.use_jinja('templates/web_runtime_features_base.cc.tmpl')
    def generate_web_implementation(self):
        return self._template_inputs()


class RuntimeFeatureTestHelpersWriter(BaseRuntimeFeatureWriter):
    class_name = 'ScopedRuntimeEnabledFeatureForTest'
    file_basename = 'runtime_enabled_features_test_helpers'

    def __init__(self, json5_file_path, output_dir):
        super(RuntimeFeatureTestHelpersWriter, self).__init__(
            json5_file_path, output_dir)
        self._outputs = {
            ('testing/' + self.file_basename + '.h'): self.generate_header
        }

    def _template_inputs(self):
        return {
            'features': self._features,
            'input_files': self._input_files,
            'header_guard': self._header_guard,
        }

    @template_expander.use_jinja('templates/' + file_basename + '.h.tmpl')
    def generate_header(self):
        return self._template_inputs()


if __name__ == '__main__':
    json5_generator.Maker(RuntimeFeatureWriter).main()
    json5_generator.Maker(RuntimeFeatureTestHelpersWriter).main()