blob: 40bcc258adb2fd5bb3ea4cded53646c2b55c0dc5 [file] [log] [blame]
Chris Sosa968a1062013-08-02 17:42:50 -07001#!/usr/bin/env python
2
Chris Sosa47a7d4e2012-03-28 11:26:55 -07003# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Module containing classes that wrap artifact downloads."""
8
Chris Sosa47a7d4e2012-03-28 11:26:55 -07009import os
Dan Shi6e50c722013-08-19 15:05:06 -070010import pickle
Chris Sosa47a7d4e2012-03-28 11:26:55 -070011import shutil
12import subprocess
13
Chris Sosa76e44b92013-01-31 12:11:38 -080014import artifact_info
15import common_util
joychen3cb228e2013-06-12 12:13:13 -070016import devserver_constants
Chris Sosa47a7d4e2012-03-28 11:26:55 -070017import gsutil_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070018import log_util
Chris Sosa47a7d4e2012-03-28 11:26:55 -070019
20
Chris Sosa76e44b92013-01-31 12:11:38 -080021_AU_BASE = 'au'
22_NTON_DIR_SUFFIX = '_nton'
23_MTON_DIR_SUFFIX = '_mton'
24
25############ Actual filenames of artifacts in Google Storage ############
26
27AU_SUITE_FILE = 'au_control.tar.bz2'
Chris Sosa968a1062013-08-02 17:42:50 -070028PAYGEN_AU_SUITE_FILE_TEMPLATE = 'paygen_au_%(channel)s_control.tar.bz2'
Chris Sosa76e44b92013-01-31 12:11:38 -080029AUTOTEST_FILE = 'autotest.tar'
30AUTOTEST_COMPRESSED_FILE = 'autotest.tar.bz2'
31DEBUG_SYMBOLS_FILE = 'debug.tgz'
beepsc3d0f872013-07-31 21:50:40 -070032FACTORY_FILE = 'ChromeOS-factory.*zip'
Chris Sosa76e44b92013-01-31 12:11:38 -080033FIRMWARE_FILE = 'firmware_from_source.tar.bz2'
34IMAGE_FILE = 'image.zip'
Chris Sosa76e44b92013-01-31 12:11:38 -080035TEST_SUITES_FILE = 'test_suites.tar.bz2'
36
37_build_artifact_locks = common_util.LockDict()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070038
39
40class ArtifactDownloadError(Exception):
41 """Error used to signify an issue processing an artifact."""
42 pass
43
44
Gilad Arnoldc65330c2012-09-20 15:17:48 -070045class BuildArtifact(log_util.Loggable):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070046 """Wrapper around an artifact to download from gsutil.
47
48 The purpose of this class is to download objects from Google Storage
49 and install them to a local directory. There are two main functions, one to
50 download/prepare the artifacts in to a temporary staging area and the second
51 to stage it into its final destination.
Chris Sosa76e44b92013-01-31 12:11:38 -080052
53 Class members:
54 archive_url = archive_url
55 name: Name given for artifact -- either a regexp or name of the artifact in
56 gs. If a regexp, is modified to actual name before call to _Download.
57 build: The version of the build i.e. R26-2342.0.0.
58 marker_name: Name used to define the lock marker for the artifacts to
59 prevent it from being re-downloaded. By default based on name
60 but can be overriden by children.
Dan Shi6e50c722013-08-19 15:05:06 -070061 exception_file_path: Path to a file containing the serialized exception,
62 which was raised in Process method. The file is located
63 in the parent folder of install_dir, since the
64 install_dir will be deleted if the build does not
65 existed.
joychen0a8e34e2013-06-24 17:58:36 -070066 install_path: Path to artifact.
Chris Sosa76e44b92013-01-31 12:11:38 -080067 install_dir: The final location where the artifact should be staged to.
68 single_name: If True the name given should only match one item. Note, if not
69 True, self.name will become a list of items returned.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070070 """
Chris Sosa76e44b92013-01-31 12:11:38 -080071 def __init__(self, install_dir, archive_url, name, build):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070072 """Args:
Chris Sosa76e44b92013-01-31 12:11:38 -080073 install_dir: Where to install the artifact.
74 archive_url: The Google Storage path to find the artifact.
75 name: Identifying name to be used to find/store the artifact.
76 build: The name of the build e.g. board/release.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070077 """
Chris Sosa6a3697f2013-01-29 16:44:43 -080078 super(BuildArtifact, self).__init__()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070079
Chris Sosa76e44b92013-01-31 12:11:38 -080080 # In-memory lock to keep the devserver from colliding with itself while
81 # attempting to stage the same artifact.
82 self._process_lock = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -070083
Chris Sosa76e44b92013-01-31 12:11:38 -080084 self.archive_url = archive_url
85 self.name = name
86 self.build = build
Chris Sosa47a7d4e2012-03-28 11:26:55 -070087
Chris Sosa76e44b92013-01-31 12:11:38 -080088 self.marker_name = '.' + self._SanitizeName(name)
Chris Sosa47a7d4e2012-03-28 11:26:55 -070089
Dan Shi6e50c722013-08-19 15:05:06 -070090 exception_file_name = ('.' + self._SanitizeName(build) + self.marker_name +
91 '.exception')
92 # The exception file needs to be located in parent folder, since the
93 # install_dir will be deleted is the build does not exist.
94 self.exception_file_path = os.path.join(os.path.dirname(install_dir),
95 exception_file_name)
96
joychen0a8e34e2013-06-24 17:58:36 -070097 self.install_path = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -070098
Chris Sosa76e44b92013-01-31 12:11:38 -080099 self.install_dir = install_dir
100
101 self.single_name = True
102
103 @staticmethod
104 def _SanitizeName(name):
105 """Sanitizes name to be used for creating a file on the filesystem.
106
107 '.','/' and '*' have special meaning in FS lingo. Replace them with words.
108 """
109 return name.replace('*', 'STAR').replace('.', 'DOT').replace('/', 'SLASH')
110
Dan Shif8eb0d12013-08-01 17:52:06 -0700111 def ArtifactStaged(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800112 """Returns True if artifact is already staged."""
113 return os.path.exists(os.path.join(self.install_dir, self.marker_name))
114
115 def _MarkArtifactStaged(self):
116 """Marks the artifact as staged."""
117 with open(os.path.join(self.install_dir, self.marker_name), 'w') as f:
118 f.write('')
119
Chris Sosac4e87842013-08-16 18:04:14 -0700120 def WaitForArtifactToExist(self, timeout, update_name=True):
121 """Waits for artifact to exist and sets self.name to appropriate name.
122
123 Args:
124 update_name: If False, don't actually update self.name.
125 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800126 names = gsutil_util.GetGSNamesWithWait(
127 self.name, self.archive_url, str(self), single_item=self.single_name,
128 timeout=timeout)
129 if not names:
130 raise ArtifactDownloadError('Could not find %s in Google Storage' %
131 self.name)
132
133 if self.single_name:
134 if len(names) > 1:
135 raise ArtifactDownloadError('Too many artifacts match %s' % self.name)
136
Chris Sosac4e87842013-08-16 18:04:14 -0700137 new_name = names[0]
Chris Sosa76e44b92013-01-31 12:11:38 -0800138 else:
Chris Sosac4e87842013-08-16 18:04:14 -0700139 new_name = names
140
141 if update_name:
142 self.name = new_name
Chris Sosa76e44b92013-01-31 12:11:38 -0800143
144 def _Download(self):
joychen0a8e34e2013-06-24 17:58:36 -0700145 """Downloads artifact from Google Storage to a local directory."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800146 gs_path = '/'.join([self.archive_url, self.name])
joychen0a8e34e2013-06-24 17:58:36 -0700147 self.install_path = os.path.join(self.install_dir, self.name)
148 gsutil_util.DownloadFromGS(gs_path, self.install_path)
Chris Sosa76e44b92013-01-31 12:11:38 -0800149
joychen0a8e34e2013-06-24 17:58:36 -0700150 def _Setup(self):
151 """For tarball like artifacts, extracts and prepares contents."""
152 pass
153
Dan Shi6e50c722013-08-19 15:05:06 -0700154 def _ClearException(self):
155 """Delete any existing exception saved for this artifact."""
156 if os.path.exists(self.exception_file_path):
157 os.remove(self.exception_file_path)
158
159 def _SaveException(self, e):
160 """Save the exception to a file for downloader.IsStaged to retrieve.
161
162 @param e: Exception object to be saved.
163 """
164 with open(self.exception_file_path, 'w') as f:
165 pickle.dump(e, f)
166
167 def GetException(self):
168 """Retrieve any exception that was raised in Process method.
169
170 @return: An Exception object that was raised when trying to process the
171 artifact. Return None if no exception was found.
172 """
173 if not os.path.exists(self.exception_file_path):
174 return None
175 with open(self.exception_file_path, 'r') as f:
176 return pickle.load(f)
Chris Sosa76e44b92013-01-31 12:11:38 -0800177
178 def Process(self, no_wait):
179 """Main call point to all artifacts. Downloads and Stages artifact.
180
181 Downloads and Stages artifact from Google Storage to the install directory
182 specified in the constructor. It multi-thread safe and does not overwrite
183 the artifact if it's already been downloaded or being downloaded. After
184 processing, leaves behind a marker to indicate to future invocations that
185 the artifact has already been staged based on the name of the artifact.
186
187 Do not override as it modifies important private variables, ensures thread
188 safety, and maintains cache semantics.
189
190 Note: this may be a blocking call when the artifact is already in the
191 process of being staged.
192
193 Args:
194 no_wait: If True, don't block waiting for artifact to exist if we fail to
195 immediately find it.
196
197 Raises:
198 ArtifactDownloadError: If the artifact fails to download from Google
199 Storage for any reason or that the regexp
200 defined by name is not specific enough.
201 """
202 if not self._process_lock:
203 self._process_lock = _build_artifact_locks.lock(
204 os.path.join(self.install_dir, self.name))
205
206 with self._process_lock:
207 common_util.MkDirP(self.install_dir)
Dan Shif8eb0d12013-08-01 17:52:06 -0700208 if not self.ArtifactStaged():
Dan Shi6e50c722013-08-19 15:05:06 -0700209 try:
210 # Delete any existing exception saved for this artifact.
211 self._ClearException()
212 # If the artifact should already have been uploaded, don't waste
213 # cycles waiting around for it to exist.
214 timeout = 1 if no_wait else 10
215 self.WaitForArtifactToExist(timeout)
216 self._Download()
217 self._Setup()
218 self._MarkArtifactStaged()
219 except Exception as e:
220 # Save the exception to a file for downloader.IsStaged to retrieve.
221 self._SaveException(e)
222 raise
Chris Sosa76e44b92013-01-31 12:11:38 -0800223 else:
224 self._Log('%s is already staged.', self)
225
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700226 def __str__(self):
227 """String representation for the download."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800228 return '->'.join(['%s/%s' % (self.archive_url, self.name),
joychen0a8e34e2013-06-24 17:58:36 -0700229 self.install_dir])
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700230
Chris Sosab26b1202013-08-16 16:40:55 -0700231 def __repr__(self):
232 return str(self)
233
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700234
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700235class AUTestPayloadBuildArtifact(BuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700236 """Wrapper for AUTest delta payloads which need additional setup."""
joychen0a8e34e2013-06-24 17:58:36 -0700237 def _Setup(self):
238 super(AUTestPayloadBuildArtifact, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700239
Chris Sosa76e44b92013-01-31 12:11:38 -0800240 # Rename to update.gz.
241 install_path = os.path.join(self.install_dir, self.name)
joychen3cb228e2013-06-12 12:13:13 -0700242 new_install_path = os.path.join(self.install_dir,
joychen7c2054a2013-07-25 11:14:07 -0700243 devserver_constants.UPDATE_FILE)
Chris Sosa76e44b92013-01-31 12:11:38 -0800244 shutil.move(install_path, new_install_path)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700245
246
Chris Sosa76e44b92013-01-31 12:11:38 -0800247# TODO(sosa): Change callers to make this artifact more sane.
248class DeltaPayloadsArtifact(BuildArtifact):
249 """Delta payloads from the archive_url.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700250
Chris Sosa76e44b92013-01-31 12:11:38 -0800251 This artifact is super strange. It custom handles directories and
252 pulls in all delta payloads. We can't specify exactly what we want
253 because unlike other artifacts, this one does not conform to something a
254 client might know. The client doesn't know the version of n-1 or whether it
255 was even generated.
256 """
257 def __init__(self, *args):
258 super(DeltaPayloadsArtifact, self).__init__(*args)
259 self.single_name = False # Expect multiple deltas
260 nton_name = 'chromeos_%s%s' % (self.build, self.name)
261 mton_name = 'chromeos_(?!%s)%s' % (self.build, self.name)
262 nton_install_dir = os.path.join(self.install_dir, _AU_BASE,
263 self.build + _NTON_DIR_SUFFIX)
264 mton_install_dir = os.path.join(self.install_dir, _AU_BASE,
265 self.build + _MTON_DIR_SUFFIX)
266 self._sub_artifacts = [
267 AUTestPayloadBuildArtifact(mton_install_dir, self.archive_url,
268 mton_name, self.build),
269 AUTestPayloadBuildArtifact(nton_install_dir, self.archive_url,
270 nton_name, self.build)]
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700271
Chris Sosa76e44b92013-01-31 12:11:38 -0800272 def _Download(self):
joychen0a8e34e2013-06-24 17:58:36 -0700273 """With sub-artifacts we do everything in _Setup()."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800274 pass
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700275
joychen0a8e34e2013-06-24 17:58:36 -0700276 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800277 """Process each sub-artifact. Only error out if none can be found."""
278 for artifact in self._sub_artifacts:
279 try:
280 artifact.Process(no_wait=True)
281 # Setup symlink so that AU will work for this payload.
282 os.symlink(
joychen25d25972013-07-30 14:54:16 -0700283 os.path.join(os.pardir, os.pardir,
joychen121fc9b2013-08-02 14:30:30 -0700284 devserver_constants.STATEFUL_FILE),
joychen25d25972013-07-30 14:54:16 -0700285 os.path.join(artifact.install_dir,
joychen121fc9b2013-08-02 14:30:30 -0700286 devserver_constants.STATEFUL_FILE))
Chris Sosa76e44b92013-01-31 12:11:38 -0800287 except ArtifactDownloadError as e:
288 self._Log('Could not process %s: %s', artifact, e)
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700289
Chris Sosa76e44b92013-01-31 12:11:38 -0800290
291class BundledBuildArtifact(BuildArtifact):
292 """A single build artifact bundle e.g. zip file or tar file."""
293 def __init__(self, install_dir, archive_url, name, build,
294 files_to_extract=None, exclude=None):
295 """Takes BuildArtifacts are with two additional args.
296
297 Additional args:
298 files_to_extract: A list of files to extract. If set to None, extract
299 all files.
300 exclude: A list of files to exclude. If None, no files are excluded.
301 """
302 super(BundledBuildArtifact, self).__init__(install_dir, archive_url, name,
303 build)
304 self._files_to_extract = files_to_extract
305 self._exclude = exclude
306
307 # We modify the marker so that it is unique to what was staged.
308 if files_to_extract:
309 self.marker_name = self._SanitizeName(
310 '_'.join(['.' + self.name] + files_to_extract))
311
312 def _Extract(self):
313 """Extracts the bundle into install_dir. Must be overridden.
314
315 If set, uses files_to_extract to only extract those items. If set, use
316 exclude to exclude specific files.
317 """
318 raise NotImplementedError()
319
joychen0a8e34e2013-06-24 17:58:36 -0700320 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800321 self._Extract()
322
323
324class TarballBuildArtifact(BundledBuildArtifact):
325 """Artifact for tar and tarball files."""
326
327 def _Extract(self):
328 """Extracts a tarball using tar.
329
330 Detects whether the tarball is compressed or not based on the file
331 extension and extracts the tarball into the install_path.
332 """
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700333 try:
joychen0a8e34e2013-06-24 17:58:36 -0700334 common_util.ExtractTarball(self.install_path, self.install_dir,
Simran Basi4baad082013-02-14 13:39:18 -0800335 files_to_extract=self._files_to_extract,
336 excluded_files=self._exclude)
337 except common_util.CommonUtilError as e:
338 raise ArtifactDownloadError(str(e))
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700339
340
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700341class AutotestTarballBuildArtifact(TarballBuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700342 """Wrapper around the autotest tarball to download from gsutil."""
343
joychen0a8e34e2013-06-24 17:58:36 -0700344 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800345 """Extracts the tarball into the install path excluding test suites."""
joychen0a8e34e2013-06-24 17:58:36 -0700346 super(AutotestTarballBuildArtifact, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700347
Chris Sosa76e44b92013-01-31 12:11:38 -0800348 # Deal with older autotest packages that may not be bundled.
joychen3cb228e2013-06-12 12:13:13 -0700349 autotest_dir = os.path.join(self.install_dir,
350 devserver_constants.AUTOTEST_DIR)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700351 autotest_pkgs_dir = os.path.join(autotest_dir, 'packages')
352 if not os.path.exists(autotest_pkgs_dir):
353 os.makedirs(autotest_pkgs_dir)
354
355 if not os.path.exists(os.path.join(autotest_pkgs_dir, 'packages.checksum')):
Chris Sosa76e44b92013-01-31 12:11:38 -0800356 cmd = ['autotest/utils/packager.py', 'upload', '--repository',
357 autotest_pkgs_dir, '--all']
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700358 try:
joychen0a8e34e2013-06-24 17:58:36 -0700359 subprocess.check_call(cmd, cwd=self.install_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700360 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800361 raise ArtifactDownloadError(
362 'Failed to create autotest packages!:\n%s' % e)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700363 else:
Gilad Arnoldf5843132012-09-25 00:31:20 -0700364 self._Log('Using pre-generated packages from autotest')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700365
Chris Masone816e38c2012-05-02 12:22:36 -0700366
Chris Sosa76e44b92013-01-31 12:11:38 -0800367class ZipfileBuildArtifact(BundledBuildArtifact):
368 """A downloadable artifact that is a zipfile."""
Chris Masone816e38c2012-05-02 12:22:36 -0700369
Chris Sosa76e44b92013-01-31 12:11:38 -0800370 def _Extract(self):
371 """Extracts files into the install path."""
372 # Unzip is weird. It expects its args before any excepts and expects its
373 # excepts in a list following the -x.
joychen0a8e34e2013-06-24 17:58:36 -0700374 cmd = ['unzip', '-o', self.install_path, '-d', self.install_dir]
Chris Sosa76e44b92013-01-31 12:11:38 -0800375 if self._files_to_extract:
376 cmd.extend(self._files_to_extract)
Chris Masone816e38c2012-05-02 12:22:36 -0700377
Chris Sosa76e44b92013-01-31 12:11:38 -0800378 if self._exclude:
379 cmd.append('-x')
380 cmd.extend(self._exclude)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700381
382 try:
Chris Sosa76e44b92013-01-31 12:11:38 -0800383 subprocess.check_call(cmd)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700384 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800385 raise ArtifactDownloadError(
386 'An error occurred when attempting to unzip %s:\n%s' %
joychen0a8e34e2013-06-24 17:58:36 -0700387 (self.install_path, e))
Gilad Arnold6f99b982012-09-12 10:49:40 -0700388
Gilad Arnold6f99b982012-09-12 10:49:40 -0700389
Chris Sosa76e44b92013-01-31 12:11:38 -0800390class ImplDescription(object):
391 """Data wrapper that describes an artifact's implementation."""
392 def __init__(self, artifact_class, name, *additional_args):
393 """Constructor:
394
395 Args:
396 artifact_class: BuildArtifact class to use for the artifact.
397 name: name to use to identify artifact (see BuildArtifact.name)
398 additional_args: If sub-class uses additional args, these are passed
399 through to them.
400 """
401 self.artifact_class = artifact_class
402 self.name = name
403 self.additional_args = additional_args
404
Chris Sosa968a1062013-08-02 17:42:50 -0700405 def __repr__(self):
406 return '%s_%s' % (self.artifact_class, self.name)
407
Chris Sosa76e44b92013-01-31 12:11:38 -0800408
409# Maps artifact names to their implementation description.
410# Please note, it is good practice to use constants for these names if you're
411# going to re-use the names ANYWHERE else in the devserver code.
412ARTIFACT_IMPLEMENTATION_MAP = {
413 artifact_info.FULL_PAYLOAD:
414 ImplDescription(AUTestPayloadBuildArtifact, '.*_full_.*'),
415 artifact_info.DELTA_PAYLOADS:
416 ImplDescription(DeltaPayloadsArtifact, '.*_delta_.*'),
417 artifact_info.STATEFUL_PAYLOAD:
joychen121fc9b2013-08-02 14:30:30 -0700418 ImplDescription(BuildArtifact, devserver_constants.STATEFUL_FILE),
Chris Sosa76e44b92013-01-31 12:11:38 -0800419
420 artifact_info.BASE_IMAGE:
421 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE,
joychen921e1fb2013-06-28 11:12:20 -0700422 [devserver_constants.BASE_IMAGE_FILE]),
Chris Sosa76e44b92013-01-31 12:11:38 -0800423 artifact_info.RECOVERY_IMAGE:
joychen921e1fb2013-06-28 11:12:20 -0700424 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE,
425 [devserver_constants.RECOVERY_IMAGE_FILE]),
Chris Sosa76e44b92013-01-31 12:11:38 -0800426 artifact_info.TEST_IMAGE:
joychen921e1fb2013-06-28 11:12:20 -0700427 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE,
428 [devserver_constants.TEST_IMAGE_FILE]),
Chris Sosa76e44b92013-01-31 12:11:38 -0800429
430 artifact_info.AUTOTEST:
431 ImplDescription(AutotestTarballBuildArtifact, AUTOTEST_FILE, None,
432 ['autotest/test_suites']),
433 artifact_info.TEST_SUITES:
434 ImplDescription(TarballBuildArtifact, TEST_SUITES_FILE),
435 artifact_info.AU_SUITE:
Chris Sosaa56c4032013-03-17 21:59:54 -0700436 ImplDescription(TarballBuildArtifact, AU_SUITE_FILE),
Chris Sosa76e44b92013-01-31 12:11:38 -0800437
438 artifact_info.FIRMWARE:
439 ImplDescription(BuildArtifact, FIRMWARE_FILE),
440 artifact_info.SYMBOLS:
441 ImplDescription(TarballBuildArtifact, DEBUG_SYMBOLS_FILE,
442 ['debug/breakpad']),
beepsc3d0f872013-07-31 21:50:40 -0700443
444 artifact_info.FACTORY_IMAGE:
445 ImplDescription(ZipfileBuildArtifact, FACTORY_FILE,
446 [devserver_constants.FACTORY_IMAGE_FILE])
Chris Sosa76e44b92013-01-31 12:11:38 -0800447}
448
Chris Sosa968a1062013-08-02 17:42:50 -0700449# Add all the paygen_au artifacts in one go.
450ARTIFACT_IMPLEMENTATION_MAP.update({
451 artifact_info.PAYGEN_AU_SUITE_TEMPLATE % { 'channel': c }: ImplDescription(
452 TarballBuildArtifact, PAYGEN_AU_SUITE_FILE_TEMPLATE % { 'channel': c })
453 for c in devserver_constants.CHANNELS
454})
455
Chris Sosa76e44b92013-01-31 12:11:38 -0800456
457class ArtifactFactory(object):
458 """A factory class that generates build artifacts from artifact names."""
459
Chris Sosa6b0c6172013-08-05 17:01:33 -0700460 def __init__(self, download_dir, archive_url, artifacts, files,
461 build):
Chris Sosa76e44b92013-01-31 12:11:38 -0800462 """Initalizes the member variables for the factory.
463
464 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800465 archive_url: the Google Storage url of the bucket where the debug
466 symbols for the desired build are stored.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700467 artifacts: List of artifacts to stage. These artifacts must be
468 defined in artifact_info.py and have a mapping in the
469 ARTIFACT_IMPLEMENTATION_MAP.
470 files: List of files to stage. These files are just downloaded and staged
471 as files into the download_dir.
Chris Sosa76e44b92013-01-31 12:11:38 -0800472 build: The name of the build.
473 """
joychen0a8e34e2013-06-24 17:58:36 -0700474 self.download_dir = download_dir
Chris Sosa76e44b92013-01-31 12:11:38 -0800475 self.archive_url = archive_url
Chris Sosa6b0c6172013-08-05 17:01:33 -0700476 self.artifacts = artifacts
477 self.files = files
Chris Sosa76e44b92013-01-31 12:11:38 -0800478 self.build = build
479
480 @staticmethod
Chris Sosa6b0c6172013-08-05 17:01:33 -0700481 def _GetDescriptionComponents(name, is_artifact):
482 """Returns a tuple of for BuildArtifact class, name, and additional args.
483
484 Raises: KeyError if artifact doesn't exist in ARTIFACT_IMPLEMENTATION_MAP.
485 """
486
487 if is_artifact:
488 description = ARTIFACT_IMPLEMENTATION_MAP[name]
489 else:
490 description = ImplDescription(BuildArtifact, name)
491
Chris Sosa76e44b92013-01-31 12:11:38 -0800492 return (description.artifact_class, description.name,
493 description.additional_args)
494
Chris Sosa6b0c6172013-08-05 17:01:33 -0700495 def _Artifacts(self, names, is_artifact):
496 """Returns an iterable of BuildArtifacts from |names|.
497
498 If is_artifact is true, then these names define artifacts that must exist in
499 the ARTIFACT_IMPLEMENTATION_MAP. Otherwise, treat as filenames to stage as
500 basic BuildArtifacts.
501
502 Raises: KeyError if artifact doesn't exist in ARTIFACT_IMPLEMENTATION_MAP.
503 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800504 artifacts = []
Chris Sosa6b0c6172013-08-05 17:01:33 -0700505 for name in names:
Chris Sosa76e44b92013-01-31 12:11:38 -0800506 artifact_class, path, args = self._GetDescriptionComponents(
Chris Sosa6b0c6172013-08-05 17:01:33 -0700507 name, is_artifact)
joychen0a8e34e2013-06-24 17:58:36 -0700508 artifacts.append(artifact_class(self.download_dir, self.archive_url, path,
Chris Sosa76e44b92013-01-31 12:11:38 -0800509 self.build, *args))
510
511 return artifacts
512
513 def RequiredArtifacts(self):
Chris Sosa6b0c6172013-08-05 17:01:33 -0700514 """Returns an iterable of BuildArtifacts for the factory's artifacts.
515
516 Raises: KeyError if artifact doesn't exist in ARTIFACT_IMPLEMENTATION_MAP.
517 """
518 artifacts = []
519 if self.artifacts:
520 artifacts.extend(self._Artifacts(self.artifacts, True))
521 if self.files:
522 artifacts.extend(self._Artifacts(self.files, False))
523
524 return artifacts
Chris Sosa76e44b92013-01-31 12:11:38 -0800525
526 def OptionalArtifacts(self):
Chris Sosa6b0c6172013-08-05 17:01:33 -0700527 """Returns an iterable of BuildArtifacts that should be cached.
528
529 Raises: KeyError if an optional artifact doesn't exist in
530 ARTIFACT_IMPLEMENTATION_MAP yet defined in
531 artifact_info.REQUESTED_TO_OPTIONAL_MAP.
532 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800533 optional_names = set()
534 for artifact_name, optional_list in (
535 artifact_info.REQUESTED_TO_OPTIONAL_MAP.iteritems()):
536 # We are already downloading it.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700537 if artifact_name in self.artifacts:
Chris Sosa76e44b92013-01-31 12:11:38 -0800538 optional_names = optional_names.union(optional_list)
539
Chris Sosa6b0c6172013-08-05 17:01:33 -0700540 return self._Artifacts(optional_names - set(self.artifacts), True)
Chris Sosa968a1062013-08-02 17:42:50 -0700541
542
543# A simple main to verify correctness of the artifact map when making simple
544# name changes.
545if __name__ == '__main__':
546 print 'ARTIFACT IMPLEMENTATION MAP (for debugging)'
547 print 'FORMAT: ARTIFACT -> IMPLEMENTATION (<class>_file)'
548 for key, value in sorted(ARTIFACT_IMPLEMENTATION_MAP.items()):
549 print '%s -> %s' % (key, value)