247 lines
10 KiB
Python
247 lines
10 KiB
Python
# Copyright 2015 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 logging
|
|
import os
|
|
|
|
from dependency_manager import base_config
|
|
from dependency_manager import exceptions
|
|
|
|
|
|
DEFAULT_TYPE = 'default'
|
|
|
|
|
|
class DependencyManager(object):
|
|
def __init__(self, configs, supported_config_types=None):
|
|
"""Manages file dependencies found locally or in cloud_storage.
|
|
|
|
Args:
|
|
configs: A list of instances of BaseConfig or it's subclasses, passed
|
|
in decreasing order of precedence.
|
|
supported_config_types: A list of whitelisted config_types.
|
|
No restrictions if None is specified.
|
|
|
|
Raises:
|
|
ValueError: If |configs| is not a list of instances of BaseConfig or
|
|
its subclasses.
|
|
UnsupportedConfigFormatError: If supported_config_types is specified and
|
|
configs contains a config not in the supported config_types.
|
|
|
|
Example: DependencyManager([config1, config2, config3])
|
|
No requirements on the type of Config, and any dependencies that have
|
|
local files for the same platform will first look in those from
|
|
config1, then those from config2, and finally those from config3.
|
|
"""
|
|
if configs is None or not isinstance(configs, list):
|
|
raise ValueError(
|
|
'Must supply a list of config files to DependencyManager')
|
|
# self._lookup_dict is a dictionary with the following format:
|
|
# { dependency1: {platform1: dependency_info1,
|
|
# platform2: dependency_info2}
|
|
# dependency2: {platform1: dependency_info3,
|
|
# ...}
|
|
# ...}
|
|
#
|
|
# Where the dependencies and platforms are strings, and the
|
|
# dependency_info's are DependencyInfo instances.
|
|
self._lookup_dict = {}
|
|
self.supported_configs = supported_config_types or []
|
|
for config in configs:
|
|
self._UpdateDependencies(config)
|
|
|
|
|
|
def FetchPathWithVersion(self, dependency, platform):
|
|
"""Get a path to an executable for |dependency|, downloading as needed.
|
|
|
|
A path to a default executable may be returned if a platform specific
|
|
version is not specified in the config(s).
|
|
|
|
Args:
|
|
dependency: Name of the desired dependency, as given in the config(s)
|
|
used in this DependencyManager.
|
|
platform: Name of the platform the dependency will run on. Often of the
|
|
form 'os_architecture'. Must match those specified in the config(s)
|
|
used in this DependencyManager.
|
|
Returns:
|
|
<path>, <version> where:
|
|
<path> is the path to an executable of |dependency| that will run
|
|
on |platform|, downloading from cloud storage if needed.
|
|
<version> is the version of the executable at <path> or None.
|
|
|
|
Raises:
|
|
NoPathFoundError: If a local copy of the executable cannot be found and
|
|
a remote path could not be downloaded from cloud_storage.
|
|
CredentialsError: If cloud_storage credentials aren't configured.
|
|
PermissionError: If cloud_storage credentials are configured, but not
|
|
with an account that has permission to download the remote file.
|
|
NotFoundError: If the remote file does not exist where expected in
|
|
cloud_storage.
|
|
ServerError: If an internal server error is hit while downloading the
|
|
remote file.
|
|
CloudStorageError: If another error occured while downloading the remote
|
|
path.
|
|
FileNotFoundError: If an attempted download was otherwise unsuccessful.
|
|
|
|
"""
|
|
dependency_info = self._GetDependencyInfo(dependency, platform)
|
|
if not dependency_info:
|
|
raise exceptions.NoPathFoundError(dependency, platform)
|
|
path = dependency_info.GetLocalPath()
|
|
version = None
|
|
if not path or not os.path.exists(path):
|
|
path = dependency_info.GetRemotePath()
|
|
if not path or not os.path.exists(path):
|
|
raise exceptions.NoPathFoundError(dependency, platform)
|
|
version = dependency_info.GetRemotePathVersion()
|
|
return path, version
|
|
|
|
def FetchPath(self, dependency, platform):
|
|
"""Get a path to an executable for |dependency|, downloading as needed.
|
|
|
|
A path to a default executable may be returned if a platform specific
|
|
version is not specified in the config(s).
|
|
|
|
Args:
|
|
dependency: Name of the desired dependency, as given in the config(s)
|
|
used in this DependencyManager.
|
|
platform: Name of the platform the dependency will run on. Often of the
|
|
form 'os_architecture'. Must match those specified in the config(s)
|
|
used in this DependencyManager.
|
|
Returns:
|
|
A path to an executable of |dependency| that will run on |platform|,
|
|
downloading from cloud storage if needed.
|
|
|
|
Raises:
|
|
NoPathFoundError: If a local copy of the executable cannot be found and
|
|
a remote path could not be downloaded from cloud_storage.
|
|
CredentialsError: If cloud_storage credentials aren't configured.
|
|
PermissionError: If cloud_storage credentials are configured, but not
|
|
with an account that has permission to download the remote file.
|
|
NotFoundError: If the remote file does not exist where expected in
|
|
cloud_storage.
|
|
ServerError: If an internal server error is hit while downloading the
|
|
remote file.
|
|
CloudStorageError: If another error occured while downloading the remote
|
|
path.
|
|
FileNotFoundError: If an attempted download was otherwise unsuccessful.
|
|
|
|
"""
|
|
path, _ = self.FetchPathWithVersion(dependency, platform)
|
|
return path
|
|
|
|
def LocalPath(self, dependency, platform):
|
|
"""Get a path to a locally stored executable for |dependency|.
|
|
|
|
A path to a default executable may be returned if a platform specific
|
|
version is not specified in the config(s).
|
|
Will not download the executable.
|
|
|
|
Args:
|
|
dependency: Name of the desired dependency, as given in the config(s)
|
|
used in this DependencyManager.
|
|
platform: Name of the platform the dependency will run on. Often of the
|
|
form 'os_architecture'. Must match those specified in the config(s)
|
|
used in this DependencyManager.
|
|
Returns:
|
|
A path to an executable for |dependency| that will run on |platform|.
|
|
|
|
Raises:
|
|
NoPathFoundError: If a local copy of the executable cannot be found.
|
|
"""
|
|
dependency_info = self._GetDependencyInfo(dependency, platform)
|
|
if not dependency_info:
|
|
raise exceptions.NoPathFoundError(dependency, platform)
|
|
local_path = dependency_info.GetLocalPath()
|
|
if not local_path or not os.path.exists(local_path):
|
|
raise exceptions.NoPathFoundError(dependency, platform)
|
|
return local_path
|
|
|
|
def PrefetchPaths(self, platform, dependencies=None, cloud_storage_retries=3):
|
|
if not dependencies:
|
|
dependencies = self._lookup_dict.keys()
|
|
|
|
skipped_deps = []
|
|
found_deps = []
|
|
missing_deps = []
|
|
for dependency in dependencies:
|
|
dependency_info = self._GetDependencyInfo(dependency, platform)
|
|
if not dependency_info:
|
|
# The dependency is only configured for other platforms.
|
|
skipped_deps.append(dependency)
|
|
continue
|
|
local_path = dependency_info.GetLocalPath()
|
|
if local_path:
|
|
found_deps.append(dependency)
|
|
continue
|
|
fetched_path = None
|
|
cloud_storage_error = None
|
|
for _ in range(0, cloud_storage_retries + 1):
|
|
try:
|
|
fetched_path = dependency_info.GetRemotePath()
|
|
except exceptions.CloudStorageError as e:
|
|
cloud_storage_error = e
|
|
break
|
|
if fetched_path:
|
|
found_deps.append(dependency)
|
|
else:
|
|
missing_deps.append(dependency)
|
|
logging.error(
|
|
'Dependency %s could not be found or fetched from cloud storage for'
|
|
' platform %s. Error: %s', dependency, platform,
|
|
cloud_storage_error)
|
|
if missing_deps:
|
|
raise exceptions.NoPathFoundError(', '.join(missing_deps), platform)
|
|
return (found_deps, skipped_deps)
|
|
|
|
def _UpdateDependencies(self, config):
|
|
"""Add the dependency information stored in |config| to this instance.
|
|
|
|
Args:
|
|
config: An instances of BaseConfig or a subclasses.
|
|
|
|
Raises:
|
|
UnsupportedConfigFormatError: If supported_config_types was specified
|
|
and config is not in the supported config_types.
|
|
"""
|
|
if not isinstance(config, base_config.BaseConfig):
|
|
raise ValueError('Must use a BaseConfig or subclass instance with the '
|
|
'DependencyManager.')
|
|
if (self.supported_configs and
|
|
config.GetConfigType() not in self.supported_configs):
|
|
raise exceptions.UnsupportedConfigFormatError(config.GetConfigType(),
|
|
config.config_path)
|
|
for dep_info in config.IterDependencyInfo():
|
|
dependency = dep_info.dependency
|
|
platform = dep_info.platform
|
|
if dependency not in self._lookup_dict:
|
|
self._lookup_dict[dependency] = {}
|
|
if platform not in self._lookup_dict[dependency]:
|
|
self._lookup_dict[dependency][platform] = dep_info
|
|
else:
|
|
self._lookup_dict[dependency][platform].Update(dep_info)
|
|
|
|
|
|
def _GetDependencyInfo(self, dependency, platform):
|
|
"""Get information for |dependency| on |platform|, or a default if needed.
|
|
|
|
Args:
|
|
dependency: Name of the desired dependency, as given in the config(s)
|
|
used in this DependencyManager.
|
|
platform: Name of the platform the dependency will run on. Often of the
|
|
form 'os_architecture'. Must match those specified in the config(s)
|
|
used in this DependencyManager.
|
|
|
|
Returns: The dependency_info for |dependency| on |platform| if it exists.
|
|
Or the default version of |dependency| if it exists, or None if neither
|
|
exist.
|
|
"""
|
|
if not self._lookup_dict or dependency not in self._lookup_dict:
|
|
return None
|
|
dependency_dict = self._lookup_dict[dependency]
|
|
device_type = platform
|
|
if not device_type in dependency_dict:
|
|
device_type = DEFAULT_TYPE
|
|
return dependency_dict.get(device_type)
|
|
|