1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
|
# 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
import stat
from catapult_base import cloud_storage
from catapult_base import support_binaries
from catapult_base.dependency_manager import base_config
from catapult_base.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 type(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 FetchPath(self, dependency, platform, try_support_binaries=False):
"""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.
try_support_binaries: True if support_binaries should be queried if the
dependency_manager was not initialized with data for |dependency|.
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.
"""
dependency_info = self._GetDependencyInfo(dependency, platform)
if not dependency_info:
logging.error(
'The dependency_manager was not initialized with the dependency.')
if not try_support_binaries:
raise exceptions.NoPathFoundError(dependency, platform)
# TODO(aiolos): Remove the support_binaries call and always raise
# NoPathFound once the binary dependencies are moved over to the new
# system.
# platform should be of the form '%s_%s' % (os_name, arch_name) when
# called from the binary_manager.
platform_parts = platform.split('_', 1)
assert len(platform_parts) == 2
platform_os, platform_arch = platform_parts
logging.info('Calling into support_binaries with dependency %s, platform '
'%s and arch %s.' % (dependency, platform_os,
platform_arch))
return support_binaries.FindPath(dependency, platform_arch,
platform_os)
path = self._LocalPath(dependency_info)
if not path or not os.path.exists(path):
path = self._CloudStoragePath(dependency_info)
if not path or not os.path.exists(path):
raise exceptions.NoPathFoundError(dependency, platform)
return path
def LocalPath(self, dependency, platform, try_support_binaries=False):
"""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.
try_support_binaries: True if support_binaries should be queried if the
dependency_manager was not initialized with data for |dependency|.
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.
"""
# TODO(aiolos): Remove the support_binaries call and always raise
# NoPathFound once the binary dependencies are moved over to the new
# system.
dependency_info = self._GetDependencyInfo(dependency, platform)
if not dependency_info:
logging.error(
'The dependency_manager was not initialized with the dependency.')
if not try_support_binaries:
raise exceptions.NoPathFoundError(dependency, platform)
return support_binaries.FindLocallyBuiltPath(dependency)
local_path = self._LocalPath(dependency_info)
if not local_path or not os.path.exists(local_path):
raise exceptions.NoPathFoundError(dependency, platform)
return local_path
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)
@staticmethod
def _LocalPath(dependency_info):
"""Return a path to a locally stored file for |dependency_info|.
Will not download the file.
Args:
dependency_info: A DependencyInfo instance for the dependency to be
found and the platform it should run on.
Returns: A path to a local file, or None if not found.
"""
if dependency_info:
paths = dependency_info.local_paths
for local_path in paths:
if os.path.exists(local_path):
return local_path
return None
@staticmethod
def _CloudStoragePath(dependency_info):
"""Return a path to a downloaded file for |dependency_info|.
May not download the file if it has already been downloaded.
Args:
dependency_info: A DependencyInfo instance for the dependency to be
found and the platform it should run on.
Returns: A path to an executable that was stored in cloud_storage, or None
if not found.
Raises:
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 needed file.
NotFoundError: If the needed file does not exist where expected in
cloud_storage.
ServerError: If an internal server error is hit while downloading the
needed file.
CloudStorageError: If another error occured while downloading the remote
path.
FileNotFoundError: If the download was otherwise unsuccessful.
"""
if not dependency_info:
return None
cs_path = dependency_info.cs_remote_path
cs_hash = dependency_info.cs_hash
cs_bucket = dependency_info.cs_bucket
download_path = dependency_info.download_path
if not cs_path or not cs_bucket or not cs_hash or not download_path:
return None
download_dir = os.path.dirname(download_path)
if not os.path.exists(download_dir):
os.makedirs(download_dir)
cloud_storage.GetIfHashChanged(cs_path, download_path, cs_bucket, cs_hash)
if not os.path.exists(download_path):
raise exceptions.FileNotFoundError(download_path)
#TODO(aiolos): Add support for unzipping files.
os.chmod(download_path,
stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP)
return os.path.abspath(download_path)
|