availability_finder.py revision 5f1c94371a64b3196d4be9466099bb892df9b88e
1# Copyright 2013 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 5import posixpath 6 7from api_models import GetNodeCategories 8from api_schema_graph import APISchemaGraph 9from branch_utility import BranchUtility, ChannelInfo 10from compiled_file_system import CompiledFileSystem, SingleFile, Unicode 11from extensions_paths import API_PATHS, JSON_TEMPLATES 12from features_bundle import FeaturesBundle 13from file_system import FileNotFoundError 14from schema_util import ProcessSchema 15from third_party.json_schema_compiler.memoize import memoize 16from third_party.json_schema_compiler.model import UnixName 17 18 19_DEVTOOLS_API = 'devtools_api.json' 20_EXTENSION_API = 'extension_api.json' 21# The version where api_features.json is first available. 22_API_FEATURES_MIN_VERSION = 28 23# The version where permission_ and manifest_features.json are available and 24# presented in the current format. 25_ORIGINAL_FEATURES_MIN_VERSION = 20 26# API schemas are aggregated in extension_api.json up to this version. 27_EXTENSION_API_MAX_VERSION = 17 28# The earliest version for which we have SVN data. 29_SVN_MIN_VERSION = 5 30 31 32def _GetChannelFromFeatures(api_name, features): 33 '''Finds API channel information for |api_name| from |features|. 34 Returns None if channel information for the API cannot be located. 35 ''' 36 feature = features.Get().get(api_name) 37 return feature.get('channel') if feature else None 38 39 40def _GetChannelFromAPIFeatures(api_name, features_bundle): 41 return _GetChannelFromFeatures(api_name, features_bundle.GetAPIFeatures()) 42 43 44def _GetChannelFromManifestFeatures(api_name, features_bundle): 45 # _manifest_features.json uses unix_style API names. 46 api_name = UnixName(api_name) 47 return _GetChannelFromFeatures(api_name, 48 features_bundle.GetManifestFeatures()) 49 50 51def _GetChannelFromPermissionFeatures(api_name, features_bundle): 52 return _GetChannelFromFeatures(api_name, 53 features_bundle.GetPermissionFeatures()) 54 55 56def _GetAPISchemaFilename(api_name, file_system, version): 57 '''Gets the name of the file which may contain the schema for |api_name| in 58 |file_system|, or None if the API is not found. Note that this may be the 59 single _EXTENSION_API file which all APIs share in older versions of Chrome, 60 in which case it is unknown whether the API actually exists there. 61 ''' 62 if version == 'trunk' or version > _ORIGINAL_FEATURES_MIN_VERSION: 63 # API schema filenames switch format to unix_hacker_style. 64 api_name = UnixName(api_name) 65 66 # Devtools API names have 'devtools.' prepended to them. 67 # The corresponding filenames do not. 68 if 'devtools_' in api_name: 69 api_name = api_name.replace('devtools_', '') 70 71 for api_path in API_PATHS: 72 try: 73 for base, _, filenames in file_system.Walk(api_path): 74 for ext in ('json', 'idl'): 75 filename = '%s.%s' % (api_name, ext) 76 if filename in filenames: 77 return posixpath.join(api_path, base, filename) 78 if _EXTENSION_API in filenames: 79 return posixpath.join(api_path, base, _EXTENSION_API) 80 except FileNotFoundError: 81 continue 82 return None 83 84 85class AvailabilityInfo(object): 86 '''Represents availability data for an API. |scheduled| is a version number 87 specifying when dev and beta APIs will become stable, or None if that data 88 is unknown. 89 ''' 90 def __init__(self, channel_info, scheduled=None): 91 assert isinstance(channel_info, ChannelInfo) 92 assert isinstance(scheduled, int) or scheduled is None 93 self.channel_info = channel_info 94 self.scheduled = scheduled 95 96 def __eq__(self, other): 97 return self.__dict__ == other.__dict__ 98 99 def __ne__(self, other): 100 return not (self == other) 101 102 def __repr__(self): 103 return '%s%s' % (type(self).__name__, repr(self.__dict__)) 104 105 def __str__(self): 106 return repr(self) 107 108 109class AvailabilityFinder(object): 110 '''Generates availability information for APIs by looking at API schemas and 111 _features files over multiple release versions of Chrome. 112 ''' 113 114 def __init__(self, 115 branch_utility, 116 compiled_fs_factory, 117 file_system_iterator, 118 host_file_system, 119 object_store_creator, 120 platform): 121 self._branch_utility = branch_utility 122 self._compiled_fs_factory = compiled_fs_factory 123 self._file_system_iterator = file_system_iterator 124 self._host_file_system = host_file_system 125 self._object_store_creator = object_store_creator 126 def create_object_store(category): 127 return object_store_creator.Create( 128 AvailabilityFinder, category='/'.join((platform, category))) 129 self._top_level_object_store = create_object_store('top_level') 130 self._node_level_object_store = create_object_store('node_level') 131 self._json_fs = compiled_fs_factory.ForJson(self._host_file_system) 132 self._platform = platform 133 134 def _GetPredeterminedAvailability(self, api_name): 135 '''Checks a configuration file for hardcoded (i.e. predetermined) 136 availability information for an API. 137 ''' 138 api_info = self._json_fs.GetFromFile( 139 JSON_TEMPLATES + 'api_availabilities.json').Get().get(api_name) 140 if api_info is None: 141 return None 142 if api_info['channel'] == 'stable': 143 return AvailabilityInfo( 144 self._branch_utility.GetStableChannelInfo(api_info['version'])) 145 return AvailabilityInfo( 146 self._branch_utility.GetChannelInfo(api_info['channel'])) 147 148 @memoize 149 def _CreateAPISchemaFileSystem(self, file_system): 150 '''Creates a CompiledFileSystem for parsing raw JSON or IDL API schema 151 data and formatting it so that it can be used to create APISchemaGraphs. 152 ''' 153 # When processing the API schemas, we retain inlined types in the schema 154 # so that there are not missing nodes in the APISchemaGraphs when trying 155 # to lookup availability. 156 def process_schema(path, data): 157 return ProcessSchema(path, data, retain_inlined_types=True) 158 return self._compiled_fs_factory.Create(file_system, 159 SingleFile(Unicode(process_schema)), 160 CompiledFileSystem, 161 category='api-schema') 162 163 def _GetAPISchema(self, api_name, file_system, version): 164 '''Searches |file_system| for |api_name|'s API schema data, and processes 165 and returns it if found. 166 ''' 167 api_filename = _GetAPISchemaFilename(api_name, file_system, version) 168 if api_filename is None: 169 # No file for the API could be found in the given |file_system|. 170 return None 171 172 schema_fs = self._CreateAPISchemaFileSystem(file_system) 173 api_schemas = schema_fs.GetFromFile(api_filename).Get() 174 matching_schemas = [api for api in api_schemas 175 if api['namespace'] == api_name] 176 # There should only be a single matching schema per file, or zero in the 177 # case of no API data being found in _EXTENSION_API. 178 assert len(matching_schemas) <= 1 179 return matching_schemas or None 180 181 def _HasAPISchema(self, api_name, file_system, version): 182 '''Whether or not an API schema for |api_name| exists in the given 183 |file_system|. 184 ''' 185 filename = _GetAPISchemaFilename(api_name, file_system, version) 186 if filename is None: 187 return False 188 if filename.endswith(_EXTENSION_API) or filename.endswith(_DEVTOOLS_API): 189 return self._GetAPISchema(api_name, file_system, version) is not None 190 return True 191 192 def _CheckStableAvailability(self, 193 api_name, 194 file_system, 195 version, 196 earliest_version=None): 197 '''Checks for availability of an API, |api_name|, on the stable channel. 198 Considers several _features.json files, file system existence, and 199 extension_api.json depending on the given |version|. 200 |earliest_version| is the version of Chrome at which |api_name| first became 201 available. It should only be given when checking stable availability for 202 API nodes, so it can be used as an alternative to the check for filesystem 203 existence. 204 ''' 205 earliest_version = earliest_version or _SVN_MIN_VERSION 206 if version < earliest_version: 207 # SVN data isn't available below this version. 208 return False 209 features_bundle = self._CreateFeaturesBundle(file_system) 210 available_channel = None 211 if version >= _API_FEATURES_MIN_VERSION: 212 # The _api_features.json file first appears in version 28 and should be 213 # the most reliable for finding API availability. 214 available_channel = _GetChannelFromAPIFeatures(api_name, 215 features_bundle) 216 if version >= _ORIGINAL_FEATURES_MIN_VERSION: 217 # The _permission_features.json and _manifest_features.json files are 218 # present in Chrome 20 and onwards. Use these if no information could be 219 # found using _api_features.json. 220 available_channel = ( 221 available_channel or 222 _GetChannelFromPermissionFeatures(api_name, features_bundle) or 223 _GetChannelFromManifestFeatures(api_name, features_bundle)) 224 if available_channel is not None: 225 return available_channel == 'stable' 226 227 # |earliest_version| == _SVN_MIN_VERSION implies we're dealing with an API. 228 # Fall back to a check for file system existence if the API is not 229 # stable in any of the _features.json files, or if the _features files 230 # do not exist (version 19 and earlier). 231 if earliest_version == _SVN_MIN_VERSION: 232 return self._HasAPISchema(api_name, file_system, version) 233 # For API nodes, assume it's available if |version| is greater than the 234 # version the node became available (which it is, because of the first 235 # check). 236 return True 237 238 def _CheckChannelAvailability(self, api_name, file_system, channel_info): 239 '''Searches through the _features files in a given |file_system|, falling 240 back to checking the file system for API schema existence, to determine 241 whether or not an API is available on the given channel, |channel_info|. 242 ''' 243 features_bundle = self._CreateFeaturesBundle(file_system) 244 available_channel = ( 245 _GetChannelFromAPIFeatures(api_name, features_bundle) or 246 _GetChannelFromPermissionFeatures(api_name, features_bundle) or 247 _GetChannelFromManifestFeatures(api_name, features_bundle)) 248 if (available_channel is None and 249 self._HasAPISchema(api_name, file_system, channel_info.version)): 250 # If an API is not represented in any of the _features files, but exists 251 # in the filesystem, then assume it is available in this version. 252 # The chrome.windows API is an example of this. 253 available_channel = channel_info.channel 254 # If the channel we're checking is the same as or newer than the 255 # |available_channel| then the API is available at this channel. 256 newest = BranchUtility.NewestChannel((available_channel, 257 channel_info.channel)) 258 return available_channel is not None and newest == channel_info.channel 259 260 def _CheckChannelAvailabilityForNode(self, 261 node_name, 262 file_system, 263 channel_info, 264 earliest_channel_info): 265 '''Searches through the _features files in a given |file_system| to 266 determine whether or not an API node is available on the given channel, 267 |channel_info|. |earliest_channel_info| is the earliest channel the node 268 was introduced. 269 ''' 270 features_bundle = self._CreateFeaturesBundle(file_system) 271 available_channel = None 272 # Only API nodes can have their availability overriden on a per-node basis, 273 # so we only need to check _api_features.json. 274 if channel_info.version >= _API_FEATURES_MIN_VERSION: 275 available_channel = _GetChannelFromAPIFeatures(node_name, features_bundle) 276 if (available_channel is None and 277 channel_info.version >= earliest_channel_info.version): 278 # Most API nodes inherit their availabiltity from their parent, so don't 279 # explicitly appear in _api_features.json. For example, "tabs.create" 280 # isn't listed; it inherits from "tabs". Assume these are available at 281 # |channel_info|. 282 available_channel = channel_info.channel 283 newest = BranchUtility.NewestChannel((available_channel, 284 channel_info.channel)) 285 return available_channel is not None and newest == channel_info.channel 286 287 @memoize 288 def _CreateFeaturesBundle(self, file_system): 289 return FeaturesBundle(file_system, 290 self._compiled_fs_factory, 291 self._object_store_creator, 292 self._platform) 293 294 def _CheckAPIAvailability(self, api_name, file_system, channel_info): 295 '''Determines the availability for an API at a certain version of Chrome. 296 Two branches of logic are used depending on whether or not the API is 297 determined to be 'stable' at the given version. 298 ''' 299 if channel_info.channel == 'stable': 300 return self._CheckStableAvailability(api_name, 301 file_system, 302 channel_info.version) 303 return self._CheckChannelAvailability(api_name, 304 file_system, 305 channel_info) 306 307 def _FindScheduled(self, api_name, earliest_version=None): 308 '''Determines the earliest version of Chrome where the API is stable. 309 Unlike the code in GetAPIAvailability, this checks if the API is stable 310 even when Chrome is in dev or beta, which shows that the API is scheduled 311 to be stable in that verison of Chrome. |earliest_version| is the version 312 |api_name| became first available. Only use it when finding scheduled 313 availability for nodes. 314 ''' 315 def check_scheduled(file_system, channel_info): 316 return self._CheckStableAvailability(api_name, 317 file_system, 318 channel_info.version, 319 earliest_version=earliest_version) 320 321 stable_channel = self._file_system_iterator.Descending( 322 self._branch_utility.GetChannelInfo('dev'), check_scheduled) 323 324 return stable_channel.version if stable_channel else None 325 326 def _CheckAPINodeAvailability(self, node_name, earliest_channel_info): 327 '''Gets availability data for a node by checking _features files. 328 ''' 329 def check_node_availability(file_system, channel_info): 330 return self._CheckChannelAvailabilityForNode(node_name, 331 file_system, 332 channel_info, 333 earliest_channel_info) 334 channel_info = (self._file_system_iterator.Descending( 335 self._branch_utility.GetChannelInfo('dev'), check_node_availability) or 336 earliest_channel_info) 337 338 if channel_info.channel == 'stable': 339 scheduled = None 340 else: 341 scheduled = self._FindScheduled( 342 node_name, 343 earliest_version=earliest_channel_info.version) 344 345 return AvailabilityInfo(channel_info, scheduled=scheduled) 346 347 def GetAPIAvailability(self, api_name): 348 '''Performs a search for an API's top-level availability by using a 349 HostFileSystemIterator instance to traverse multiple version of the 350 SVN filesystem. 351 ''' 352 availability = self._top_level_object_store.Get(api_name).Get() 353 if availability is not None: 354 return availability 355 356 # Check for predetermined availability and cache this information if found. 357 availability = self._GetPredeterminedAvailability(api_name) 358 if availability is not None: 359 self._top_level_object_store.Set(api_name, availability) 360 return availability 361 362 def check_api_availability(file_system, channel_info): 363 return self._CheckAPIAvailability(api_name, file_system, channel_info) 364 365 channel_info = self._file_system_iterator.Descending( 366 self._branch_utility.GetChannelInfo('dev'), 367 check_api_availability) 368 if channel_info is None: 369 # The API wasn't available on 'dev', so it must be a 'trunk'-only API. 370 channel_info = self._branch_utility.GetChannelInfo('trunk') 371 372 # If the API is not stable, check when it will be scheduled to be stable. 373 if channel_info.channel == 'stable': 374 scheduled = None 375 else: 376 scheduled = self._FindScheduled(api_name) 377 378 availability = AvailabilityInfo(channel_info, scheduled=scheduled) 379 380 self._top_level_object_store.Set(api_name, availability) 381 return availability 382 383 def GetAPINodeAvailability(self, api_name): 384 '''Returns an APISchemaGraph annotated with each node's availability (the 385 ChannelInfo at the oldest channel it's available in). 386 ''' 387 availability_graph = self._node_level_object_store.Get(api_name).Get() 388 if availability_graph is not None: 389 return availability_graph 390 391 def assert_not_none(value): 392 assert value is not None 393 return value 394 395 availability_graph = APISchemaGraph() 396 host_fs = self._host_file_system 397 trunk_stat = assert_not_none(host_fs.Stat(_GetAPISchemaFilename( 398 api_name, host_fs, 'trunk'))) 399 400 # Weird object thing here because nonlocal is Python 3. 401 previous = type('previous', (object,), {'stat': None, 'graph': None}) 402 403 def update_availability_graph(file_system, channel_info): 404 # If we can't find a filename, skip checking at this branch. 405 # For example, something could have a predetermined availability of 23, 406 # but it doesn't show up in the file system until 26. 407 # We know that the file will become available at some point. 408 # 409 # The problem with this is that at the first version where the API file 410 # exists, we'll get a huge chunk of new objects that don't match 411 # the predetermined API availability. 412 version_filename = _GetAPISchemaFilename(api_name, 413 file_system, 414 channel_info.version) 415 if version_filename is None: 416 # Continue the loop at the next version. 417 return True 418 419 version_stat = assert_not_none(file_system.Stat(version_filename)) 420 421 # Important optimisation: only re-parse the graph if the file changed in 422 # the last revision. Parsing the same schema and forming a graph on every 423 # iteration is really expensive. 424 if version_stat == previous.stat: 425 version_graph = previous.graph 426 else: 427 # Keep track of any new schema elements from this version by adding 428 # them to |availability_graph|. 429 # 430 # Calling |availability_graph|.Lookup() on the nodes being updated 431 # will return the |annotation| object -- the current |channel_info|. 432 version_graph = APISchemaGraph( 433 api_schema=self._GetAPISchema(api_name, 434 file_system, 435 channel_info.version)) 436 def annotator(node_name): 437 return self._CheckAPINodeAvailability('%s.%s' % (api_name, node_name), 438 channel_info) 439 440 availability_graph.Update(version_graph.Subtract(availability_graph), 441 annotator) 442 443 previous.stat = version_stat 444 previous.graph = version_graph 445 446 # Continue looping until there are no longer differences between this 447 # version and trunk. 448 return version_stat != trunk_stat 449 450 self._file_system_iterator.Ascending( 451 self.GetAPIAvailability(api_name).channel_info, 452 update_availability_graph) 453 454 self._node_level_object_store.Set(api_name, availability_graph) 455 return availability_graph 456