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