• 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 not isinstance(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        continue
173      local_path = dependency_info.GetLocalPath()
174      if local_path:
175        found_deps.append(dependency)
176        continue
177      fetched_path = None
178      cloud_storage_error = None
179      for _ in range(0, cloud_storage_retries + 1):
180        try:
181          fetched_path = dependency_info.GetRemotePath()
182        except exceptions.CloudStorageError as e:
183          cloud_storage_error = e
184        break
185      if fetched_path:
186        found_deps.append(dependency)
187      else:
188        missing_deps.append(dependency)
189        logging.error(
190            'Dependency %s could not be found or fetched from cloud storage for'
191            ' platform %s. Error: %s', dependency, platform,
192            cloud_storage_error)
193    if missing_deps:
194      raise exceptions.NoPathFoundError(', '.join(missing_deps), platform)
195    return (found_deps, skipped_deps)
196
197  def _UpdateDependencies(self, config):
198    """Add the dependency information stored in |config| to this instance.
199
200    Args:
201        config: An instances of BaseConfig or a subclasses.
202
203    Raises:
204        UnsupportedConfigFormatError: If supported_config_types was specified
205        and config is not in the supported config_types.
206    """
207    if not isinstance(config, base_config.BaseConfig):
208      raise ValueError('Must use a BaseConfig or subclass instance with the '
209                       'DependencyManager.')
210    if (self.supported_configs and
211        config.GetConfigType() not in self.supported_configs):
212      raise exceptions.UnsupportedConfigFormatError(config.GetConfigType(),
213                                                    config.config_path)
214    for dep_info in config.IterDependencyInfo():
215      dependency = dep_info.dependency
216      platform = dep_info.platform
217      if dependency not in self._lookup_dict:
218        self._lookup_dict[dependency] = {}
219      if platform not in self._lookup_dict[dependency]:
220        self._lookup_dict[dependency][platform] = dep_info
221      else:
222        self._lookup_dict[dependency][platform].Update(dep_info)
223
224
225  def _GetDependencyInfo(self, dependency, platform):
226    """Get information for |dependency| on |platform|, or a default if needed.
227
228    Args:
229        dependency: Name of the desired dependency, as given in the config(s)
230            used in this DependencyManager.
231        platform: Name of the platform the dependency will run on. Often of the
232            form 'os_architecture'. Must match those specified in the config(s)
233            used in this DependencyManager.
234
235    Returns: The dependency_info for |dependency| on |platform| if it exists.
236        Or the default version of |dependency| if it exists, or None if neither
237        exist.
238    """
239    if not self._lookup_dict or dependency not in self._lookup_dict:
240      return None
241    dependency_dict = self._lookup_dict[dependency]
242    device_type = platform
243    if not device_type in dependency_dict:
244      device_type = DEFAULT_TYPE
245    return dependency_dict.get(device_type)
246
247