blob: 51340231ab7d11c8d3ba741f47fc88f4d07da533 [file] [log] [blame]
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import posixpath
from api_models import GetNodeCategories
from api_schema_graph import APISchemaGraph
from branch_utility import BranchUtility, ChannelInfo
from compiled_file_system import Cache, CompiledFileSystem, SingleFile, Unicode
from extensions_paths import API_PATHS, JSON_TEMPLATES
from features_bundle import FeaturesBundle
from file_system import FileNotFoundError
from schema_processor import SchemaProcessor
from third_party.json_schema_compiler.memoize import memoize
from third_party.json_schema_compiler.model import UnixName
_DEVTOOLS_API = 'devtools_api.json'
_EXTENSION_API = 'extension_api.json'
# The version where api_features.json is first available.
_API_FEATURES_MIN_VERSION = 28
# The version where permission_ and manifest_features.json are available and
# presented in the current format.
_ORIGINAL_FEATURES_MIN_VERSION = 20
# API schemas are aggregated in extension_api.json up to this version.
_EXTENSION_API_MAX_VERSION = 17
# The earliest version for which we have SVN data.
_SVN_MIN_VERSION = 5
def _GetChannelFromFeatures(api_name, features):
'''Finds API channel information for |api_name| from |features|.
Returns None if channel information for the API cannot be located.
'''
feature = features.Get().get(api_name)
channel = feature.get('channel') if feature else None
# Change "trunk" to "master". Extension features use "trunk" while the
# Docserver uses "master".
return "master" if channel == "trunk" else channel
def _GetChannelFromAPIFeatures(api_name, features_bundle):
return _GetChannelFromFeatures(api_name, features_bundle.GetAPIFeatures())
def _GetChannelFromManifestFeatures(api_name, features_bundle):
# _manifest_features.json uses unix_style API names.
api_name = UnixName(api_name)
return _GetChannelFromFeatures(api_name,
features_bundle.GetManifestFeatures())
def _GetChannelFromPermissionFeatures(api_name, features_bundle):
return _GetChannelFromFeatures(api_name,
features_bundle.GetPermissionFeatures())
def _GetAPISchemaFilename(api_name, file_system, version):
'''Gets the name of the file which may contain the schema for |api_name| in
|file_system|, or None if the API is not found. Note that this may be the
single _EXTENSION_API file which all APIs share in older versions of Chrome,
in which case it is unknown whether the API actually exists there.
'''
if version == 'master' or version > _ORIGINAL_FEATURES_MIN_VERSION:
# API schema filenames switch format to unix_hacker_style.
api_name = UnixName(api_name)
# Devtools API names have 'devtools.' prepended to them.
# The corresponding filenames do not.
if 'devtools_' in api_name:
api_name = api_name.replace('devtools_', '')
for api_path in API_PATHS:
try:
for base, _, filenames in file_system.Walk(api_path):
for ext in ('json', 'idl'):
filename = '%s.%s' % (api_name, ext)
if filename in filenames:
return posixpath.join(api_path, base, filename)
if _EXTENSION_API in filenames:
return posixpath.join(api_path, base, _EXTENSION_API)
except FileNotFoundError:
continue
return None
class AvailabilityInfo(object):
'''Represents availability data for an API. |scheduled| is a version number
specifying when dev and beta APIs will become stable, or None if that data
is unknown.
'''
def __init__(self, channel_info, scheduled=None):
assert isinstance(channel_info, ChannelInfo)
assert isinstance(scheduled, int) or scheduled is None
self.channel_info = channel_info
self.scheduled = scheduled
def __eq__(self, other):
return self.__dict__ == other.__dict__
def __ne__(self, other):
return not (self == other)
def __repr__(self):
return '%s%s' % (type(self).__name__, repr(self.__dict__))
def __str__(self):
return repr(self)
class AvailabilityFinder(object):
'''Generates availability information for APIs by looking at API schemas and
_features files over multiple release versions of Chrome.
'''
def __init__(self,
branch_utility,
compiled_fs_factory,
file_system_iterator,
host_file_system,
object_store_creator,
platform,
schema_processor_factory):
self._branch_utility = branch_utility
self._compiled_fs_factory = compiled_fs_factory
self._file_system_iterator = file_system_iterator
self._host_file_system = host_file_system
self._object_store_creator = object_store_creator
def create_object_store(category):
return object_store_creator.Create(
AvailabilityFinder, category='/'.join((platform, category)))
self._top_level_object_store = create_object_store('top_level')
self._node_level_object_store = create_object_store('node_level')
self._json_fs = compiled_fs_factory.ForJson(self._host_file_system)
self._platform = platform
# When processing the API schemas, we retain inlined types in the schema
# so that there are not missing nodes in the APISchemaGraphs when trying
# to lookup availability.
self._schema_processor = schema_processor_factory.Create(True)
def _GetPredeterminedNodeAvailability(self, node_name):
'''Checks a configuration file for hardcoded (i.e. predetermined)
availability information for an API node.
'''
node_info = self._json_fs.GetFromFile(
JSON_TEMPLATES + 'api_availabilities.json').Get().get(node_name)
if node_info is None:
return None
channel_info = None
if node_info['channel'] == 'stable':
channel_info = self._branch_utility.GetStableChannelInfo(
node_info['version'])
else:
channel_info = self._branch_utility.GetChannelInfo(node_info['channel'])
return AvailabilityInfo(channel_info) if channel_info else None
@memoize
def _CreateAPISchemaFileSystem(self, file_system):
'''Creates a CompiledFileSystem for parsing raw JSON or IDL API schema
data and formatting it so that it can be used to create APISchemaGraphs.
'''
def process_schema(path, data):
return self._schema_processor.Process(path, data)
return self._compiled_fs_factory.Create(
file_system,
Cache(SingleFile(Unicode(process_schema))),
CompiledFileSystem,
category='api-schema')
def _GetAPISchema(self, api_name, file_system, version):
'''Searches |file_system| for |api_name|'s API schema data, and processes
and returns it if found.
'''
api_filename = _GetAPISchemaFilename(api_name, file_system, version)
if api_filename is None:
# No file for the API could be found in the given |file_system|.
return None
schema_fs = self._CreateAPISchemaFileSystem(file_system)
api_schemas = schema_fs.GetFromFile(api_filename).Get()
matching_schemas = [api for api in api_schemas
if api and api['namespace'] == api_name]
# There should only be a single matching schema per file, or zero in the
# case of no API data being found in _EXTENSION_API.
assert len(matching_schemas) <= 1
return matching_schemas or None
def _HasAPISchema(self, api_name, file_system, version):
'''Whether or not an API schema for |api_name| exists in the given
|file_system|.
'''
filename = _GetAPISchemaFilename(api_name, file_system, version)
if filename is None:
return False
if filename.endswith(_EXTENSION_API) or filename.endswith(_DEVTOOLS_API):
return self._GetAPISchema(api_name, file_system, version) is not None
return True
def _CheckStableAvailability(self,
api_name,
file_system,
version,
earliest_version=None):
'''Checks for availability of an API, |api_name|, on the stable channel.
Considers several _features.json files, file system existence, and
extension_api.json depending on the given |version|.
|earliest_version| is the version of Chrome at which |api_name| first became
available. It should only be given when checking stable availability for
API nodes, so it can be used as an alternative to the check for filesystem
existence.
'''
earliest_version = earliest_version or _SVN_MIN_VERSION
if version < earliest_version:
# SVN data isn't available below this version.
return False
features_bundle = self._CreateFeaturesBundle(file_system)
available_channel = None
if version >= _API_FEATURES_MIN_VERSION:
# The _api_features.json file first appears in version 28 and should be
# the most reliable for finding API availability.
available_channel = _GetChannelFromAPIFeatures(api_name,
features_bundle)
if version >= _ORIGINAL_FEATURES_MIN_VERSION:
# The _permission_features.json and _manifest_features.json files are
# present in Chrome 20 and onwards. Use these if no information could be
# found using _api_features.json.
available_channel = (
available_channel or
_GetChannelFromPermissionFeatures(api_name, features_bundle) or
_GetChannelFromManifestFeatures(api_name, features_bundle))
if available_channel is not None:
return available_channel == 'stable'
# |earliest_version| == _SVN_MIN_VERSION implies we're dealing with an API.
# Fall back to a check for file system existence if the API is not
# stable in any of the _features.json files, or if the _features files
# do not exist (version 19 and earlier).
if earliest_version == _SVN_MIN_VERSION:
return self._HasAPISchema(api_name, file_system, version)
# For API nodes, assume it's available if |version| is greater than the
# version the node became available (which it is, because of the first
# check).
return True
def _CheckChannelAvailability(self, api_name, file_system, channel_info):
'''Searches through the _features files in a given |file_system|, falling
back to checking the file system for API schema existence, to determine
whether or not an API is available on the given channel, |channel_info|.
'''
features_bundle = self._CreateFeaturesBundle(file_system)
available_channel = (
_GetChannelFromAPIFeatures(api_name, features_bundle) or
_GetChannelFromPermissionFeatures(api_name, features_bundle) or
_GetChannelFromManifestFeatures(api_name, features_bundle))
if (available_channel is None and
self._HasAPISchema(api_name, file_system, channel_info.version)):
# If an API is not represented in any of the _features files, but exists
# in the filesystem, then assume it is available in this version.
# The chrome.windows API is an example of this.
available_channel = channel_info.channel
# If the channel we're checking is the same as or newer than the
# |available_channel| then the API is available at this channel.
newest = BranchUtility.NewestChannel((available_channel,
channel_info.channel))
return available_channel is not None and newest == channel_info.channel
def _CheckChannelAvailabilityForNode(self,
node_name,
file_system,
channel_info,
earliest_channel_info):
'''Searches through the _features files in a given |file_system| to
determine whether or not an API node is available on the given channel,
|channel_info|. |earliest_channel_info| is the earliest channel the node
was introduced.
'''
features_bundle = self._CreateFeaturesBundle(file_system)
available_channel = None
# Only API nodes can have their availability overriden on a per-node basis,
# so we only need to check _api_features.json.
if channel_info.version >= _API_FEATURES_MIN_VERSION:
available_channel = _GetChannelFromAPIFeatures(node_name, features_bundle)
if (available_channel is None and
channel_info.version >= earliest_channel_info.version):
# Most API nodes inherit their availabiltity from their parent, so don't
# explicitly appear in _api_features.json. For example, "tabs.create"
# isn't listed; it inherits from "tabs". Assume these are available at
# |channel_info|.
available_channel = channel_info.channel
newest = BranchUtility.NewestChannel((available_channel,
channel_info.channel))
return available_channel is not None and newest == channel_info.channel
@memoize
def _CreateFeaturesBundle(self, file_system):
return FeaturesBundle(file_system,
self._compiled_fs_factory,
self._object_store_creator,
self._platform)
def _CheckAPIAvailability(self, api_name, file_system, channel_info):
'''Determines the availability for an API at a certain version of Chrome.
Two branches of logic are used depending on whether or not the API is
determined to be 'stable' at the given version.
'''
if channel_info.channel == 'stable':
return self._CheckStableAvailability(api_name,
file_system,
channel_info.version)
return self._CheckChannelAvailability(api_name,
file_system,
channel_info)
def _FindScheduled(self, api_name, earliest_version=None):
'''Determines the earliest version of Chrome where the API is stable.
Unlike the code in GetAPIAvailability, this checks if the API is stable
even when Chrome is in dev or beta, which shows that the API is scheduled
to be stable in that verison of Chrome. |earliest_version| is the version
|api_name| became first available. Only use it when finding scheduled
availability for nodes.
'''
def check_scheduled(file_system, channel_info):
return self._CheckStableAvailability(api_name,
file_system,
channel_info.version,
earliest_version=earliest_version)
stable_channel = self._file_system_iterator.Descending(
self._branch_utility.GetChannelInfo('dev'), check_scheduled)
return stable_channel.version if stable_channel else None
def _CheckAPINodeAvailability(self, node_name, earliest_channel_info):
'''Gets availability data for a node by checking _features files.
'''
# Check for predetermined availability and cache this information if found.
availability = self._GetPredeterminedNodeAvailability(node_name)
if availability is not None:
self._top_level_object_store.Set(node_name, availability)
return availability
def check_node_availability(file_system, channel_info):
return self._CheckChannelAvailabilityForNode(node_name,
file_system,
channel_info,
earliest_channel_info)
channel_info = (self._file_system_iterator.Descending(
self._branch_utility.GetChannelInfo('dev'), check_node_availability) or
earliest_channel_info)
if channel_info.channel == 'stable':
scheduled = None
else:
scheduled = self._FindScheduled(
node_name,
earliest_version=earliest_channel_info.version)
return AvailabilityInfo(channel_info, scheduled=scheduled)
def GetAPIAvailability(self, api_name):
'''Performs a search for an API's top-level availability by using a
HostFileSystemIterator instance to traverse multiple version of the
SVN filesystem.
'''
availability = self._top_level_object_store.Get(api_name).Get()
if availability is not None:
return availability
# Check for predetermined availability and cache this information if found.
availability = self._GetPredeterminedNodeAvailability(api_name)
if availability is not None:
self._top_level_object_store.Set(api_name, availability)
return availability
def check_api_availability(file_system, channel_info):
return self._CheckAPIAvailability(api_name, file_system, channel_info)
channel_info = self._file_system_iterator.Descending(
self._branch_utility.GetChannelInfo('dev'),
check_api_availability)
if channel_info is None:
# The API wasn't available on 'dev', so it must be a 'master'-only API.
channel_info = self._branch_utility.GetChannelInfo('master')
# If the API is not stable, check when it will be scheduled to be stable.
if channel_info.channel == 'stable':
scheduled = None
else:
scheduled = self._FindScheduled(api_name)
availability = AvailabilityInfo(channel_info, scheduled=scheduled)
self._top_level_object_store.Set(api_name, availability)
return availability
def GetAPINodeAvailability(self, api_name):
'''Returns an APISchemaGraph annotated with each node's availability (the
ChannelInfo at the oldest channel it's available in).
'''
availability_graph = self._node_level_object_store.Get(api_name).Get()
if availability_graph is not None:
return availability_graph
def assert_not_none(value):
assert value is not None
return value
availability_graph = APISchemaGraph()
host_fs = self._host_file_system
master_stat = assert_not_none(host_fs.Stat(_GetAPISchemaFilename(
api_name, host_fs, 'master')))
# Weird object thing here because nonlocal is Python 3.
previous = type('previous', (object,), {'stat': None, 'graph': None})
def update_availability_graph(file_system, channel_info):
# If we can't find a filename, skip checking at this branch.
# For example, something could have a predetermined availability of 23,
# but it doesn't show up in the file system until 26.
# We know that the file will become available at some point.
#
# The problem with this is that at the first version where the API file
# exists, we'll get a huge chunk of new objects that don't match
# the predetermined API availability.
version_filename = _GetAPISchemaFilename(api_name,
file_system,
channel_info.version)
if version_filename is None:
# Continue the loop at the next version.
return True
version_stat = assert_not_none(file_system.Stat(version_filename))
# Important optimisation: only re-parse the graph if the file changed in
# the last revision. Parsing the same schema and forming a graph on every
# iteration is really expensive.
if version_stat == previous.stat:
version_graph = previous.graph
else:
# Keep track of any new schema elements from this version by adding
# them to |availability_graph|.
#
# Calling |availability_graph|.Lookup() on the nodes being updated
# will return the |annotation| object -- the current |channel_info|.
version_graph = APISchemaGraph(
api_schema=self._GetAPISchema(api_name,
file_system,
channel_info.version))
def annotator(node_name):
return self._CheckAPINodeAvailability('%s.%s' % (api_name, node_name),
channel_info)
availability_graph.Update(version_graph.Subtract(availability_graph),
annotator)
previous.stat = version_stat
previous.graph = version_graph
# Continue looping until there are no longer differences between this
# version and master.
return version_stat != master_stat
self._file_system_iterator.Ascending(
self.GetAPIAvailability(api_name).channel_info,
update_availability_graph)
self._node_level_object_store.Set(api_name, availability_graph)
return availability_graph