• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2015 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 logging
6import os
7
8from dependency_manager import base_config
9from dependency_manager import exceptions
10
11
12DEFAULT_TYPE = 'default'
13
14
15class DependencyManager(object):
16  def __init__(self, configs, supported_config_types=None):
17    """Manages file dependencies found locally or in cloud_storage.
18
19    Args:
20        configs: A list of instances of BaseConfig or it's subclasses, passed
21            in decreasing order of precedence.
22        supported_config_types: A list of whitelisted config_types.
23            No restrictions if None is specified.
24
25    Raises:
26        ValueError: If |configs| is not a list of instances of BaseConfig or
27            its subclasses.
28        UnsupportedConfigFormatError: If supported_config_types is specified and
29            configs contains a config not in the supported config_types.
30
31    Example: DependencyManager([config1, config2, config3])
32        No requirements on the type of Config, and any dependencies that have
33        local files for the same platform will first look in those from
34        config1, then those from config2, and finally those from config3.
35    """
36    if configs is None or type(configs) != list:
37      raise ValueError(
38          'Must supply a list of config files to DependencyManager')
39    # self._lookup_dict is a dictionary with the following format:
40    # { dependency1: {platform1: dependency_info1,
41    #                 platform2: dependency_info2}
42    #   dependency2: {platform1: dependency_info3,
43    #                  ...}
44    #   ...}
45    #
46    # Where the dependencies and platforms are strings, and the
47    # dependency_info's are DependencyInfo instances.
48    self._lookup_dict = {}
49    self.supported_configs = supported_config_types or []
50    for config in configs:
51      self._UpdateDependencies(config)
52
53
54  def FetchPathWithVersion(self, dependency, platform):
55    """Get a path to an executable for |dependency|, downloading as needed.
56
57    A path to a default executable may be returned if a platform specific
58    version is not specified in the config(s).
59
60    Args:
61        dependency: Name of the desired dependency, as given in the config(s)
62            used in this DependencyManager.
63        platform: Name of the platform the dependency will run on. Often of the
64            form 'os_architecture'. Must match those specified in the config(s)
65            used in this DependencyManager.
66    Returns:
67        <path>, <version> where:
68            <path> is the path to an executable of |dependency| that will run
69            on |platform|, downloading from cloud storage if needed.
70            <version> is the version of the executable at <path> or None.
71
72    Raises:
73        NoPathFoundError: If a local copy of the executable cannot be found and
74            a remote path could not be downloaded from cloud_storage.
75        CredentialsError: If cloud_storage credentials aren't configured.
76        PermissionError: If cloud_storage credentials are configured, but not
77            with an account that has permission to download the remote file.
78        NotFoundError: If the remote file does not exist where expected in
79            cloud_storage.
80        ServerError: If an internal server error is hit while downloading the
81            remote file.
82        CloudStorageError: If another error occured while downloading the remote
83            path.
84        FileNotFoundError: If an attempted download was otherwise unsuccessful.
85
86    """
87    dependency_info = self._GetDependencyInfo(dependency, platform)
88    if not dependency_info:
89      raise exceptions.NoPathFoundError(dependency, platform)
90    path = dependency_info.GetLocalPath()
91    version = None
92    if not path or not os.path.exists(path):
93      path = dependency_info.GetRemotePath()
94      if not path or not os.path.exists(path):
95        raise exceptions.NoPathFoundError(dependency, platform)
96      version = dependency_info.GetRemotePathVersion()
97    return path, version
98
99  def FetchPath(self, dependency, platform):
100    """Get a path to an executable for |dependency|, downloading as needed.
101
102    A path to a default executable may be returned if a platform specific
103    version is not specified in the config(s).
104
105    Args:
106        dependency: Name of the desired dependency, as given in the config(s)
107            used in this DependencyManager.
108        platform: Name of the platform the dependency will run on. Often of the
109            form 'os_architecture'. Must match those specified in the config(s)
110            used in this DependencyManager.
111    Returns:
112        A path to an executable of |dependency| that will run on |platform|,
113        downloading from cloud storage if needed.
114
115    Raises:
116        NoPathFoundError: If a local copy of the executable cannot be found and
117            a remote path could not be downloaded from cloud_storage.
118        CredentialsError: If cloud_storage credentials aren't configured.
119        PermissionError: If cloud_storage credentials are configured, but not
120            with an account that has permission to download the remote file.
121        NotFoundError: If the remote file does not exist where expected in
122            cloud_storage.
123        ServerError: If an internal server error is hit while downloading the
124            remote file.
125        CloudStorageError: If another error occured while downloading the remote
126            path.
127        FileNotFoundError: If an attempted download was otherwise unsuccessful.
128
129    """
130    path, _ = self.FetchPathWithVersion(dependency, platform)
131    return path
132
133  def LocalPath(self, dependency, platform):
134    """Get a path to a locally stored executable for |dependency|.
135
136    A path to a default executable may be returned if a platform specific
137    version is not specified in the config(s).
138    Will not download the executable.
139
140    Args:
141        dependency: Name of the desired dependency, as given in the config(s)
142            used in this DependencyManager.
143        platform: Name of the platform the dependency will run on. Often of the
144            form 'os_architecture'. Must match those specified in the config(s)
145            used in this DependencyManager.
146    Returns:
147        A path to an executable for |dependency| that will run on |platform|.
148
149    Raises:
150        NoPathFoundError: If a local copy of the executable cannot be found.
151    """
152    dependency_info = self._GetDependencyInfo(dependency, platform)
153    if not dependency_info:
154      raise exceptions.NoPathFoundError(dependency, platform)
155    local_path = dependency_info.GetLocalPath()
156    if not local_path or not os.path.exists(local_path):
157      raise exceptions.NoPathFoundError(dependency, platform)
158    return local_path
159
160  def PrefetchPaths(self, platform, dependencies=None, cloud_storage_retries=3):
161    if not dependencies:
162      dependencies = self._lookup_dict.keys()
163
164    skipped_deps = []
165    found_deps = []
166    missing_deps = []
167    for dependency in dependencies:
168      dependency_info = self._GetDependencyInfo(dependency, platform)
169      if not dependency_info:
170        # The dependency is only configured for other platforms.
171        skipped_deps.append(dependency)
172        logging.warning(
173            'Dependency %s not configured for platform %s. Skipping prefetch.',
174            dependency, platform)
175        continue
176      local_path = dependency_info.GetLocalPath()
177      if local_path:
178        found_deps.append(dependency)
179        continue
180      fetched_path = None
181      for _ in range(0, cloud_storage_retries + 1):
182        try:
183          fetched_path = dependency_info.GetRemotePath()
184        except exceptions.CloudStorageError:
185          continue
186        break
187      if fetched_path:
188        found_deps.append(dependency)
189      else:
190        missing_deps.append(dependency)
191        logging.error(
192            'Dependency %s could not be found or fetched from cloud storage for'
193            ' platform %s.', dependency, platform)
194    if missing_deps:
195      raise exceptions.NoPathFoundError(', '.join(missing_deps), platform)
196    return (found_deps, skipped_deps)
197
198  def _UpdateDependencies(self, config):
199    """Add the dependency information stored in |config| to this instance.
200
201    Args:
202        config: An instances of BaseConfig or a subclasses.
203
204    Raises:
205        UnsupportedConfigFormatError: If supported_config_types was specified
206        and config is not in the supported config_types.
207    """
208    if not isinstance(config, base_config.BaseConfig):
209      raise ValueError('Must use a BaseConfig or subclass instance with the '
210                       'DependencyManager.')
211    if (self.supported_configs and
212        config.GetConfigType() not in self.supported_configs):
213      raise exceptions.UnsupportedConfigFormatError(config.GetConfigType(),
214                                                    config.config_path)
215    for dep_info in config.IterDependencyInfo():
216      dependency = dep_info.dependency
217      platform = dep_info.platform
218      if dependency not in self._lookup_dict:
219        self._lookup_dict[dependency] = {}
220      if platform not in self._lookup_dict[dependency]:
221        self._lookup_dict[dependency][platform] = dep_info
222      else:
223        self._lookup_dict[dependency][platform].Update(dep_info)
224
225
226  def _GetDependencyInfo(self, dependency, platform):
227    """Get information for |dependency| on |platform|, or a default if needed.
228
229    Args:
230        dependency: Name of the desired dependency, as given in the config(s)
231            used in this DependencyManager.
232        platform: Name of the platform the dependency will run on. Often of the
233            form 'os_architecture'. Must match those specified in the config(s)
234            used in this DependencyManager.
235
236    Returns: The dependency_info for |dependency| on |platform| if it exists.
237        Or the default version of |dependency| if it exists, or None if neither
238        exist.
239    """
240    if not self._lookup_dict or dependency not in self._lookup_dict:
241      return None
242    dependency_dict = self._lookup_dict[dependency]
243    device_type = platform
244    if not device_type in dependency_dict:
245      device_type = DEFAULT_TYPE
246    return dependency_dict.get(device_type)
247
248