availability_finder.py revision 5c02ac1a9c1b504631c0a3d2b6e737b5d738bae1
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 5from collections import Mapping 6import posixpath 7 8from api_schema_graph import APISchemaGraph 9from branch_utility import BranchUtility, ChannelInfo 10from extensions_paths import API_PATHS, JSON_TEMPLATES 11from features_bundle import FeaturesBundle 12import features_utility 13from file_system import FileNotFoundError 14from third_party.json_schema_compiler.memoize import memoize 15from third_party.json_schema_compiler.model import UnixName 16 17 18_EXTENSION_API = 'extension_api.json' 19 20# The version where api_features.json is first available. 21_API_FEATURES_MIN_VERSION = 28 22# The version where permission_ and manifest_features.json are available and 23# presented in the current format. 24_ORIGINAL_FEATURES_MIN_VERSION = 20 25# API schemas are aggregated in extension_api.json up to this version. 26_EXTENSION_API_MAX_VERSION = 17 27# The earliest version for which we have SVN data. 28_SVN_MIN_VERSION = 5 29 30 31def _GetChannelFromFeatures(api_name, features): 32 '''Finds API channel information for |api_name| from |features|. 33 Returns None if channel information for the API cannot be located. 34 ''' 35 feature = features.Get().get(api_name) 36 return feature.get('channel') if feature else None 37 38 39class AvailabilityInfo(object): 40 '''Represents availability data for an API. |scheduled| is a version number 41 specifying when dev and beta APIs will become stable, or None if that data 42 is unknown. 43 ''' 44 def __init__(self, channel_info, scheduled=None): 45 assert isinstance(channel_info, ChannelInfo) 46 assert isinstance(scheduled, int) or scheduled is None 47 self.channel_info = channel_info 48 self.scheduled = scheduled 49 50 def __eq__(self, other): 51 return self.__dict__ == other.__dict__ 52 53 def __ne__(self, other): 54 return not (self == other) 55 56 def __repr__(self): 57 return '%s%s' % (type(self).__name__, repr(self.__dict__)) 58 59 def __str__(self): 60 return repr(self) 61 62 63class AvailabilityFinder(object): 64 '''Generates availability information for APIs by looking at API schemas and 65 _features files over multiple release versions of Chrome. 66 ''' 67 68 def __init__(self, 69 branch_utility, 70 compiled_fs_factory, 71 file_system_iterator, 72 host_file_system, 73 object_store_creator): 74 self._branch_utility = branch_utility 75 self._compiled_fs_factory = compiled_fs_factory 76 self._file_system_iterator = file_system_iterator 77 self._host_file_system = host_file_system 78 self._object_store_creator = object_store_creator 79 def create_object_store(category): 80 return object_store_creator.Create(AvailabilityFinder, category=category) 81 self._top_level_object_store = create_object_store('top_level') 82 self._node_level_object_store = create_object_store('node_level') 83 self._json_fs = compiled_fs_factory.ForJson(self._host_file_system) 84 85 def _GetPredeterminedAvailability(self, api_name): 86 '''Checks a configuration file for hardcoded (i.e. predetermined) 87 availability information for an API. 88 ''' 89 api_info = self._json_fs.GetFromFile( 90 JSON_TEMPLATES + 'api_availabilities.json').Get().get(api_name) 91 if api_info is None: 92 return None 93 if api_info['channel'] == 'stable': 94 return AvailabilityInfo( 95 self._branch_utility.GetStableChannelInfo(api_info['version'])) 96 return AvailabilityInfo( 97 self._branch_utility.GetChannelInfo(api_info['channel'])) 98 99 def _GetApiSchemaFilename(self, api_name, file_system, version): 100 '''Gets the name of the file which may contain the schema for |api_name| in 101 |file_system|, or None if the API is not found. Note that this may be the 102 single _EXTENSION_API file which all APIs share in older versions of Chrome, 103 in which case it is unknown whether the API actually exists there. 104 ''' 105 if version == 'trunk' or version > _ORIGINAL_FEATURES_MIN_VERSION: 106 # API schema filenames switch format to unix_hacker_style. 107 api_name = UnixName(api_name) 108 109 found_files = file_system.Read(API_PATHS, skip_not_found=True) 110 for path, filenames in found_files.Get().iteritems(): 111 try: 112 for ext in ('json', 'idl'): 113 filename = '%s.%s' % (api_name, ext) 114 if filename in filenames: 115 return path + filename 116 if _EXTENSION_API in filenames: 117 return path + _EXTENSION_API 118 except FileNotFoundError: 119 pass 120 return None 121 122 def _GetApiSchema(self, api_name, file_system, version): 123 '''Searches |file_system| for |api_name|'s API schema data, and processes 124 and returns it if found. 125 ''' 126 api_filename = self._GetApiSchemaFilename(api_name, file_system, version) 127 if api_filename is None: 128 # No file for the API could be found in the given |file_system|. 129 return None 130 131 schema_fs = self._compiled_fs_factory.ForApiSchema(file_system) 132 api_schemas = schema_fs.GetFromFile(api_filename).Get() 133 matching_schemas = [api for api in api_schemas 134 if api['namespace'] == api_name] 135 # There should only be a single matching schema per file, or zero in the 136 # case of no API data being found in _EXTENSION_API. 137 assert len(matching_schemas) <= 1 138 return matching_schemas or None 139 140 def _HasApiSchema(self, api_name, file_system, version): 141 '''Whether or not an API schema for |api_name|exists in the given 142 |file_system|. 143 ''' 144 filename = self._GetApiSchemaFilename(api_name, file_system, version) 145 if filename is None: 146 return False 147 if filename.endswith(_EXTENSION_API): 148 return self._GetApiSchema(api_name, file_system, version) is not None 149 return True 150 151 def _CheckStableAvailability(self, api_name, file_system, version): 152 '''Checks for availability of an API, |api_name|, on the stable channel. 153 Considers several _features.json files, file system existence, and 154 extension_api.json depending on the given |version|. 155 ''' 156 if version < _SVN_MIN_VERSION: 157 # SVN data isn't available below this version. 158 return False 159 features_bundle = self._CreateFeaturesBundle(file_system) 160 available_channel = None 161 if version >= _API_FEATURES_MIN_VERSION: 162 # The _api_features.json file first appears in version 28 and should be 163 # the most reliable for finding API availability. 164 available_channel = self._GetChannelFromApiFeatures(api_name, 165 features_bundle) 166 if version >= _ORIGINAL_FEATURES_MIN_VERSION: 167 # The _permission_features.json and _manifest_features.json files are 168 # present in Chrome 20 and onwards. Use these if no information could be 169 # found using _api_features.json. 170 available_channel = ( 171 available_channel or 172 self._GetChannelFromPermissionFeatures(api_name, features_bundle) or 173 self._GetChannelFromManifestFeatures(api_name, features_bundle)) 174 if available_channel is not None: 175 return available_channel == 'stable' 176 if version >= _SVN_MIN_VERSION: 177 # Fall back to a check for file system existence if the API is not 178 # stable in any of the _features.json files, or if the _features files 179 # do not exist (version 19 and earlier). 180 return self._HasApiSchema(api_name, file_system, version) 181 182 def _CheckChannelAvailability(self, api_name, file_system, channel_info): 183 '''Searches through the _features files in a given |file_system|, falling 184 back to checking the file system for API schema existence, to determine 185 whether or not an API is available on the given channel, |channel_info|. 186 ''' 187 features_bundle = self._CreateFeaturesBundle(file_system) 188 available_channel = ( 189 self._GetChannelFromApiFeatures(api_name, features_bundle) or 190 self._GetChannelFromPermissionFeatures(api_name, features_bundle) or 191 self._GetChannelFromManifestFeatures(api_name, features_bundle)) 192 if (available_channel is None and 193 self._HasApiSchema(api_name, file_system, channel_info.version)): 194 # If an API is not represented in any of the _features files, but exists 195 # in the filesystem, then assume it is available in this version. 196 # The chrome.windows API is an example of this. 197 available_channel = channel_info.channel 198 # If the channel we're checking is the same as or newer than the 199 # |available_channel| then the API is available at this channel. 200 newest = BranchUtility.NewestChannel((available_channel, 201 channel_info.channel)) 202 return available_channel is not None and newest == channel_info.channel 203 204 @memoize 205 def _CreateFeaturesBundle(self, file_system): 206 return FeaturesBundle(file_system, 207 self._compiled_fs_factory, 208 self._object_store_creator) 209 210 def _GetChannelFromApiFeatures(self, api_name, features_bundle): 211 return _GetChannelFromFeatures(api_name, features_bundle.GetAPIFeatures()) 212 213 def _GetChannelFromManifestFeatures(self, api_name, features_bundle): 214 # _manifest_features.json uses unix_style API names. 215 api_name = UnixName(api_name) 216 return _GetChannelFromFeatures(api_name, 217 features_bundle.GetManifestFeatures()) 218 219 def _GetChannelFromPermissionFeatures(self, api_name, features_bundle): 220 return _GetChannelFromFeatures(api_name, 221 features_bundle.GetPermissionFeatures()) 222 223 def _CheckApiAvailability(self, api_name, file_system, channel_info): 224 '''Determines the availability for an API at a certain version of Chrome. 225 Two branches of logic are used depending on whether or not the API is 226 determined to be 'stable' at the given version. 227 ''' 228 if channel_info.channel == 'stable': 229 return self._CheckStableAvailability(api_name, 230 file_system, 231 channel_info.version) 232 return self._CheckChannelAvailability(api_name, 233 file_system, 234 channel_info) 235 236 def _FindScheduled(self, api_name): 237 '''Determines the earliest version of Chrome where the API is stable. 238 Unlike the code in GetApiAvailability, this checks if the API is stable 239 even when Chrome is in dev or beta, which shows that the API is scheduled 240 to be stable in that verison of Chrome. 241 ''' 242 def check_scheduled(file_system, channel_info): 243 return self._CheckStableAvailability( 244 api_name, file_system, channel_info.version) 245 246 stable_channel = self._file_system_iterator.Descending( 247 self._branch_utility.GetChannelInfo('dev'), check_scheduled) 248 249 return stable_channel.version if stable_channel else None 250 251 def GetApiAvailability(self, api_name): 252 '''Performs a search for an API's top-level availability by using a 253 HostFileSystemIterator instance to traverse multiple version of the 254 SVN filesystem. 255 ''' 256 availability = self._top_level_object_store.Get(api_name).Get() 257 if availability is not None: 258 return availability 259 260 # Check for predetermined availability and cache this information if found. 261 availability = self._GetPredeterminedAvailability(api_name) 262 if availability is not None: 263 self._top_level_object_store.Set(api_name, availability) 264 return availability 265 266 def check_api_availability(file_system, channel_info): 267 return self._CheckApiAvailability(api_name, file_system, channel_info) 268 269 channel_info = self._file_system_iterator.Descending( 270 self._branch_utility.GetChannelInfo('dev'), 271 check_api_availability) 272 if channel_info is None: 273 # The API wasn't available on 'dev', so it must be a 'trunk'-only API. 274 channel_info = self._branch_utility.GetChannelInfo('trunk') 275 276 # If the API is not stable, check when it will be scheduled to be stable. 277 if channel_info.channel == 'stable': 278 scheduled = None 279 else: 280 scheduled = self._FindScheduled(api_name) 281 282 availability = AvailabilityInfo(channel_info, scheduled=scheduled) 283 284 self._top_level_object_store.Set(api_name, availability) 285 return availability 286 287 def GetApiNodeAvailability(self, api_name): 288 '''Returns an APISchemaGraph annotated with each node's availability (the 289 ChannelInfo at the oldest channel it's available in). 290 ''' 291 availability_graph = self._node_level_object_store.Get(api_name).Get() 292 if availability_graph is not None: 293 return availability_graph 294 295 def assert_not_none(value): 296 assert value is not None 297 return value 298 299 availability_graph = APISchemaGraph() 300 301 host_fs = self._host_file_system 302 trunk_stat = assert_not_none(host_fs.Stat(self._GetApiSchemaFilename( 303 api_name, host_fs, 'trunk'))) 304 305 # Weird object thing here because nonlocal is Python 3. 306 previous = type('previous', (object,), {'stat': None, 'graph': None}) 307 308 def update_availability_graph(file_system, channel_info): 309 version_filename = assert_not_none(self._GetApiSchemaFilename( 310 api_name, file_system, channel_info.version)) 311 version_stat = assert_not_none(file_system.Stat(version_filename)) 312 313 # Important optimisation: only re-parse the graph if the file changed in 314 # the last revision. Parsing the same schema and forming a graph on every 315 # iteration is really expensive. 316 if version_stat == previous.stat: 317 version_graph = previous.graph 318 else: 319 # Keep track of any new schema elements from this version by adding 320 # them to |availability_graph|. 321 # 322 # Calling |availability_graph|.Lookup() on the nodes being updated 323 # will return the |annotation| object -- the current |channel_info|. 324 version_graph = APISchemaGraph(self._GetApiSchema( 325 api_name, file_system, channel_info.version)) 326 availability_graph.Update(version_graph.Subtract(availability_graph), 327 annotation=channel_info) 328 329 previous.stat = version_stat 330 previous.graph = version_graph 331 332 # Continue looping until there are no longer differences between this 333 # version and trunk. 334 return version_stat != trunk_stat 335 336 self._file_system_iterator.Ascending( 337 self.GetApiAvailability(api_name).channel_info, 338 update_availability_graph) 339 340 self._node_level_object_store.Set(api_name, availability_graph) 341 return availability_graph 342