OLD | NEW |
(Empty) | |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. |
| 4 |
| 5 import argparse |
| 6 import copy |
| 7 from datetime import datetime |
| 8 import os |
| 9 |
| 10 from code import Code |
| 11 import json_parse |
| 12 |
| 13 # The template for the header file of the generated FeatureProvider. |
| 14 HEADER_FILE_TEMPLATE = """ |
| 15 // Copyright %(year)s The Chromium Authors. All rights reserved. |
| 16 // Use of this source code is governed by a BSD-style license that can be |
| 17 // found in the LICENSE file. |
| 18 |
| 19 // GENERATED FROM THE FEATURES FILE: |
| 20 // %(source_files)s |
| 21 // DO NOT EDIT. |
| 22 |
| 23 #ifndef %(header_guard)s |
| 24 #define %(header_guard)s |
| 25 |
| 26 #include "extensions/common/features/base_feature_provider.h" |
| 27 |
| 28 namespace extensions { |
| 29 |
| 30 class %(provider_class)s : public BaseFeatureProvider { |
| 31 public: |
| 32 %(provider_class)s(); |
| 33 ~%(provider_class)s() override; |
| 34 |
| 35 private: |
| 36 DISALLOW_COPY_AND_ASSIGN(%(provider_class)s); |
| 37 }; |
| 38 |
| 39 } // namespace extensions |
| 40 |
| 41 #endif // %(header_guard)s |
| 42 """ |
| 43 |
| 44 # The beginning of the .cc file for the generated FeatureProvider. |
| 45 CC_FILE_BEGIN = """ |
| 46 // Copyright %(year)s The Chromium Authors. All rights reserved. |
| 47 // Use of this source code is governed by a BSD-style license that can be |
| 48 // found in the LICENSE file. |
| 49 |
| 50 // GENERATED FROM THE FEATURES FILE: |
| 51 // %(source_files)s |
| 52 // DO NOT EDIT. |
| 53 |
| 54 #include "%(header_file_path)s" |
| 55 |
| 56 #include "extensions/common/features/api_feature.h" |
| 57 #include "extensions/common/features/complex_feature.h" |
| 58 |
| 59 namespace extensions { |
| 60 |
| 61 """ |
| 62 |
| 63 # The end of the .cc file for the generated FeatureProvider. |
| 64 CC_FILE_END = """ |
| 65 %(provider_class)s::~%(provider_class)s() {} |
| 66 |
| 67 } // namespace extensions |
| 68 """ |
| 69 |
| 70 # A "grammar" for what is and isn't allowed in the features.json files. This |
| 71 # grammar has to list all possible keys and the requirements for each. The |
| 72 # format of each entry is: |
| 73 # 'key': { |
| 74 # allowed_type_1: optional_properties, |
| 75 # allowed_type_2: optional_properties, |
| 76 # } |
| 77 # |allowed_types| are the types of values that can be used for a given key. The |
| 78 # possible values are list, unicode, bool, and int. |
| 79 # |optional_properties| provide more restrictions on the given type. The options |
| 80 # are: |
| 81 # 'subtype': Only applicable for lists. If provided, this enforces that each |
| 82 # entry in the list is of the specified type. |
| 83 # 'enum_map': A map of strings to C++ enums. When the compiler sees the given |
| 84 # enum string, it will replace it with the C++ version in the |
| 85 # compiled code. For instance, if a feature specifies |
| 86 # 'channel': 'stable', the generated C++ will assign |
| 87 # version_info::Channel::STABLE to channel. The keys in this map |
| 88 # also serve as a list all of possible values. |
| 89 # 'allow_all': Only applicable for lists. If present, this will check for |
| 90 # a value of "all" for a list value, and will replace it with |
| 91 # the collection of all possible values. For instance, if a |
| 92 # feature specifies 'contexts': 'all', the generated C++ will |
| 93 # assign the list of Feature::BLESSED_EXTENSION_CONTEXT, |
| 94 # Feature::BLESSED_WEB_PAGE_CONTEXT et al for contexts. If not |
| 95 # specified, defaults to false. |
| 96 # 'values': A list of all possible allowed values for a given key. |
| 97 # If a type definition does not have any restrictions (beyond the type itself), |
| 98 # an empty definition ({}) is used. |
| 99 FEATURE_GRAMMAR = ( |
| 100 { |
| 101 'blacklist': { |
| 102 list: {'subtype': unicode} |
| 103 }, |
| 104 'channel': { |
| 105 unicode: { |
| 106 'enum_map': { |
| 107 'trunk': 'version_info::Channel::UNKNOWN', |
| 108 'canary': 'version_info::Channel::CANARY', |
| 109 'dev': 'version_info::Channel::DEV', |
| 110 'beta': 'version_info::Channel::BETA', |
| 111 'stable': 'version_info::Channel::STABLE', |
| 112 } |
| 113 } |
| 114 }, |
| 115 'command_line_switch': { |
| 116 unicode: {} |
| 117 }, |
| 118 'component_extensions_auto_granted': { |
| 119 bool: {} |
| 120 }, |
| 121 'contexts': { |
| 122 list: { |
| 123 'enum_map': { |
| 124 'blessed_extension': 'Feature::BLESSED_EXTENSION_CONTEXT', |
| 125 'blessed_web_page': 'Feature::BLESSED_WEB_PAGE_CONTEXT', |
| 126 'content_script': 'Feature::CONTENT_SCRIPT_CONTEXT', |
| 127 'extension_service_worker': 'Feature::SERVICE_WORKER_CONTEXT', |
| 128 'web_page': 'Feature::WEB_PAGE_CONTEXT', |
| 129 'webui': 'Feature::WEBUI_CONTEXT', |
| 130 'unblessed_extension': 'Feature::UNBLESSED_EXTENSION_CONTEXT', |
| 131 }, |
| 132 'allow_all': True |
| 133 }, |
| 134 }, |
| 135 'default_parent': { |
| 136 bool: {'values': [True]} |
| 137 }, |
| 138 'dependencies': { |
| 139 list: {'subtype': unicode} |
| 140 }, |
| 141 'extension_types': { |
| 142 list: { |
| 143 'enum_map': { |
| 144 'extension': 'Manifest::TYPE_EXTENSION', |
| 145 'hosted_app': 'Manifest::TYPE_HOSTED_APP', |
| 146 'legacy_packaged_app': 'Manifest::TYPE_LEGACY_PACKAGED_APP', |
| 147 'platform_app': 'Manifest::TYPE_PLATFORM_APP', |
| 148 'shared_module': 'Manifest::TYPE_SHARED_MODULE', |
| 149 'theme': 'Manifest::TYPE_THEME', |
| 150 }, |
| 151 'allow_all': True |
| 152 }, |
| 153 }, |
| 154 'location': { |
| 155 unicode: { |
| 156 'enum_map': { |
| 157 'component': 'SimpleFeature::COMPONENT_LOCATION', |
| 158 'external_component': 'SimpleFeature::EXTERNAL_COMPONENT_LOCATION', |
| 159 'policy': 'SimpleFeature::POLICY_LOCATION', |
| 160 } |
| 161 } |
| 162 }, |
| 163 'internal': { |
| 164 bool: {'values': [True]} |
| 165 }, |
| 166 'matches': { |
| 167 list: {'subtype': unicode} |
| 168 }, |
| 169 'max_manifest_version': { |
| 170 int: {'values': [1]} |
| 171 }, |
| 172 'min_manifest_version': { |
| 173 int: {'values': [2]} |
| 174 }, |
| 175 'noparent': { |
| 176 bool: {'values': [True]} |
| 177 }, |
| 178 'platforms': { |
| 179 list: { |
| 180 'enum_map': { |
| 181 'chromeos': 'Feature::CHROMEOS_PLATFORM', |
| 182 'linux': 'Feature::LINUX_PLATFORM', |
| 183 'mac': 'Feature::MACOSX_PLATFORM', |
| 184 'win': 'Feature::WIN_PLATFORM', |
| 185 } |
| 186 } |
| 187 }, |
| 188 'whitelist': { |
| 189 list: {'subtype': unicode} |
| 190 }, |
| 191 }) |
| 192 |
| 193 # These keys are used to find the parents of different features, but are not |
| 194 # compiled into the features themselves. |
| 195 IGNORED_KEYS = ['noparent', 'default_parent'] |
| 196 |
| 197 # By default, if an error is encountered, assert to stop the compilation. This |
| 198 # can be disabled for testing. |
| 199 ENABLE_ASSERTIONS = True |
| 200 |
| 201 # JSON parsing returns all strings of characters as unicode types. For testing, |
| 202 # we can enable converting all string types to unicode to avoid writing u'' |
| 203 # everywhere. |
| 204 STRINGS_TO_UNICODE = False |
| 205 |
| 206 class Feature(object): |
| 207 """A representation of a single simple feature that can handle all parsing, |
| 208 validation, and code generation. |
| 209 """ |
| 210 def __init__(self, name): |
| 211 self.name = name |
| 212 self.has_parent = False |
| 213 self.errors = [] |
| 214 self.feature_values = {} |
| 215 |
| 216 def _GetType(self, value): |
| 217 """Returns the type of the given value. This can be different than type() if |
| 218 STRINGS_TO_UNICODE is enabled. |
| 219 """ |
| 220 t = type(value) |
| 221 if not STRINGS_TO_UNICODE: |
| 222 return t |
| 223 if t is str: |
| 224 return unicode |
| 225 return t |
| 226 |
| 227 def _AddError(self, key, error): |
| 228 """Adds an error to the feature. If ENABLE_ASSERTIONS is active, this will |
| 229 also assert to stop the compilation process (since errors should never be |
| 230 found in production). |
| 231 """ |
| 232 error = 'Error parsing feature "%s" at key "%s": %s' % ( |
| 233 self.name, key, error) |
| 234 self.errors.append(error) |
| 235 if ENABLE_ASSERTIONS: |
| 236 assert False, error |
| 237 |
| 238 def _GetCheckedValue(self, key, expected_type, expected_values, |
| 239 enum_map, value): |
| 240 """Returns a string to be used in the generated C++ code for a given key's |
| 241 python value, or None if the value is invalid. For example, if the python |
| 242 value is True, this returns 'true', for a string foo, this returns "foo", |
| 243 and for an enum, this looks up the C++ definition in the enum map. |
| 244 key: The key being parsed. |
| 245 expected_type: The expected type for this value, or None if any type is |
| 246 allowed. |
| 247 expected_values: The list of allowed values for this value, or None if any |
| 248 value is allowed. |
| 249 enum_map: The map from python value -> cpp value for all allowed values, |
| 250 or None if no special mapping should be made. |
| 251 value: The value to check. |
| 252 """ |
| 253 valid = True |
| 254 if expected_values and value not in expected_values: |
| 255 self._AddError(key, 'Illegal value: "%s"' % value) |
| 256 valid = False |
| 257 |
| 258 t = self._GetType(value) |
| 259 if expected_type and t is not expected_type: |
| 260 self._AddError(key, 'Illegal value: "%s"' % value) |
| 261 valid = False |
| 262 |
| 263 if not valid: |
| 264 return None |
| 265 |
| 266 if enum_map: |
| 267 return enum_map[value] |
| 268 |
| 269 if t in [str, unicode]: |
| 270 return '"%s"' % str(value) |
| 271 if t is int: |
| 272 return str(value) |
| 273 if t is bool: |
| 274 return 'true' if value else 'false' |
| 275 assert False, 'Unsupported type: %s' % value |
| 276 |
| 277 def _ParseKey(self, key, value, grammar): |
| 278 """Parses the specific key according to the grammar rule for that key if it |
| 279 is present in the json value. |
| 280 key: The key to parse. |
| 281 value: The full value for this feature. |
| 282 grammar: The rule for the specific key. |
| 283 """ |
| 284 if key not in value: |
| 285 return |
| 286 v = value[key] |
| 287 |
| 288 is_all = False |
| 289 if v == 'all' and list in grammar and 'allow_all' in grammar[list]: |
| 290 v = [] |
| 291 is_all = True |
| 292 |
| 293 value_type = self._GetType(v) |
| 294 if value_type not in grammar: |
| 295 self._AddError(key, 'Illegal value: "%s"' % v) |
| 296 return |
| 297 |
| 298 expected = grammar[value_type] |
| 299 expected_values = None |
| 300 enum_map = None |
| 301 if 'values' in expected: |
| 302 expected_values = expected['values'] |
| 303 elif 'enum_map' in expected: |
| 304 enum_map = expected['enum_map'] |
| 305 expected_values = enum_map.keys() |
| 306 |
| 307 if is_all: |
| 308 v = copy.deepcopy(expected_values) |
| 309 |
| 310 expected_type = None |
| 311 if value_type is list and 'subtype' in expected: |
| 312 expected_type = expected['subtype'] |
| 313 |
| 314 cpp_value = None |
| 315 # If this value is a list, iterate over each entry and validate. Otherwise, |
| 316 # validate the single value. |
| 317 if value_type is list: |
| 318 cpp_value = [] |
| 319 for sub_value in v: |
| 320 cpp_sub_value = self._GetCheckedValue(key, expected_type, |
| 321 expected_values, enum_map, |
| 322 sub_value) |
| 323 if cpp_sub_value: |
| 324 cpp_value.append(cpp_sub_value) |
| 325 if cpp_value: |
| 326 cpp_value = '{' + ','.join(cpp_value) + '}' |
| 327 else: |
| 328 cpp_value = self._GetCheckedValue(key, expected_type, expected_values, |
| 329 enum_map, v) |
| 330 |
| 331 if cpp_value: |
| 332 self.feature_values[key] = cpp_value |
| 333 |
| 334 def SetParent(self, parent): |
| 335 """Sets the parent of this feature, and inherits all properties from that |
| 336 parent. |
| 337 """ |
| 338 assert not self.feature_values, 'Parents must be set before parsing' |
| 339 self.feature_values = copy.deepcopy(parent.feature_values) |
| 340 self.has_parent = True |
| 341 |
| 342 def Parse(self, parsed_json): |
| 343 """Parses the feature from the given json value.""" |
| 344 for key in parsed_json.keys(): |
| 345 if key not in FEATURE_GRAMMAR: |
| 346 self._AddError(key, 'Unrecognized key') |
| 347 for key, key_grammar in FEATURE_GRAMMAR.iteritems(): |
| 348 self._ParseKey(key, parsed_json, key_grammar) |
| 349 |
| 350 def GetCode(self, feature_class): |
| 351 """Returns the Code object for generating this feature.""" |
| 352 c = Code() |
| 353 c.Append('std::unique_ptr<%s> feature(new %s());' % |
| 354 (feature_class, feature_class)) |
| 355 c.Append('feature->set_name("%s");' % self.name) |
| 356 for key in sorted(self.feature_values.keys()): |
| 357 if key in IGNORED_KEYS: |
| 358 continue; |
| 359 c.Append('feature->set_%s(%s);' % (key, self.feature_values[key])) |
| 360 return c |
| 361 |
| 362 class FeatureCompiler(object): |
| 363 """A compiler to load, parse, and generate C++ code for a number of |
| 364 features.json files.""" |
| 365 def __init__(self, chrome_root, source_files, feature_class, |
| 366 provider_class, out_root, out_base_filename): |
| 367 # See __main__'s ArgumentParser for documentation on these properties. |
| 368 self._chrome_root = chrome_root |
| 369 self._source_files = source_files |
| 370 self._feature_class = feature_class |
| 371 self._provider_class = provider_class |
| 372 self._out_root = out_root |
| 373 self._out_base_filename = out_base_filename |
| 374 |
| 375 # The json value for the feature files. |
| 376 self._json = {} |
| 377 # The parsed features. |
| 378 self._features = {} |
| 379 |
| 380 def _Load(self): |
| 381 """Loads and parses the source from each input file and puts the result in |
| 382 self._json.""" |
| 383 for f in self._source_files: |
| 384 abs_source_file = os.path.join(self._chrome_root, f) |
| 385 try: |
| 386 with open(abs_source_file, 'r') as f: |
| 387 f_json = json_parse.Parse(f.read()) |
| 388 except: |
| 389 print('FAILED: Exception encountered while loading "%s"' % |
| 390 abs_source_file) |
| 391 raise |
| 392 dupes = set(f_json) & set(self._json) |
| 393 assert not dupes, 'Duplicate keys found: %s' % list(dupes) |
| 394 self._json.update(f_json) |
| 395 |
| 396 def _FindParent(self, feature_name, feature_value): |
| 397 """Checks to see if a feature has a parent. If it does, returns the |
| 398 parent.""" |
| 399 sep = feature_name.rfind('.') |
| 400 if sep is -1 or 'noparent' in feature_value: |
| 401 return None |
| 402 parent_name = feature_name[:sep] |
| 403 if parent_name not in self._features: |
| 404 # TODO(devlin): It'd be kind of nice to be able to assert that the |
| 405 # deduced parent name is in our features, but some dotted features don't |
| 406 # have parents and also don't have noparent, e.g. system.cpu. We should |
| 407 # probably just noparent them so that we can assert this. |
| 408 # raise KeyError('Could not find parent "%s" for feature "%s".' % |
| 409 # (parent_name, feature_name)) |
| 410 return None |
| 411 parent_value = self._features[parent_name] |
| 412 parent = parent_value |
| 413 if type(parent_value) is list: |
| 414 for p in parent_value: |
| 415 if 'default_parent' in p.feature_values: |
| 416 parent = p |
| 417 break |
| 418 assert parent, 'No default parent found for %s' % parent_name |
| 419 return parent |
| 420 |
| 421 def _CompileFeature(self, feature_name, feature_value): |
| 422 """Parses a single feature.""" |
| 423 if 'nocompile' in feature_value: |
| 424 assert feature_value['nocompile'], ( |
| 425 'nocompile should only be true; otherwise omit this key.') |
| 426 return |
| 427 parent = self._FindParent(feature_name, feature_value) |
| 428 # Handle complex features, which are lists of simple features. |
| 429 if type(feature_value) is list: |
| 430 feature_list = [] |
| 431 # This doesn't handle nested complex features. I think that's probably for |
| 432 # the best. |
| 433 for v in feature_value: |
| 434 feature = Feature(feature_name) |
| 435 if parent: |
| 436 feature.SetParent(parent) |
| 437 feature.Parse(v) |
| 438 feature_list.append(feature) |
| 439 self._features[feature_name] = feature_list |
| 440 return |
| 441 |
| 442 feature = Feature(feature_name) |
| 443 if parent: |
| 444 feature.SetParent(parent) |
| 445 feature.Parse(feature_value) |
| 446 self._features[feature_name] = feature |
| 447 |
| 448 def Compile(self): |
| 449 """Parses all features after loading the input files.""" |
| 450 self._Load(); |
| 451 # Iterate over in sorted order so that parents come first. |
| 452 for k in sorted(self._json.keys()): |
| 453 self._CompileFeature(k, self._json[k]) |
| 454 |
| 455 def Render(self): |
| 456 """Returns the Code object for the body of the .cc file, which handles the |
| 457 initialization of all features.""" |
| 458 c = Code() |
| 459 c.Append('%s::%s() {' % (self._provider_class, self._provider_class)) |
| 460 c.Sblock() |
| 461 for k in sorted(self._features.keys()): |
| 462 c.Sblock('{') |
| 463 feature = self._features[k] |
| 464 if type(feature) is list: |
| 465 c.Append('std::unique_ptr<ComplexFeature::FeatureList> features(') |
| 466 c.Append(' new ComplexFeature::FeatureList());') |
| 467 for f in feature: |
| 468 c.Sblock('{') |
| 469 c.Concat(f.GetCode(self._feature_class)) |
| 470 c.Append('features->push_back(std::move(feature));') |
| 471 c.Eblock('}') |
| 472 c.Append('std::unique_ptr<ComplexFeature> feature(') |
| 473 c.Append(' new ComplexFeature(std::move(features)));') |
| 474 c.Append('feature->set_name("%s");' % k) |
| 475 else: |
| 476 c.Concat(feature.GetCode(self._feature_class)) |
| 477 c.Append('features_["%s"] = std::move(feature);' % k) |
| 478 c.Eblock('}') |
| 479 c.Eblock('}') |
| 480 return c |
| 481 |
| 482 def Write(self): |
| 483 """Writes the output.""" |
| 484 header_file_path = self._out_base_filename + '.h' |
| 485 cc_file_path = self._out_base_filename + '.cc' |
| 486 substitutions = ({ |
| 487 'header_file_path': header_file_path, |
| 488 'header_guard': (header_file_path.replace('/', '_'). |
| 489 replace('.', '_').upper()), |
| 490 'provider_class': self._provider_class, |
| 491 'source_files': str(self._source_files), |
| 492 'year': str(datetime.now().year) |
| 493 }) |
| 494 if not os.path.exists(self._out_root): |
| 495 os.makedirs(self._out_root) |
| 496 # Write the .h file. |
| 497 with open(os.path.join(self._out_root, header_file_path), 'w') as f: |
| 498 header_file = Code() |
| 499 header_file.Append(HEADER_FILE_TEMPLATE) |
| 500 header_file.Substitute(substitutions) |
| 501 f.write(header_file.Render().strip()) |
| 502 # Write the .cc file. |
| 503 with open(os.path.join(self._out_root, cc_file_path), 'w') as f: |
| 504 cc_file = Code() |
| 505 cc_file.Append(CC_FILE_BEGIN) |
| 506 cc_file.Substitute(substitutions) |
| 507 cc_file.Concat(self.Render()) |
| 508 cc_end = Code() |
| 509 cc_end.Append(CC_FILE_END) |
| 510 cc_end.Substitute(substitutions) |
| 511 cc_file.Concat(cc_end) |
| 512 f.write(cc_file.Render().strip()) |
| 513 |
| 514 if __name__ == '__main__': |
| 515 parser = argparse.ArgumentParser(description='Compile json feature files') |
| 516 parser.add_argument('chrome_root', type=str, |
| 517 help='The root directory of the chrome checkout') |
| 518 parser.add_argument( |
| 519 'feature_class', type=str, |
| 520 help='The name of the class to use in feature generation ' + |
| 521 '(e.g. APIFeature, PermissionFeature)') |
| 522 parser.add_argument('provider_class', type=str, |
| 523 help='The name of the class for the feature provider') |
| 524 parser.add_argument('out_root', type=str, |
| 525 help='The root directory to generate the C++ files into') |
| 526 parser.add_argument( |
| 527 'out_base_filename', type=str, |
| 528 help='The base filename for the C++ files (.h and .cc will be appended)') |
| 529 parser.add_argument('source_files', type=str, nargs='+', |
| 530 help='The source features.json files') |
| 531 args = parser.parse_args() |
| 532 c = FeatureCompiler(args.chrome_root, args.source_files, args.feature_class, |
| 533 args.provider_class, args.out_root, |
| 534 args.out_base_filename) |
| 535 c.Compile() |
| 536 c.Write() |
OLD | NEW |