blob: 44969ba44320ec857a40e44137af64db96dc2018 [file] [log] [blame]
Gabe Black3b567202015-09-23 14:07:59 -07001#!/usr/bin/python2
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
Gabe Black3b567202015-09-23 14:07:59 -07009from __future__ import print_function
10
11import itertools
Chris Sosa47a7d4e2012-03-28 11:26:55 -070012import os
Dan Shi6e50c722013-08-19 15:05:06 -070013import pickle
Gilad Arnold950569b2013-08-27 14:38:01 -070014import re
Chris Sosa47a7d4e2012-03-28 11:26:55 -070015import shutil
16import subprocess
17
Chris Sosa76e44b92013-01-31 12:11:38 -080018import artifact_info
19import common_util
joychen3cb228e2013-06-12 12:13:13 -070020import devserver_constants
Gilad Arnoldc65330c2012-09-20 15:17:48 -070021import log_util
Chris Sosa47a7d4e2012-03-28 11:26:55 -070022
23
Chris Sosa76e44b92013-01-31 12:11:38 -080024_AU_BASE = 'au'
25_NTON_DIR_SUFFIX = '_nton'
26_MTON_DIR_SUFFIX = '_mton'
27
28############ Actual filenames of artifacts in Google Storage ############
29
30AU_SUITE_FILE = 'au_control.tar.bz2'
Chris Sosa968a1062013-08-02 17:42:50 -070031PAYGEN_AU_SUITE_FILE_TEMPLATE = 'paygen_au_%(channel)s_control.tar.bz2'
Chris Sosa76e44b92013-01-31 12:11:38 -080032AUTOTEST_FILE = 'autotest.tar'
Simran Basiea0590d2014-10-29 11:31:26 -070033CONTROL_FILES_FILE = 'control_files.tar'
34AUTOTEST_PACKAGES_FILE = 'autotest_packages.tar'
Chris Sosa76e44b92013-01-31 12:11:38 -080035AUTOTEST_COMPRESSED_FILE = 'autotest.tar.bz2'
Simran Basi6459bba2015-02-04 14:47:23 -080036AUTOTEST_SERVER_PACKAGE_FILE = 'autotest_server_package.tar.bz2'
Chris Sosa76e44b92013-01-31 12:11:38 -080037DEBUG_SYMBOLS_FILE = 'debug.tgz'
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -080038FACTORY_FILE = 'ChromeOS-factory*.zip'
Chris Sosa76e44b92013-01-31 12:11:38 -080039FIRMWARE_FILE = 'firmware_from_source.tar.bz2'
40IMAGE_FILE = 'image.zip'
Chris Sosa76e44b92013-01-31 12:11:38 -080041TEST_SUITES_FILE = 'test_suites.tar.bz2'
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -080042BASE_IMAGE_FILE = 'chromiumos_base_image.tar.xz'
43TEST_IMAGE_FILE = 'chromiumos_test_image.tar.xz'
44RECOVERY_IMAGE_FILE = 'recovery_image.tar.xz'
45
Gabe Black3b567202015-09-23 14:07:59 -070046############ Actual filenames of Android build artifacts ############
47
Dan Shiba4e00f2015-10-27 12:03:53 -070048ANDROID_IMAGE_ZIP = r'.*-img-[^-]*\.zip'
Gabe Black3b567202015-09-23 14:07:59 -070049ANDROID_RADIO_IMAGE = 'radio.img'
50ANDROID_BOOTLOADER_IMAGE = 'bootloader.img'
51ANDROID_FASTBOOT = 'fastboot'
52ANDROID_TEST_ZIP = r'[^-]*-tests-.*\.zip'
Chris Sosa76e44b92013-01-31 12:11:38 -080053
54_build_artifact_locks = common_util.LockDict()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070055
56
57class ArtifactDownloadError(Exception):
58 """Error used to signify an issue processing an artifact."""
59 pass
60
61
Gabe Black3b567202015-09-23 14:07:59 -070062class ArtifactMeta(type):
63 """metaclass for an artifact type.
64
65 This metaclass is for class Artifact and its subclasses to have a meaningful
66 string composed of class name and the corresponding artifact name, e.g.,
67 `Artifact_full_payload`. This helps to better logging, refer to logging in
68 method Downloader.Download.
69 """
70
71 ARTIFACT_NAME = None
72
73 def __str__(cls):
74 return '%s_%s' % (cls.__name__, cls.ARTIFACT_NAME)
75
76 def __repr__(cls):
77 return str(cls)
78
79
80class Artifact(log_util.Loggable):
81 """Wrapper around an artifact to download using a fetcher.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070082
83 The purpose of this class is to download objects from Google Storage
84 and install them to a local directory. There are two main functions, one to
85 download/prepare the artifacts in to a temporary staging area and the second
86 to stage it into its final destination.
Chris Sosa76e44b92013-01-31 12:11:38 -080087
Gilad Arnold950569b2013-08-27 14:38:01 -070088 IMPORTANT! (i) `name' is a glob expression by default (and not a regex), be
89 attentive when adding new artifacts; (ii) name matching semantics differ
90 between a glob (full name string match) and a regex (partial match).
91
Chris Sosa76e44b92013-01-31 12:11:38 -080092 Class members:
Gabe Black3b567202015-09-23 14:07:59 -070093 fetcher: An object which knows how to fetch the artifact.
Gilad Arnold950569b2013-08-27 14:38:01 -070094 name: Name given for artifact; in fact, it is a pattern that captures the
95 names of files contained in the artifact. This can either be an
96 ordinary shell-style glob (the default), or a regular expression (if
97 is_regex_name is True).
98 is_regex_name: Whether the name value is a regex (default: glob).
Chris Sosa76e44b92013-01-31 12:11:38 -080099 build: The version of the build i.e. R26-2342.0.0.
100 marker_name: Name used to define the lock marker for the artifacts to
101 prevent it from being re-downloaded. By default based on name
102 but can be overriden by children.
Dan Shi6e50c722013-08-19 15:05:06 -0700103 exception_file_path: Path to a file containing the serialized exception,
104 which was raised in Process method. The file is located
105 in the parent folder of install_dir, since the
106 install_dir will be deleted if the build does not
107 existed.
joychen0a8e34e2013-06-24 17:58:36 -0700108 install_path: Path to artifact.
Gabe Black3b567202015-09-23 14:07:59 -0700109 install_subdir: Directory within install_path where the artifact is actually
110 stored.
Chris Sosa76e44b92013-01-31 12:11:38 -0800111 install_dir: The final location where the artifact should be staged to.
112 single_name: If True the name given should only match one item. Note, if not
113 True, self.name will become a list of items returned.
Gilad Arnold1638d822013-11-07 23:38:16 -0800114 installed_files: A list of files that were the final result of downloading
115 and setting up the artifact.
116 store_installed_files: Whether the list of installed files is stored in the
117 marker file.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700118 """
Gilad Arnold950569b2013-08-27 14:38:01 -0700119
Gabe Black3b567202015-09-23 14:07:59 -0700120 __metaclass__ = ArtifactMeta
121
122 def __init__(self, name, install_dir, build, install_subdir='',
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800123 is_regex_name=False, optional_name=None):
Gilad Arnold950569b2013-08-27 14:38:01 -0700124 """Constructor.
125
126 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800127 install_dir: Where to install the artifact.
Chris Sosa76e44b92013-01-31 12:11:38 -0800128 name: Identifying name to be used to find/store the artifact.
129 build: The name of the build e.g. board/release.
Gabe Black3b567202015-09-23 14:07:59 -0700130 install_subdir: Directory within install_path where the artifact is
131 actually stored.
Gilad Arnold950569b2013-08-27 14:38:01 -0700132 is_regex_name: Whether the name pattern is a regex (default: glob).
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800133 optional_name: An alternative name to find the artifact, which can lead
134 to faster download. Unlike |name|, there is no guarantee that an
135 artifact named |optional_name| is/will be on Google Storage. If it
136 exists, we download it. Otherwise, we fall back to wait for |name|.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700137 """
Gabe Black3b567202015-09-23 14:07:59 -0700138 super(Artifact, self).__init__()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700139
Chris Sosa76e44b92013-01-31 12:11:38 -0800140 # In-memory lock to keep the devserver from colliding with itself while
141 # attempting to stage the same artifact.
142 self._process_lock = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700143
Chris Sosa76e44b92013-01-31 12:11:38 -0800144 self.name = name
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800145 self.optional_name = optional_name
Gilad Arnold950569b2013-08-27 14:38:01 -0700146 self.is_regex_name = is_regex_name
Chris Sosa76e44b92013-01-31 12:11:38 -0800147 self.build = build
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700148
Chris Sosa76e44b92013-01-31 12:11:38 -0800149 self.marker_name = '.' + self._SanitizeName(name)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700150
Dan Shi6e50c722013-08-19 15:05:06 -0700151 exception_file_name = ('.' + self._SanitizeName(build) + self.marker_name +
152 '.exception')
153 # The exception file needs to be located in parent folder, since the
154 # install_dir will be deleted is the build does not exist.
155 self.exception_file_path = os.path.join(os.path.dirname(install_dir),
Gilad Arnold950569b2013-08-27 14:38:01 -0700156 exception_file_name)
Dan Shi6e50c722013-08-19 15:05:06 -0700157
joychen0a8e34e2013-06-24 17:58:36 -0700158 self.install_path = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700159
Chris Sosa76e44b92013-01-31 12:11:38 -0800160 self.install_dir = install_dir
Gabe Black3b567202015-09-23 14:07:59 -0700161 self.install_subdir = install_subdir
Chris Sosa76e44b92013-01-31 12:11:38 -0800162
163 self.single_name = True
164
Gilad Arnold1638d822013-11-07 23:38:16 -0800165 self.installed_files = []
166 self.store_installed_files = True
167
Chris Sosa76e44b92013-01-31 12:11:38 -0800168 @staticmethod
169 def _SanitizeName(name):
170 """Sanitizes name to be used for creating a file on the filesystem.
171
172 '.','/' and '*' have special meaning in FS lingo. Replace them with words.
Gilad Arnold950569b2013-08-27 14:38:01 -0700173
174 Args:
175 name: A file name/path.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800176
Gilad Arnold950569b2013-08-27 14:38:01 -0700177 Returns:
178 The sanitized name/path.
Chris Sosa76e44b92013-01-31 12:11:38 -0800179 """
180 return name.replace('*', 'STAR').replace('.', 'DOT').replace('/', 'SLASH')
181
Dan Shif8eb0d12013-08-01 17:52:06 -0700182 def ArtifactStaged(self):
Gilad Arnold1638d822013-11-07 23:38:16 -0800183 """Returns True if artifact is already staged.
184
185 This checks for (1) presence of the artifact marker file, and (2) the
186 presence of each installed file listed in this marker. Both must hold for
187 the artifact to be considered staged. Note that this method is safe for use
188 even if the artifacts were not stageed by this instance, as it is assumed
Gabe Black3b567202015-09-23 14:07:59 -0700189 that any Artifact instance that did the staging wrote the list of
Gilad Arnold1638d822013-11-07 23:38:16 -0800190 files actually installed into the marker.
191 """
192 marker_file = os.path.join(self.install_dir, self.marker_name)
193
194 # If the marker is missing, it's definitely not staged.
195 if not os.path.exists(marker_file):
196 return False
197
198 # We want to ensure that every file listed in the marker is actually there.
199 if self.store_installed_files:
200 with open(marker_file) as f:
201 files = [line.strip() for line in f]
202
203 # Check to see if any of the purportedly installed files are missing, in
204 # which case the marker is outdated and should be removed.
205 missing_files = [fname for fname in files if not os.path.exists(fname)]
206 if missing_files:
207 self._Log('***ATTENTION*** %s files listed in %s are missing:\n%s',
208 'All' if len(files) == len(missing_files) else 'Some',
209 marker_file, '\n'.join(missing_files))
210 os.remove(marker_file)
211 return False
212
213 return True
Chris Sosa76e44b92013-01-31 12:11:38 -0800214
215 def _MarkArtifactStaged(self):
216 """Marks the artifact as staged."""
217 with open(os.path.join(self.install_dir, self.marker_name), 'w') as f:
Gilad Arnold1638d822013-11-07 23:38:16 -0800218 f.write('\n'.join(self.installed_files))
Chris Sosa76e44b92013-01-31 12:11:38 -0800219
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800220 def _UpdateName(self, names):
221 if self.single_name and len(names) > 1:
222 raise ArtifactDownloadError('Too many artifacts match %s' % self.name)
Chris Sosa76e44b92013-01-31 12:11:38 -0800223
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800224 self.name = names[0]
Chris Sosa76e44b92013-01-31 12:11:38 -0800225
joychen0a8e34e2013-06-24 17:58:36 -0700226 def _Setup(self):
Gilad Arnold1638d822013-11-07 23:38:16 -0800227 """Process the downloaded content, update the list of installed files."""
228 # In this primitive case, what was downloaded (has to be a single file) is
229 # what's installed.
230 self.installed_files = [self.install_path]
joychen0a8e34e2013-06-24 17:58:36 -0700231
Dan Shi6e50c722013-08-19 15:05:06 -0700232 def _ClearException(self):
233 """Delete any existing exception saved for this artifact."""
234 if os.path.exists(self.exception_file_path):
235 os.remove(self.exception_file_path)
236
237 def _SaveException(self, e):
238 """Save the exception to a file for downloader.IsStaged to retrieve.
239
Gilad Arnold950569b2013-08-27 14:38:01 -0700240 Args:
241 e: Exception object to be saved.
Dan Shi6e50c722013-08-19 15:05:06 -0700242 """
243 with open(self.exception_file_path, 'w') as f:
244 pickle.dump(e, f)
245
246 def GetException(self):
247 """Retrieve any exception that was raised in Process method.
248
Gilad Arnold950569b2013-08-27 14:38:01 -0700249 Returns:
250 An Exception object that was raised when trying to process the artifact.
251 Return None if no exception was found.
Dan Shi6e50c722013-08-19 15:05:06 -0700252 """
253 if not os.path.exists(self.exception_file_path):
254 return None
255 with open(self.exception_file_path, 'r') as f:
256 return pickle.load(f)
Chris Sosa76e44b92013-01-31 12:11:38 -0800257
Gabe Black3b567202015-09-23 14:07:59 -0700258 def Process(self, downloader, no_wait):
Chris Sosa76e44b92013-01-31 12:11:38 -0800259 """Main call point to all artifacts. Downloads and Stages artifact.
260
261 Downloads and Stages artifact from Google Storage to the install directory
262 specified in the constructor. It multi-thread safe and does not overwrite
263 the artifact if it's already been downloaded or being downloaded. After
264 processing, leaves behind a marker to indicate to future invocations that
265 the artifact has already been staged based on the name of the artifact.
266
267 Do not override as it modifies important private variables, ensures thread
268 safety, and maintains cache semantics.
269
270 Note: this may be a blocking call when the artifact is already in the
271 process of being staged.
272
273 Args:
Gabe Black3b567202015-09-23 14:07:59 -0700274 downloader: A downloader instance containing the logic to download
275 artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800276 no_wait: If True, don't block waiting for artifact to exist if we fail to
277 immediately find it.
278
279 Raises:
280 ArtifactDownloadError: If the artifact fails to download from Google
281 Storage for any reason or that the regexp
282 defined by name is not specific enough.
283 """
284 if not self._process_lock:
285 self._process_lock = _build_artifact_locks.lock(
286 os.path.join(self.install_dir, self.name))
287
Gabe Black3b567202015-09-23 14:07:59 -0700288 real_install_dir = os.path.join(self.install_dir, self.install_subdir)
Chris Sosa76e44b92013-01-31 12:11:38 -0800289 with self._process_lock:
Gabe Black3b567202015-09-23 14:07:59 -0700290 common_util.MkDirP(real_install_dir)
Dan Shif8eb0d12013-08-01 17:52:06 -0700291 if not self.ArtifactStaged():
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800292 # Delete any existing exception saved for this artifact.
293 self._ClearException()
294 found_artifact = False
295 if self.optional_name:
296 try:
Gabe Black3b567202015-09-23 14:07:59 -0700297 # Check if the artifact named |optional_name| exists.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800298 # Because this artifact may not always exist, don't bother
299 # to wait for it (set timeout=1).
Gabe Black3b567202015-09-23 14:07:59 -0700300 new_names = downloader.Wait(
301 self.optional_name, self.is_regex_name, timeout=1)
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800302 self._UpdateName(new_names)
303
304 except ArtifactDownloadError:
305 self._Log('Unable to download %s; fall back to download %s',
306 self.optional_name, self.name)
307 else:
308 found_artifact = True
309
Dan Shi6e50c722013-08-19 15:05:06 -0700310 try:
Dan Shi6e50c722013-08-19 15:05:06 -0700311 # If the artifact should already have been uploaded, don't waste
312 # cycles waiting around for it to exist.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800313 if not found_artifact:
314 timeout = 1 if no_wait else 10
Gabe Black3b567202015-09-23 14:07:59 -0700315 new_names = downloader.Wait(
316 self.name, self.is_regex_name, timeout)
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800317 self._UpdateName(new_names)
318
319 self._Log('Downloading file %s', self.name)
Gabe Black3b567202015-09-23 14:07:59 -0700320 self.install_path = downloader.Fetch(self.name, real_install_dir)
Dan Shi6e50c722013-08-19 15:05:06 -0700321 self._Setup()
322 self._MarkArtifactStaged()
323 except Exception as e:
324 # Save the exception to a file for downloader.IsStaged to retrieve.
325 self._SaveException(e)
Gilad Arnold02dc6552013-11-14 11:27:54 -0800326
327 # Convert an unknown exception into an ArtifactDownloadError.
328 if type(e) is ArtifactDownloadError:
329 raise
330 else:
331 raise ArtifactDownloadError('An error occurred: %s' % e)
Chris Sosa76e44b92013-01-31 12:11:38 -0800332 else:
333 self._Log('%s is already staged.', self)
334
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700335 def __str__(self):
336 """String representation for the download."""
Gabe Black3b567202015-09-23 14:07:59 -0700337 return '%s->%s' % (self.name, self.install_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700338
Chris Sosab26b1202013-08-16 16:40:55 -0700339 def __repr__(self):
340 return str(self)
341
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700342
Gabe Black3b567202015-09-23 14:07:59 -0700343class AUTestPayload(Artifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700344 """Wrapper for AUTest delta payloads which need additional setup."""
Gilad Arnold950569b2013-08-27 14:38:01 -0700345
joychen0a8e34e2013-06-24 17:58:36 -0700346 def _Setup(self):
Gabe Black3b567202015-09-23 14:07:59 -0700347 super(AUTestPayload, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700348
Chris Sosa76e44b92013-01-31 12:11:38 -0800349 # Rename to update.gz.
Gabe Black3b567202015-09-23 14:07:59 -0700350 install_path = os.path.join(self.install_dir, self.install_subdir,
351 self.name)
352 new_install_path = os.path.join(self.install_dir, self.install_subdir,
joychen7c2054a2013-07-25 11:14:07 -0700353 devserver_constants.UPDATE_FILE)
Chris Sosa76e44b92013-01-31 12:11:38 -0800354 shutil.move(install_path, new_install_path)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700355
Gilad Arnold1638d822013-11-07 23:38:16 -0800356 # Reflect the rename in the list of installed files.
357 self.installed_files.remove(install_path)
358 self.installed_files = [new_install_path]
359
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700360
Gabe Black3b567202015-09-23 14:07:59 -0700361class DeltaPayloadBase(AUTestPayload):
Chris Sosa76e44b92013-01-31 12:11:38 -0800362 """Delta payloads from the archive_url.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700363
Gabe Black3b567202015-09-23 14:07:59 -0700364 These artifacts are super strange. They custom handle directories and
365 pull in all delta payloads. We can't specify exactly what we want
Chris Sosa76e44b92013-01-31 12:11:38 -0800366 because unlike other artifacts, this one does not conform to something a
367 client might know. The client doesn't know the version of n-1 or whether it
368 was even generated.
Gilad Arnold950569b2013-08-27 14:38:01 -0700369
370 IMPORTANT! Note that this artifact simply ignores the `name' argument because
Gabe Black3b567202015-09-23 14:07:59 -0700371 that name is derived internally.
Chris Sosa76e44b92013-01-31 12:11:38 -0800372 """
Gilad Arnold950569b2013-08-27 14:38:01 -0700373
joychen0a8e34e2013-06-24 17:58:36 -0700374 def _Setup(self):
Gabe Black3b567202015-09-23 14:07:59 -0700375 super(DeltaPayloadBase, self)._Setup()
376 # Setup symlink so that AU will work for this payload.
377 stateful_update_symlink = os.path.join(
378 self.install_dir, self.install_subdir,
379 devserver_constants.STATEFUL_FILE)
380 os.symlink(os.path.join(os.pardir, os.pardir,
381 devserver_constants.STATEFUL_FILE),
382 stateful_update_symlink)
383 self.installed_files.append(stateful_update_symlink)
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700384
Chris Sosa76e44b92013-01-31 12:11:38 -0800385
Gabe Black3b567202015-09-23 14:07:59 -0700386class BundledArtifact(Artifact):
Chris Sosa76e44b92013-01-31 12:11:38 -0800387 """A single build artifact bundle e.g. zip file or tar file."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800388
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800389 def __init__(self, *args, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700390 """Takes Artifact args with some additional ones.
Gilad Arnold950569b2013-08-27 14:38:01 -0700391
392 Args:
Gabe Black3b567202015-09-23 14:07:59 -0700393 *args: See Artifact documentation.
394 **kwargs: See Artifact documentation.
Gilad Arnold950569b2013-08-27 14:38:01 -0700395 files_to_extract: A list of files to extract. If set to None, extract
396 all files.
397 exclude: A list of files to exclude. If None, no files are excluded.
Chris Sosa76e44b92013-01-31 12:11:38 -0800398 """
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800399 self._files_to_extract = kwargs.pop('files_to_extract', None)
400 self._exclude = kwargs.pop('exclude', None)
Gabe Black3b567202015-09-23 14:07:59 -0700401 super(BundledArtifact, self).__init__(*args, **kwargs)
Chris Sosa76e44b92013-01-31 12:11:38 -0800402
403 # We modify the marker so that it is unique to what was staged.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800404 if self._files_to_extract:
Chris Sosa76e44b92013-01-31 12:11:38 -0800405 self.marker_name = self._SanitizeName(
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800406 '_'.join(['.' + self.name] + self._files_to_extract))
Chris Sosa76e44b92013-01-31 12:11:38 -0800407
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800408 def _RunUnzip(self, list_only):
409 # Unzip is weird. It expects its args before any excludes and expects its
410 # excludes in a list following the -x.
411 cmd = ['unzip', '-qql' if list_only else '-o', self.install_path]
412 if not list_only:
413 cmd += ['-d', self.install_dir]
Chris Sosa76e44b92013-01-31 12:11:38 -0800414
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800415 if self._files_to_extract:
416 cmd.extend(self._files_to_extract)
417
418 if self._exclude:
419 cmd.append('-x')
420 cmd.extend(self._exclude)
421
422 try:
423 return subprocess.check_output(cmd).strip('\n').splitlines()
424 except subprocess.CalledProcessError, e:
425 raise ArtifactDownloadError(
426 'An error occurred when attempting to unzip %s:\n%s' %
427 (self.install_path, e))
Chris Sosa76e44b92013-01-31 12:11:38 -0800428
joychen0a8e34e2013-06-24 17:58:36 -0700429 def _Setup(self):
Gilad Arnold1638d822013-11-07 23:38:16 -0800430 extract_result = self._Extract()
431 if self.store_installed_files:
432 # List both the archive and the extracted files.
433 self.installed_files.append(self.install_path)
434 self.installed_files.extend(extract_result)
Chris Sosa76e44b92013-01-31 12:11:38 -0800435
Chris Sosa76e44b92013-01-31 12:11:38 -0800436 def _Extract(self):
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800437 """Extracts files into the install path."""
438 if self.name.endswith('.zip'):
439 return self._ExtractZipfile()
440 else:
441 return self._ExtractTarball()
442
443 def _ExtractZipfile(self):
444 """Extracts a zip file using unzip."""
445 file_list = [os.path.join(self.install_dir, line[30:].strip())
446 for line in self._RunUnzip(True)
447 if not line.endswith('/')]
448 if file_list:
449 self._RunUnzip(False)
450
451 return file_list
452
453 def _ExtractTarball(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800454 """Extracts a tarball using tar.
455
456 Detects whether the tarball is compressed or not based on the file
457 extension and extracts the tarball into the install_path.
458 """
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700459 try:
Gilad Arnold1638d822013-11-07 23:38:16 -0800460 return common_util.ExtractTarball(self.install_path, self.install_dir,
461 files_to_extract=self._files_to_extract,
462 excluded_files=self._exclude,
463 return_extracted_files=True)
Simran Basi4baad082013-02-14 13:39:18 -0800464 except common_util.CommonUtilError as e:
465 raise ArtifactDownloadError(str(e))
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700466
467
Gabe Black3b567202015-09-23 14:07:59 -0700468class AutotestTarball(BundledArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700469 """Wrapper around the autotest tarball to download from gsutil."""
470
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800471 def __init__(self, *args, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700472 super(AutotestTarball, self).__init__(*args, **kwargs)
Gilad Arnold1638d822013-11-07 23:38:16 -0800473 # We don't store/check explicit file lists in Autotest tarball markers;
474 # this can get huge and unwieldy, and generally make little sense.
475 self.store_installed_files = False
476
joychen0a8e34e2013-06-24 17:58:36 -0700477 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800478 """Extracts the tarball into the install path excluding test suites."""
Gabe Black3b567202015-09-23 14:07:59 -0700479 super(AutotestTarball, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700480
Chris Sosa76e44b92013-01-31 12:11:38 -0800481 # Deal with older autotest packages that may not be bundled.
joychen3cb228e2013-06-12 12:13:13 -0700482 autotest_dir = os.path.join(self.install_dir,
483 devserver_constants.AUTOTEST_DIR)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700484 autotest_pkgs_dir = os.path.join(autotest_dir, 'packages')
485 if not os.path.exists(autotest_pkgs_dir):
486 os.makedirs(autotest_pkgs_dir)
487
488 if not os.path.exists(os.path.join(autotest_pkgs_dir, 'packages.checksum')):
Chris Sosa76e44b92013-01-31 12:11:38 -0800489 cmd = ['autotest/utils/packager.py', 'upload', '--repository',
490 autotest_pkgs_dir, '--all']
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700491 try:
joychen0a8e34e2013-06-24 17:58:36 -0700492 subprocess.check_call(cmd, cwd=self.install_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700493 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800494 raise ArtifactDownloadError(
495 'Failed to create autotest packages!:\n%s' % e)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700496 else:
Gilad Arnoldf5843132012-09-25 00:31:20 -0700497 self._Log('Using pre-generated packages from autotest')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700498
Chris Masone816e38c2012-05-02 12:22:36 -0700499
Gabe Black3b567202015-09-23 14:07:59 -0700500def _CreateNewArtifact(tag, base, name, *fixed_args, **fixed_kwargs):
501 """Get a data wrapper that describes an artifact's implementation.
Gilad Arnold950569b2013-08-27 14:38:01 -0700502
Gabe Black3b567202015-09-23 14:07:59 -0700503 Args:
504 tag: Tag of the artifact, defined in artifact_info.
505 base: Class of the artifact, e.g., BundledArtifact.
506 name: Name of the artifact, e.g., image.zip.
507 *fixed_args: Fixed arguments that are additional to the one used in base
508 class.
509 **fixed_kwargs: Fixed keyword arguments that are additional to the one used
510 in base class.
Chris Sosa76e44b92013-01-31 12:11:38 -0800511
Gabe Black3b567202015-09-23 14:07:59 -0700512 Returns:
513 A data wrapper that describes an artifact's implementation.
Chris Sosa76e44b92013-01-31 12:11:38 -0800514
Gabe Black3b567202015-09-23 14:07:59 -0700515 """
516 class NewArtifact(base):
517 """A data wrapper that describes an artifact's implementation."""
518 ARTIFACT_TAG = tag
519 ARTIFACT_NAME = name
520
521 def __init__(self, *args, **kwargs):
522 all_args = fixed_args + args
523 all_kwargs = {}
524 all_kwargs.update(fixed_kwargs)
525 all_kwargs.update(kwargs)
526 super(NewArtifact, self).__init__(self.ARTIFACT_NAME,
527 *all_args, **all_kwargs)
528
529 NewArtifact.__name__ = base.__name__
530 return NewArtifact
Chris Sosa968a1062013-08-02 17:42:50 -0700531
Chris Sosa76e44b92013-01-31 12:11:38 -0800532
Gabe Black3b567202015-09-23 14:07:59 -0700533# TODO(dshi): Refactor the code here to split out the logic of creating the
534# artifacts mapping to a different module.
535chromeos_artifact_map = {}
Chris Sosa76e44b92013-01-31 12:11:38 -0800536
Chris Sosa76e44b92013-01-31 12:11:38 -0800537
Gabe Black3b567202015-09-23 14:07:59 -0700538def _AddCrOSArtifact(tag, base, name, *fixed_args, **fixed_kwargs):
539 """Add a data wrapper that describes a ChromeOS artifact's implementation to
540 chromeos_artifact_map.
541 """
542 artifact = _CreateNewArtifact(tag, base, name, *fixed_args, **fixed_kwargs)
543 chromeos_artifact_map.setdefault(tag, []).append(artifact)
Chris Sosa76e44b92013-01-31 12:11:38 -0800544
beepsc3d0f872013-07-31 21:50:40 -0700545
Gabe Black3b567202015-09-23 14:07:59 -0700546_AddCrOSArtifact(artifact_info.FULL_PAYLOAD, AUTestPayload, '*_full_*')
547
548
549class DeltaPayloadNtoN(DeltaPayloadBase):
550 """ChromeOS Delta payload artifact for updating from version N to N."""
551 ARTIFACT_TAG = artifact_info.DELTA_PAYLOADS
552 ARTIFACT_NAME = 'NOT_APPLICABLE'
553
554 def __init__(self, install_dir, build, *args, **kwargs):
555 name = 'chromeos_%s*_delta_*' % build
556 install_subdir = os.path.join(_AU_BASE, build + _NTON_DIR_SUFFIX)
557 super(DeltaPayloadNtoN, self).__init__(name, install_dir, build, *args,
558 install_subdir=install_subdir,
559 **kwargs)
560
561
562class DeltaPayloadMtoN(DeltaPayloadBase):
563 """ChromeOS Delta payload artifact for updating from version M to N."""
564 ARTIFACT_TAG = artifact_info.DELTA_PAYLOADS
565 ARTIFACT_NAME = 'NOT_APPLICABLE'
566
567 def __init__(self, install_dir, build, *args, **kwargs):
568 name = ('chromeos_(?!%s).*_delta_.*' % re.escape(build))
569 install_subdir = os.path.join(_AU_BASE, build + _MTON_DIR_SUFFIX)
570 super(DeltaPayloadMtoN, self).__init__(name, install_dir, build, *args,
571 install_subdir=install_subdir,
572 is_regex_name=True, **kwargs)
573
574
575chromeos_artifact_map[artifact_info.DELTA_PAYLOADS] = [DeltaPayloadNtoN,
576 DeltaPayloadMtoN]
577
578
579_AddCrOSArtifact(artifact_info.STATEFUL_PAYLOAD, Artifact,
580 devserver_constants.STATEFUL_FILE)
581_AddCrOSArtifact(artifact_info.BASE_IMAGE, BundledArtifact, IMAGE_FILE,
582 optional_name=BASE_IMAGE_FILE,
583 files_to_extract=[devserver_constants.BASE_IMAGE_FILE])
584_AddCrOSArtifact(artifact_info.RECOVERY_IMAGE, BundledArtifact, IMAGE_FILE,
585 optional_name=RECOVERY_IMAGE_FILE,
586 files_to_extract=[devserver_constants.RECOVERY_IMAGE_FILE])
587_AddCrOSArtifact(artifact_info.DEV_IMAGE, BundledArtifact, IMAGE_FILE,
588 files_to_extract=[devserver_constants.IMAGE_FILE])
589_AddCrOSArtifact(artifact_info.TEST_IMAGE, BundledArtifact, IMAGE_FILE,
590 optional_name=TEST_IMAGE_FILE,
591 files_to_extract=[devserver_constants.TEST_IMAGE_FILE])
592_AddCrOSArtifact(artifact_info.AUTOTEST, AutotestTarball, AUTOTEST_FILE,
593 files_to_extract=None, exclude=['autotest/test_suites'])
594_AddCrOSArtifact(artifact_info.CONTROL_FILES, BundledArtifact,
595 CONTROL_FILES_FILE)
596_AddCrOSArtifact(artifact_info.AUTOTEST_PACKAGES, AutotestTarball,
597 AUTOTEST_PACKAGES_FILE)
598_AddCrOSArtifact(artifact_info.TEST_SUITES, BundledArtifact, TEST_SUITES_FILE)
599_AddCrOSArtifact(artifact_info.AU_SUITE, BundledArtifact, AU_SUITE_FILE)
600_AddCrOSArtifact(artifact_info.AUTOTEST_SERVER_PACKAGE, Artifact,
601 AUTOTEST_SERVER_PACKAGE_FILE)
602_AddCrOSArtifact(artifact_info.FIRMWARE, Artifact, FIRMWARE_FILE)
603_AddCrOSArtifact(artifact_info.SYMBOLS, BundledArtifact, DEBUG_SYMBOLS_FILE,
604 files_to_extract=['debug/breakpad'])
605_AddCrOSArtifact(artifact_info.FACTORY_IMAGE, BundledArtifact, FACTORY_FILE,
606 files_to_extract=[devserver_constants.FACTORY_IMAGE_FILE])
Chris Sosa76e44b92013-01-31 12:11:38 -0800607
Chris Sosa968a1062013-08-02 17:42:50 -0700608# Add all the paygen_au artifacts in one go.
Gabe Black3b567202015-09-23 14:07:59 -0700609for c in devserver_constants.CHANNELS:
610 _AddCrOSArtifact(artifact_info.PAYGEN_AU_SUITE_TEMPLATE % {'channel': c},
611 BundledArtifact,
612 PAYGEN_AU_SUITE_FILE_TEMPLATE % {'channel': c})
613
614android_artifact_map = {}
Chris Sosa968a1062013-08-02 17:42:50 -0700615
Chris Sosa76e44b92013-01-31 12:11:38 -0800616
Gabe Black3b567202015-09-23 14:07:59 -0700617def _AddAndroidArtifact(tag, base, name, *fixed_args, **fixed_kwargs):
618 """Add a data wrapper that describes an Android artifact's implementation to
619 android_artifact_map.
620 """
621 artifact = _CreateNewArtifact(tag, base, name, *fixed_args, **fixed_kwargs)
622 android_artifact_map.setdefault(tag, []).append(artifact)
623
624
Dan Shiba4e00f2015-10-27 12:03:53 -0700625_AddAndroidArtifact(artifact_info.ANDROID_ZIP_IMAGES, Artifact,
Gabe Black3b567202015-09-23 14:07:59 -0700626 ANDROID_IMAGE_ZIP, is_regex_name=True)
627_AddAndroidArtifact(artifact_info.ANDROID_RADIO_IMAGE, Artifact,
628 ANDROID_RADIO_IMAGE)
629_AddAndroidArtifact(artifact_info.ANDROID_BOOTLOADER_IMAGE, Artifact,
630 ANDROID_BOOTLOADER_IMAGE)
631_AddAndroidArtifact(artifact_info.ANDROID_FASTBOOT, Artifact, ANDROID_FASTBOOT)
632_AddAndroidArtifact(artifact_info.ANDROID_TEST_ZIP, BundledArtifact,
633 ANDROID_TEST_ZIP, is_regex_name=True)
634
635class BaseArtifactFactory(object):
Chris Sosa76e44b92013-01-31 12:11:38 -0800636 """A factory class that generates build artifacts from artifact names."""
637
Gabe Black3b567202015-09-23 14:07:59 -0700638 def __init__(self, artifact_map, download_dir, artifacts, files, build):
Chris Sosa76e44b92013-01-31 12:11:38 -0800639 """Initalizes the member variables for the factory.
640
641 Args:
Gabe Black3b567202015-09-23 14:07:59 -0700642 artifact_map: A map from artifact names to ImplDescription objects.
Gilad Arnold950569b2013-08-27 14:38:01 -0700643 download_dir: A directory to which artifacts are downloaded.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700644 artifacts: List of artifacts to stage. These artifacts must be
645 defined in artifact_info.py and have a mapping in the
646 ARTIFACT_IMPLEMENTATION_MAP.
647 files: List of files to stage. These files are just downloaded and staged
648 as files into the download_dir.
Chris Sosa76e44b92013-01-31 12:11:38 -0800649 build: The name of the build.
650 """
Gabe Black3b567202015-09-23 14:07:59 -0700651 self.artifact_map = artifact_map
joychen0a8e34e2013-06-24 17:58:36 -0700652 self.download_dir = download_dir
Chris Sosa6b0c6172013-08-05 17:01:33 -0700653 self.artifacts = artifacts
654 self.files = files
Chris Sosa76e44b92013-01-31 12:11:38 -0800655 self.build = build
656
Chris Sosa6b0c6172013-08-05 17:01:33 -0700657 def _Artifacts(self, names, is_artifact):
Gabe Black3b567202015-09-23 14:07:59 -0700658 """Returns the Artifacts from |names|.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700659
660 If is_artifact is true, then these names define artifacts that must exist in
661 the ARTIFACT_IMPLEMENTATION_MAP. Otherwise, treat as filenames to stage as
662 basic BuildArtifacts.
663
Gilad Arnold950569b2013-08-27 14:38:01 -0700664 Args:
665 names: A sequence of artifact names.
666 is_artifact: Whether this is a named (True) or file (False) artifact.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800667
Gilad Arnold950569b2013-08-27 14:38:01 -0700668 Returns:
Gabe Black3b567202015-09-23 14:07:59 -0700669 An iterable of Artifacts.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800670
Gilad Arnold950569b2013-08-27 14:38:01 -0700671 Raises:
Gabe Black3b567202015-09-23 14:07:59 -0700672 KeyError: if artifact doesn't exist in the artifact map.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700673 """
Gabe Black3b567202015-09-23 14:07:59 -0700674 if is_artifact:
675 classes = itertools.chain(*(self.artifact_map[name] for name in names))
676 return list(cls(self.download_dir, self.build) for cls in classes)
677 else:
678 return list(Artifact(name, self.download_dir, self.build)
679 for name in names)
Chris Sosa76e44b92013-01-31 12:11:38 -0800680
681 def RequiredArtifacts(self):
Gilad Arnold950569b2013-08-27 14:38:01 -0700682 """Returns BuildArtifacts for the factory's artifacts.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700683
Gilad Arnold950569b2013-08-27 14:38:01 -0700684 Returns:
685 An iterable of BuildArtifacts.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800686
Gilad Arnold950569b2013-08-27 14:38:01 -0700687 Raises:
688 KeyError: if artifact doesn't exist in ARTIFACT_IMPLEMENTATION_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700689 """
690 artifacts = []
691 if self.artifacts:
692 artifacts.extend(self._Artifacts(self.artifacts, True))
693 if self.files:
694 artifacts.extend(self._Artifacts(self.files, False))
695
696 return artifacts
Chris Sosa76e44b92013-01-31 12:11:38 -0800697
698 def OptionalArtifacts(self):
Gilad Arnold950569b2013-08-27 14:38:01 -0700699 """Returns BuildArtifacts that should be cached.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700700
Gilad Arnold950569b2013-08-27 14:38:01 -0700701 Returns:
702 An iterable of BuildArtifacts.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800703
Gilad Arnold950569b2013-08-27 14:38:01 -0700704 Raises:
705 KeyError: if an optional artifact doesn't exist in
706 ARTIFACT_IMPLEMENTATION_MAP yet defined in
707 artifact_info.REQUESTED_TO_OPTIONAL_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700708 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800709 optional_names = set()
710 for artifact_name, optional_list in (
711 artifact_info.REQUESTED_TO_OPTIONAL_MAP.iteritems()):
712 # We are already downloading it.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700713 if artifact_name in self.artifacts:
Chris Sosa76e44b92013-01-31 12:11:38 -0800714 optional_names = optional_names.union(optional_list)
715
Chris Sosa6b0c6172013-08-05 17:01:33 -0700716 return self._Artifacts(optional_names - set(self.artifacts), True)
Chris Sosa968a1062013-08-02 17:42:50 -0700717
718
Gabe Black3b567202015-09-23 14:07:59 -0700719class ChromeOSArtifactFactory(BaseArtifactFactory):
720 """A factory class that generates ChromeOS build artifacts from names."""
721
722 def __init__(self, download_dir, artifacts, files, build):
723 """Pass the ChromeOS artifact map to the base class."""
724 super(ChromeOSArtifactFactory, self).__init__(
725 chromeos_artifact_map, download_dir, artifacts, files, build)
726
727
728class AndroidArtifactFactory(BaseArtifactFactory):
729 """A factory class that generates Android build artifacts from names."""
730
731 def __init__(self, download_dir, artifacts, files, build):
732 """Pass the Android artifact map to the base class."""
733 super(AndroidArtifactFactory, self).__init__(
734 android_artifact_map, download_dir, artifacts, files, build)
735
736
Chris Sosa968a1062013-08-02 17:42:50 -0700737# A simple main to verify correctness of the artifact map when making simple
738# name changes.
739if __name__ == '__main__':
Gabe Black3b567202015-09-23 14:07:59 -0700740 print('ARTIFACT IMPLEMENTATION MAPs (for debugging)')
741 print('FORMAT: ARTIFACT -> IMPLEMENTATION (<type>_file)')
742 for label, mapping in (('CHROMEOS', chromeos_artifact_map),
743 ('ANDROID', android_artifact_map)):
744 print('%s:' % label)
745 for key, value in sorted(mapping.items()):
746 print(' %s -> %s' % (key, ', '.join(str(val) for val in value)))