blob: ca85bacce536603604c4f1347c95d365a275b3eb [file] [log] [blame]
Gilad Arnold950569b2013-08-27 14:38:01 -07001#!/usr/bin/python
Chris Sosa968a1062013-08-02 17:42:50 -07002
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
Gilad Arnold950569b2013-08-27 14:38:01 -070011import re
Chris Sosa47a7d4e2012-03-28 11:26:55 -070012import shutil
13import subprocess
14
Chris Sosa76e44b92013-01-31 12:11:38 -080015import artifact_info
16import common_util
joychen3cb228e2013-06-12 12:13:13 -070017import devserver_constants
Chris Sosa47a7d4e2012-03-28 11:26:55 -070018import gsutil_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070019import log_util
Chris Sosa47a7d4e2012-03-28 11:26:55 -070020
21
Chris Sosa76e44b92013-01-31 12:11:38 -080022_AU_BASE = 'au'
23_NTON_DIR_SUFFIX = '_nton'
24_MTON_DIR_SUFFIX = '_mton'
25
26############ Actual filenames of artifacts in Google Storage ############
27
28AU_SUITE_FILE = 'au_control.tar.bz2'
Chris Sosa968a1062013-08-02 17:42:50 -070029PAYGEN_AU_SUITE_FILE_TEMPLATE = 'paygen_au_%(channel)s_control.tar.bz2'
Chris Sosa76e44b92013-01-31 12:11:38 -080030AUTOTEST_FILE = 'autotest.tar'
31AUTOTEST_COMPRESSED_FILE = 'autotest.tar.bz2'
32DEBUG_SYMBOLS_FILE = 'debug.tgz'
Gilad Arnold950569b2013-08-27 14:38:01 -070033FACTORY_FILE = 'ChromeOS-factory*zip'
Chris Sosa76e44b92013-01-31 12:11:38 -080034FIRMWARE_FILE = 'firmware_from_source.tar.bz2'
35IMAGE_FILE = 'image.zip'
Chris Sosa76e44b92013-01-31 12:11:38 -080036TEST_SUITES_FILE = 'test_suites.tar.bz2'
37
38_build_artifact_locks = common_util.LockDict()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070039
40
41class ArtifactDownloadError(Exception):
42 """Error used to signify an issue processing an artifact."""
43 pass
44
45
Gilad Arnoldc65330c2012-09-20 15:17:48 -070046class BuildArtifact(log_util.Loggable):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070047 """Wrapper around an artifact to download from gsutil.
48
49 The purpose of this class is to download objects from Google Storage
50 and install them to a local directory. There are two main functions, one to
51 download/prepare the artifacts in to a temporary staging area and the second
52 to stage it into its final destination.
Chris Sosa76e44b92013-01-31 12:11:38 -080053
Gilad Arnold950569b2013-08-27 14:38:01 -070054 IMPORTANT! (i) `name' is a glob expression by default (and not a regex), be
55 attentive when adding new artifacts; (ii) name matching semantics differ
56 between a glob (full name string match) and a regex (partial match).
57
Chris Sosa76e44b92013-01-31 12:11:38 -080058 Class members:
Gilad Arnold950569b2013-08-27 14:38:01 -070059 archive_url: An archive URL.
60 name: Name given for artifact; in fact, it is a pattern that captures the
61 names of files contained in the artifact. This can either be an
62 ordinary shell-style glob (the default), or a regular expression (if
63 is_regex_name is True).
64 is_regex_name: Whether the name value is a regex (default: glob).
Chris Sosa76e44b92013-01-31 12:11:38 -080065 build: The version of the build i.e. R26-2342.0.0.
66 marker_name: Name used to define the lock marker for the artifacts to
67 prevent it from being re-downloaded. By default based on name
68 but can be overriden by children.
Dan Shi6e50c722013-08-19 15:05:06 -070069 exception_file_path: Path to a file containing the serialized exception,
70 which was raised in Process method. The file is located
71 in the parent folder of install_dir, since the
72 install_dir will be deleted if the build does not
73 existed.
joychen0a8e34e2013-06-24 17:58:36 -070074 install_path: Path to artifact.
Chris Sosa76e44b92013-01-31 12:11:38 -080075 install_dir: The final location where the artifact should be staged to.
76 single_name: If True the name given should only match one item. Note, if not
77 True, self.name will become a list of items returned.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070078 """
Gilad Arnold950569b2013-08-27 14:38:01 -070079
80 def __init__(self, install_dir, archive_url, name, build,
81 is_regex_name=False):
82 """Constructor.
83
84 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -080085 install_dir: Where to install the artifact.
86 archive_url: The Google Storage path to find the artifact.
87 name: Identifying name to be used to find/store the artifact.
88 build: The name of the build e.g. board/release.
Gilad Arnold950569b2013-08-27 14:38:01 -070089 is_regex_name: Whether the name pattern is a regex (default: glob).
Chris Sosa47a7d4e2012-03-28 11:26:55 -070090 """
Chris Sosa6a3697f2013-01-29 16:44:43 -080091 super(BuildArtifact, self).__init__()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070092
Chris Sosa76e44b92013-01-31 12:11:38 -080093 # In-memory lock to keep the devserver from colliding with itself while
94 # attempting to stage the same artifact.
95 self._process_lock = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -070096
Chris Sosa76e44b92013-01-31 12:11:38 -080097 self.archive_url = archive_url
98 self.name = name
Gilad Arnold950569b2013-08-27 14:38:01 -070099 self.is_regex_name = is_regex_name
Chris Sosa76e44b92013-01-31 12:11:38 -0800100 self.build = build
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700101
Chris Sosa76e44b92013-01-31 12:11:38 -0800102 self.marker_name = '.' + self._SanitizeName(name)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700103
Dan Shi6e50c722013-08-19 15:05:06 -0700104 exception_file_name = ('.' + self._SanitizeName(build) + self.marker_name +
105 '.exception')
106 # The exception file needs to be located in parent folder, since the
107 # install_dir will be deleted is the build does not exist.
108 self.exception_file_path = os.path.join(os.path.dirname(install_dir),
Gilad Arnold950569b2013-08-27 14:38:01 -0700109 exception_file_name)
Dan Shi6e50c722013-08-19 15:05:06 -0700110
joychen0a8e34e2013-06-24 17:58:36 -0700111 self.install_path = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700112
Chris Sosa76e44b92013-01-31 12:11:38 -0800113 self.install_dir = install_dir
114
115 self.single_name = True
116
117 @staticmethod
118 def _SanitizeName(name):
119 """Sanitizes name to be used for creating a file on the filesystem.
120
121 '.','/' and '*' have special meaning in FS lingo. Replace them with words.
Gilad Arnold950569b2013-08-27 14:38:01 -0700122
123 Args:
124 name: A file name/path.
125 Returns:
126 The sanitized name/path.
Chris Sosa76e44b92013-01-31 12:11:38 -0800127 """
128 return name.replace('*', 'STAR').replace('.', 'DOT').replace('/', 'SLASH')
129
Dan Shif8eb0d12013-08-01 17:52:06 -0700130 def ArtifactStaged(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800131 """Returns True if artifact is already staged."""
132 return os.path.exists(os.path.join(self.install_dir, self.marker_name))
133
134 def _MarkArtifactStaged(self):
135 """Marks the artifact as staged."""
136 with open(os.path.join(self.install_dir, self.marker_name), 'w') as f:
137 f.write('')
138
Chris Sosac4e87842013-08-16 18:04:14 -0700139 def WaitForArtifactToExist(self, timeout, update_name=True):
140 """Waits for artifact to exist and sets self.name to appropriate name.
141
142 Args:
Gilad Arnold950569b2013-08-27 14:38:01 -0700143 timeout: How long to wait for artifact to become available.
Chris Sosac4e87842013-08-16 18:04:14 -0700144 update_name: If False, don't actually update self.name.
Gilad Arnold950569b2013-08-27 14:38:01 -0700145 Raises:
146 ArtifactDownloadError: An error occurred when obtaining artifact.
Chris Sosac4e87842013-08-16 18:04:14 -0700147 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800148 names = gsutil_util.GetGSNamesWithWait(
Gilad Arnold950569b2013-08-27 14:38:01 -0700149 self.name, self.archive_url, str(self), timeout=timeout,
150 is_regex_pattern=self.is_regex_name)
Chris Sosa76e44b92013-01-31 12:11:38 -0800151 if not names:
152 raise ArtifactDownloadError('Could not find %s in Google Storage' %
153 self.name)
154
155 if self.single_name:
156 if len(names) > 1:
157 raise ArtifactDownloadError('Too many artifacts match %s' % self.name)
158
Chris Sosac4e87842013-08-16 18:04:14 -0700159 new_name = names[0]
Chris Sosa76e44b92013-01-31 12:11:38 -0800160 else:
Chris Sosac4e87842013-08-16 18:04:14 -0700161 new_name = names
162
163 if update_name:
164 self.name = new_name
Chris Sosa76e44b92013-01-31 12:11:38 -0800165
166 def _Download(self):
joychen0a8e34e2013-06-24 17:58:36 -0700167 """Downloads artifact from Google Storage to a local directory."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800168 gs_path = '/'.join([self.archive_url, self.name])
joychen0a8e34e2013-06-24 17:58:36 -0700169 self.install_path = os.path.join(self.install_dir, self.name)
170 gsutil_util.DownloadFromGS(gs_path, self.install_path)
Chris Sosa76e44b92013-01-31 12:11:38 -0800171
joychen0a8e34e2013-06-24 17:58:36 -0700172 def _Setup(self):
173 """For tarball like artifacts, extracts and prepares contents."""
174 pass
175
Dan Shi6e50c722013-08-19 15:05:06 -0700176 def _ClearException(self):
177 """Delete any existing exception saved for this artifact."""
178 if os.path.exists(self.exception_file_path):
179 os.remove(self.exception_file_path)
180
181 def _SaveException(self, e):
182 """Save the exception to a file for downloader.IsStaged to retrieve.
183
Gilad Arnold950569b2013-08-27 14:38:01 -0700184 Args:
185 e: Exception object to be saved.
Dan Shi6e50c722013-08-19 15:05:06 -0700186 """
187 with open(self.exception_file_path, 'w') as f:
188 pickle.dump(e, f)
189
190 def GetException(self):
191 """Retrieve any exception that was raised in Process method.
192
Gilad Arnold950569b2013-08-27 14:38:01 -0700193 Returns:
194 An Exception object that was raised when trying to process the artifact.
195 Return None if no exception was found.
Dan Shi6e50c722013-08-19 15:05:06 -0700196 """
197 if not os.path.exists(self.exception_file_path):
198 return None
199 with open(self.exception_file_path, 'r') as f:
200 return pickle.load(f)
Chris Sosa76e44b92013-01-31 12:11:38 -0800201
202 def Process(self, no_wait):
203 """Main call point to all artifacts. Downloads and Stages artifact.
204
205 Downloads and Stages artifact from Google Storage to the install directory
206 specified in the constructor. It multi-thread safe and does not overwrite
207 the artifact if it's already been downloaded or being downloaded. After
208 processing, leaves behind a marker to indicate to future invocations that
209 the artifact has already been staged based on the name of the artifact.
210
211 Do not override as it modifies important private variables, ensures thread
212 safety, and maintains cache semantics.
213
214 Note: this may be a blocking call when the artifact is already in the
215 process of being staged.
216
217 Args:
218 no_wait: If True, don't block waiting for artifact to exist if we fail to
219 immediately find it.
220
221 Raises:
222 ArtifactDownloadError: If the artifact fails to download from Google
223 Storage for any reason or that the regexp
224 defined by name is not specific enough.
225 """
226 if not self._process_lock:
227 self._process_lock = _build_artifact_locks.lock(
228 os.path.join(self.install_dir, self.name))
229
230 with self._process_lock:
231 common_util.MkDirP(self.install_dir)
Dan Shif8eb0d12013-08-01 17:52:06 -0700232 if not self.ArtifactStaged():
Dan Shi6e50c722013-08-19 15:05:06 -0700233 try:
234 # Delete any existing exception saved for this artifact.
235 self._ClearException()
236 # If the artifact should already have been uploaded, don't waste
237 # cycles waiting around for it to exist.
238 timeout = 1 if no_wait else 10
239 self.WaitForArtifactToExist(timeout)
240 self._Download()
241 self._Setup()
242 self._MarkArtifactStaged()
243 except Exception as e:
244 # Save the exception to a file for downloader.IsStaged to retrieve.
245 self._SaveException(e)
246 raise
Chris Sosa76e44b92013-01-31 12:11:38 -0800247 else:
248 self._Log('%s is already staged.', self)
249
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700250 def __str__(self):
251 """String representation for the download."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800252 return '->'.join(['%s/%s' % (self.archive_url, self.name),
Gilad Arnold950569b2013-08-27 14:38:01 -0700253 self.install_dir])
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700254
Chris Sosab26b1202013-08-16 16:40:55 -0700255 def __repr__(self):
256 return str(self)
257
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700258
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700259class AUTestPayloadBuildArtifact(BuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700260 """Wrapper for AUTest delta payloads which need additional setup."""
Gilad Arnold950569b2013-08-27 14:38:01 -0700261
joychen0a8e34e2013-06-24 17:58:36 -0700262 def _Setup(self):
263 super(AUTestPayloadBuildArtifact, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700264
Chris Sosa76e44b92013-01-31 12:11:38 -0800265 # Rename to update.gz.
266 install_path = os.path.join(self.install_dir, self.name)
joychen3cb228e2013-06-12 12:13:13 -0700267 new_install_path = os.path.join(self.install_dir,
joychen7c2054a2013-07-25 11:14:07 -0700268 devserver_constants.UPDATE_FILE)
Chris Sosa76e44b92013-01-31 12:11:38 -0800269 shutil.move(install_path, new_install_path)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700270
271
Chris Sosa76e44b92013-01-31 12:11:38 -0800272# TODO(sosa): Change callers to make this artifact more sane.
273class DeltaPayloadsArtifact(BuildArtifact):
274 """Delta payloads from the archive_url.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700275
Chris Sosa76e44b92013-01-31 12:11:38 -0800276 This artifact is super strange. It custom handles directories and
277 pulls in all delta payloads. We can't specify exactly what we want
278 because unlike other artifacts, this one does not conform to something a
279 client might know. The client doesn't know the version of n-1 or whether it
280 was even generated.
Gilad Arnold950569b2013-08-27 14:38:01 -0700281
282 IMPORTANT! Note that this artifact simply ignores the `name' argument because
283 that name is derived internally in accordance with sub-artifacts. Also note
284 the different types of names (in fact, file name patterns) used for the
285 different sub-artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800286 """
Gilad Arnold950569b2013-08-27 14:38:01 -0700287
Chris Sosa76e44b92013-01-31 12:11:38 -0800288 def __init__(self, *args):
289 super(DeltaPayloadsArtifact, self).__init__(*args)
Gilad Arnold950569b2013-08-27 14:38:01 -0700290 # Override the name field, we know what it should be.
291 self.name = '*_delta_*'
292 self.is_regex_name = False
293 self.single_name = False # Expect multiple deltas
294
295 # We use a regular glob for the N-to-N delta payload.
296 nton_name = 'chromeos_%s*_delta_*' % self.build
297 # We use a regular expression for the M-to-N delta payload.
298 mton_name = ('chromeos_(?!%s).*_delta_.*' % re.escape(self.build))
299
Chris Sosa76e44b92013-01-31 12:11:38 -0800300 nton_install_dir = os.path.join(self.install_dir, _AU_BASE,
301 self.build + _NTON_DIR_SUFFIX)
302 mton_install_dir = os.path.join(self.install_dir, _AU_BASE,
Gilad Arnold950569b2013-08-27 14:38:01 -0700303 self.build + _MTON_DIR_SUFFIX)
Chris Sosa76e44b92013-01-31 12:11:38 -0800304 self._sub_artifacts = [
305 AUTestPayloadBuildArtifact(mton_install_dir, self.archive_url,
Gilad Arnold950569b2013-08-27 14:38:01 -0700306 mton_name, self.build, is_regex_name=True),
Chris Sosa76e44b92013-01-31 12:11:38 -0800307 AUTestPayloadBuildArtifact(nton_install_dir, self.archive_url,
308 nton_name, self.build)]
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700309
Chris Sosa76e44b92013-01-31 12:11:38 -0800310 def _Download(self):
joychen0a8e34e2013-06-24 17:58:36 -0700311 """With sub-artifacts we do everything in _Setup()."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800312 pass
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700313
joychen0a8e34e2013-06-24 17:58:36 -0700314 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800315 """Process each sub-artifact. Only error out if none can be found."""
316 for artifact in self._sub_artifacts:
317 try:
318 artifact.Process(no_wait=True)
319 # Setup symlink so that AU will work for this payload.
320 os.symlink(
joychen25d25972013-07-30 14:54:16 -0700321 os.path.join(os.pardir, os.pardir,
joychen121fc9b2013-08-02 14:30:30 -0700322 devserver_constants.STATEFUL_FILE),
joychen25d25972013-07-30 14:54:16 -0700323 os.path.join(artifact.install_dir,
joychen121fc9b2013-08-02 14:30:30 -0700324 devserver_constants.STATEFUL_FILE))
Chris Sosa76e44b92013-01-31 12:11:38 -0800325 except ArtifactDownloadError as e:
326 self._Log('Could not process %s: %s', artifact, e)
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700327
Chris Sosa76e44b92013-01-31 12:11:38 -0800328
329class BundledBuildArtifact(BuildArtifact):
330 """A single build artifact bundle e.g. zip file or tar file."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800331
Gilad Arnold950569b2013-08-27 14:38:01 -0700332 def __init__(self, install_dir, archive_url, name, build,
333 is_regex_name=False, files_to_extract=None, exclude=None):
334 """Takes BuildArtifact args with some additional ones.
335
336 Args:
337 install_dir: See superclass.
338 archive_url: See superclass.
339 name: See superclass.
340 build: See superclass.
341 is_regex_name: See superclass.
342 files_to_extract: A list of files to extract. If set to None, extract
343 all files.
344 exclude: A list of files to exclude. If None, no files are excluded.
Chris Sosa76e44b92013-01-31 12:11:38 -0800345 """
Gilad Arnold950569b2013-08-27 14:38:01 -0700346 super(BundledBuildArtifact, self).__init__(
347 install_dir, archive_url, name, build, is_regex_name=is_regex_name)
Chris Sosa76e44b92013-01-31 12:11:38 -0800348 self._files_to_extract = files_to_extract
349 self._exclude = exclude
350
351 # We modify the marker so that it is unique to what was staged.
352 if files_to_extract:
353 self.marker_name = self._SanitizeName(
354 '_'.join(['.' + self.name] + files_to_extract))
355
356 def _Extract(self):
357 """Extracts the bundle into install_dir. Must be overridden.
358
359 If set, uses files_to_extract to only extract those items. If set, use
360 exclude to exclude specific files.
361 """
362 raise NotImplementedError()
363
joychen0a8e34e2013-06-24 17:58:36 -0700364 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800365 self._Extract()
366
367
368class TarballBuildArtifact(BundledBuildArtifact):
369 """Artifact for tar and tarball files."""
370
371 def _Extract(self):
372 """Extracts a tarball using tar.
373
374 Detects whether the tarball is compressed or not based on the file
375 extension and extracts the tarball into the install_path.
376 """
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700377 try:
joychen0a8e34e2013-06-24 17:58:36 -0700378 common_util.ExtractTarball(self.install_path, self.install_dir,
Simran Basi4baad082013-02-14 13:39:18 -0800379 files_to_extract=self._files_to_extract,
380 excluded_files=self._exclude)
381 except common_util.CommonUtilError as e:
382 raise ArtifactDownloadError(str(e))
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700383
384
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700385class AutotestTarballBuildArtifact(TarballBuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700386 """Wrapper around the autotest tarball to download from gsutil."""
387
joychen0a8e34e2013-06-24 17:58:36 -0700388 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800389 """Extracts the tarball into the install path excluding test suites."""
joychen0a8e34e2013-06-24 17:58:36 -0700390 super(AutotestTarballBuildArtifact, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700391
Chris Sosa76e44b92013-01-31 12:11:38 -0800392 # Deal with older autotest packages that may not be bundled.
joychen3cb228e2013-06-12 12:13:13 -0700393 autotest_dir = os.path.join(self.install_dir,
394 devserver_constants.AUTOTEST_DIR)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700395 autotest_pkgs_dir = os.path.join(autotest_dir, 'packages')
396 if not os.path.exists(autotest_pkgs_dir):
397 os.makedirs(autotest_pkgs_dir)
398
399 if not os.path.exists(os.path.join(autotest_pkgs_dir, 'packages.checksum')):
Chris Sosa76e44b92013-01-31 12:11:38 -0800400 cmd = ['autotest/utils/packager.py', 'upload', '--repository',
401 autotest_pkgs_dir, '--all']
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700402 try:
joychen0a8e34e2013-06-24 17:58:36 -0700403 subprocess.check_call(cmd, cwd=self.install_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700404 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800405 raise ArtifactDownloadError(
406 'Failed to create autotest packages!:\n%s' % e)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700407 else:
Gilad Arnoldf5843132012-09-25 00:31:20 -0700408 self._Log('Using pre-generated packages from autotest')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700409
Chris Masone816e38c2012-05-02 12:22:36 -0700410
Chris Sosa76e44b92013-01-31 12:11:38 -0800411class ZipfileBuildArtifact(BundledBuildArtifact):
412 """A downloadable artifact that is a zipfile."""
Chris Masone816e38c2012-05-02 12:22:36 -0700413
Chris Sosa76e44b92013-01-31 12:11:38 -0800414 def _Extract(self):
415 """Extracts files into the install path."""
416 # Unzip is weird. It expects its args before any excepts and expects its
417 # excepts in a list following the -x.
joychen0a8e34e2013-06-24 17:58:36 -0700418 cmd = ['unzip', '-o', self.install_path, '-d', self.install_dir]
Chris Sosa76e44b92013-01-31 12:11:38 -0800419 if self._files_to_extract:
420 cmd.extend(self._files_to_extract)
Chris Masone816e38c2012-05-02 12:22:36 -0700421
Chris Sosa76e44b92013-01-31 12:11:38 -0800422 if self._exclude:
423 cmd.append('-x')
424 cmd.extend(self._exclude)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700425
426 try:
Chris Sosa76e44b92013-01-31 12:11:38 -0800427 subprocess.check_call(cmd)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700428 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800429 raise ArtifactDownloadError(
430 'An error occurred when attempting to unzip %s:\n%s' %
joychen0a8e34e2013-06-24 17:58:36 -0700431 (self.install_path, e))
Gilad Arnold6f99b982012-09-12 10:49:40 -0700432
Gilad Arnold6f99b982012-09-12 10:49:40 -0700433
Chris Sosa76e44b92013-01-31 12:11:38 -0800434class ImplDescription(object):
435 """Data wrapper that describes an artifact's implementation."""
Gilad Arnold950569b2013-08-27 14:38:01 -0700436
437 def __init__(self, artifact_class, name, *additional_args,
438 **additional_dargs):
439 """Constructor.
Chris Sosa76e44b92013-01-31 12:11:38 -0800440
441 Args:
442 artifact_class: BuildArtifact class to use for the artifact.
443 name: name to use to identify artifact (see BuildArtifact.name)
Gilad Arnold950569b2013-08-27 14:38:01 -0700444 *additional_args: Additional arguments to pass to artifact_class.
445 **additional_dargs: Additional named arguments to pass to artifact_class.
Chris Sosa76e44b92013-01-31 12:11:38 -0800446 """
447 self.artifact_class = artifact_class
448 self.name = name
449 self.additional_args = additional_args
Gilad Arnold950569b2013-08-27 14:38:01 -0700450 self.additional_dargs = additional_dargs
Chris Sosa76e44b92013-01-31 12:11:38 -0800451
Chris Sosa968a1062013-08-02 17:42:50 -0700452 def __repr__(self):
453 return '%s_%s' % (self.artifact_class, self.name)
454
Chris Sosa76e44b92013-01-31 12:11:38 -0800455
456# Maps artifact names to their implementation description.
457# Please note, it is good practice to use constants for these names if you're
458# going to re-use the names ANYWHERE else in the devserver code.
459ARTIFACT_IMPLEMENTATION_MAP = {
Gilad Arnold950569b2013-08-27 14:38:01 -0700460 artifact_info.FULL_PAYLOAD:
461 ImplDescription(AUTestPayloadBuildArtifact, '*_full_*'),
462 artifact_info.DELTA_PAYLOADS:
463 ImplDescription(DeltaPayloadsArtifact, 'DONTCARE'),
464 artifact_info.STATEFUL_PAYLOAD:
465 ImplDescription(BuildArtifact, devserver_constants.STATEFUL_FILE),
Chris Sosa76e44b92013-01-31 12:11:38 -0800466
Gilad Arnold950569b2013-08-27 14:38:01 -0700467 artifact_info.BASE_IMAGE:
468 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE,
469 [devserver_constants.BASE_IMAGE_FILE]),
470 artifact_info.RECOVERY_IMAGE:
471 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE,
472 [devserver_constants.RECOVERY_IMAGE_FILE]),
473 artifact_info.TEST_IMAGE:
474 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE,
475 [devserver_constants.TEST_IMAGE_FILE]),
Chris Sosa76e44b92013-01-31 12:11:38 -0800476
Gilad Arnold950569b2013-08-27 14:38:01 -0700477 artifact_info.AUTOTEST:
478 ImplDescription(AutotestTarballBuildArtifact, AUTOTEST_FILE,
479 files_to_extract=None,
480 exclude=['autotest/test_suites']),
481 artifact_info.TEST_SUITES:
482 ImplDescription(TarballBuildArtifact, TEST_SUITES_FILE),
483 artifact_info.AU_SUITE:
484 ImplDescription(TarballBuildArtifact, AU_SUITE_FILE),
Chris Sosa76e44b92013-01-31 12:11:38 -0800485
Gilad Arnold950569b2013-08-27 14:38:01 -0700486 artifact_info.FIRMWARE:
487 ImplDescription(BuildArtifact, FIRMWARE_FILE),
488 artifact_info.SYMBOLS:
489 ImplDescription(TarballBuildArtifact, DEBUG_SYMBOLS_FILE,
490 files_to_extract=['debug/breakpad']),
beepsc3d0f872013-07-31 21:50:40 -0700491
Gilad Arnold950569b2013-08-27 14:38:01 -0700492 artifact_info.FACTORY_IMAGE:
493 ImplDescription(ZipfileBuildArtifact, FACTORY_FILE,
494 [devserver_constants.FACTORY_IMAGE_FILE])
Chris Sosa76e44b92013-01-31 12:11:38 -0800495}
496
Chris Sosa968a1062013-08-02 17:42:50 -0700497# Add all the paygen_au artifacts in one go.
498ARTIFACT_IMPLEMENTATION_MAP.update({
Gilad Arnold950569b2013-08-27 14:38:01 -0700499 artifact_info.PAYGEN_AU_SUITE_TEMPLATE % {'channel': c}:
500 ImplDescription(
501 TarballBuildArtifact, PAYGEN_AU_SUITE_FILE_TEMPLATE % {'channel': c})
502 for c in devserver_constants.CHANNELS
Chris Sosa968a1062013-08-02 17:42:50 -0700503})
504
Chris Sosa76e44b92013-01-31 12:11:38 -0800505
506class ArtifactFactory(object):
507 """A factory class that generates build artifacts from artifact names."""
508
Chris Sosa6b0c6172013-08-05 17:01:33 -0700509 def __init__(self, download_dir, archive_url, artifacts, files,
510 build):
Chris Sosa76e44b92013-01-31 12:11:38 -0800511 """Initalizes the member variables for the factory.
512
513 Args:
Gilad Arnold950569b2013-08-27 14:38:01 -0700514 download_dir: A directory to which artifacts are downloaded.
Chris Sosa76e44b92013-01-31 12:11:38 -0800515 archive_url: the Google Storage url of the bucket where the debug
516 symbols for the desired build are stored.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700517 artifacts: List of artifacts to stage. These artifacts must be
518 defined in artifact_info.py and have a mapping in the
519 ARTIFACT_IMPLEMENTATION_MAP.
520 files: List of files to stage. These files are just downloaded and staged
521 as files into the download_dir.
Chris Sosa76e44b92013-01-31 12:11:38 -0800522 build: The name of the build.
523 """
joychen0a8e34e2013-06-24 17:58:36 -0700524 self.download_dir = download_dir
Chris Sosa76e44b92013-01-31 12:11:38 -0800525 self.archive_url = archive_url
Chris Sosa6b0c6172013-08-05 17:01:33 -0700526 self.artifacts = artifacts
527 self.files = files
Chris Sosa76e44b92013-01-31 12:11:38 -0800528 self.build = build
529
530 @staticmethod
Chris Sosa6b0c6172013-08-05 17:01:33 -0700531 def _GetDescriptionComponents(name, is_artifact):
Gilad Arnold950569b2013-08-27 14:38:01 -0700532 """Returns components for constructing a BuildArtifact.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700533
Gilad Arnold950569b2013-08-27 14:38:01 -0700534 Args:
535 name: The artifact name / file pattern.
536 is_artifact: Whether this is a named (True) or file (False) artifact.
537 Returns:
538 A tuple consisting of the BuildArtifact subclass, name, and additional
539 list- and named-arguments.
540 Raises:
541 KeyError: if artifact doesn't exist in ARTIFACT_IMPLEMENTATION_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700542 """
543
544 if is_artifact:
545 description = ARTIFACT_IMPLEMENTATION_MAP[name]
546 else:
547 description = ImplDescription(BuildArtifact, name)
548
Chris Sosa76e44b92013-01-31 12:11:38 -0800549 return (description.artifact_class, description.name,
Gilad Arnold950569b2013-08-27 14:38:01 -0700550 description.additional_args, description.additional_dargs)
Chris Sosa76e44b92013-01-31 12:11:38 -0800551
Chris Sosa6b0c6172013-08-05 17:01:33 -0700552 def _Artifacts(self, names, is_artifact):
Gilad Arnold950569b2013-08-27 14:38:01 -0700553 """Returns the BuildArtifacts from |names|.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700554
555 If is_artifact is true, then these names define artifacts that must exist in
556 the ARTIFACT_IMPLEMENTATION_MAP. Otherwise, treat as filenames to stage as
557 basic BuildArtifacts.
558
Gilad Arnold950569b2013-08-27 14:38:01 -0700559 Args:
560 names: A sequence of artifact names.
561 is_artifact: Whether this is a named (True) or file (False) artifact.
562 Returns:
563 An iterable of BuildArtifacts.
564 Raises:
565 KeyError: if artifact doesn't exist in ARTIFACT_IMPLEMENTATION_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700566 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800567 artifacts = []
Chris Sosa6b0c6172013-08-05 17:01:33 -0700568 for name in names:
Gilad Arnold950569b2013-08-27 14:38:01 -0700569 artifact_class, path, args, dargs = self._GetDescriptionComponents(
Chris Sosa6b0c6172013-08-05 17:01:33 -0700570 name, is_artifact)
joychen0a8e34e2013-06-24 17:58:36 -0700571 artifacts.append(artifact_class(self.download_dir, self.archive_url, path,
Gilad Arnold950569b2013-08-27 14:38:01 -0700572 self.build, *args, **dargs))
Chris Sosa76e44b92013-01-31 12:11:38 -0800573
574 return artifacts
575
576 def RequiredArtifacts(self):
Gilad Arnold950569b2013-08-27 14:38:01 -0700577 """Returns BuildArtifacts for the factory's artifacts.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700578
Gilad Arnold950569b2013-08-27 14:38:01 -0700579 Returns:
580 An iterable of BuildArtifacts.
581 Raises:
582 KeyError: if artifact doesn't exist in ARTIFACT_IMPLEMENTATION_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700583 """
584 artifacts = []
585 if self.artifacts:
586 artifacts.extend(self._Artifacts(self.artifacts, True))
587 if self.files:
588 artifacts.extend(self._Artifacts(self.files, False))
589
590 return artifacts
Chris Sosa76e44b92013-01-31 12:11:38 -0800591
592 def OptionalArtifacts(self):
Gilad Arnold950569b2013-08-27 14:38:01 -0700593 """Returns BuildArtifacts that should be cached.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700594
Gilad Arnold950569b2013-08-27 14:38:01 -0700595 Returns:
596 An iterable of BuildArtifacts.
597 Raises:
598 KeyError: if an optional artifact doesn't exist in
599 ARTIFACT_IMPLEMENTATION_MAP yet defined in
600 artifact_info.REQUESTED_TO_OPTIONAL_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700601 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800602 optional_names = set()
603 for artifact_name, optional_list in (
604 artifact_info.REQUESTED_TO_OPTIONAL_MAP.iteritems()):
605 # We are already downloading it.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700606 if artifact_name in self.artifacts:
Chris Sosa76e44b92013-01-31 12:11:38 -0800607 optional_names = optional_names.union(optional_list)
608
Chris Sosa6b0c6172013-08-05 17:01:33 -0700609 return self._Artifacts(optional_names - set(self.artifacts), True)
Chris Sosa968a1062013-08-02 17:42:50 -0700610
611
612# A simple main to verify correctness of the artifact map when making simple
613# name changes.
614if __name__ == '__main__':
615 print 'ARTIFACT IMPLEMENTATION MAP (for debugging)'
616 print 'FORMAT: ARTIFACT -> IMPLEMENTATION (<class>_file)'
617 for key, value in sorted(ARTIFACT_IMPLEMENTATION_MAP.items()):
618 print '%s -> %s' % (key, value)