blob: e11eb7c2e2443fdb56a10b3b09c828dd16f446b3 [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'
Dan Shi74136ae2015-12-01 14:40:06 -080053ANDROID_VENDOR_PARTITION_ZIP = r'[^-]*-vendor_partitions-.*\.zip'
Chris Sosa76e44b92013-01-31 12:11:38 -080054
55_build_artifact_locks = common_util.LockDict()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070056
57
58class ArtifactDownloadError(Exception):
59 """Error used to signify an issue processing an artifact."""
60 pass
61
62
Gabe Black3b567202015-09-23 14:07:59 -070063class ArtifactMeta(type):
64 """metaclass for an artifact type.
65
66 This metaclass is for class Artifact and its subclasses to have a meaningful
67 string composed of class name and the corresponding artifact name, e.g.,
68 `Artifact_full_payload`. This helps to better logging, refer to logging in
69 method Downloader.Download.
70 """
71
72 ARTIFACT_NAME = None
73
74 def __str__(cls):
75 return '%s_%s' % (cls.__name__, cls.ARTIFACT_NAME)
76
77 def __repr__(cls):
78 return str(cls)
79
80
81class Artifact(log_util.Loggable):
82 """Wrapper around an artifact to download using a fetcher.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070083
84 The purpose of this class is to download objects from Google Storage
85 and install them to a local directory. There are two main functions, one to
86 download/prepare the artifacts in to a temporary staging area and the second
87 to stage it into its final destination.
Chris Sosa76e44b92013-01-31 12:11:38 -080088
Gilad Arnold950569b2013-08-27 14:38:01 -070089 IMPORTANT! (i) `name' is a glob expression by default (and not a regex), be
90 attentive when adding new artifacts; (ii) name matching semantics differ
91 between a glob (full name string match) and a regex (partial match).
92
Chris Sosa76e44b92013-01-31 12:11:38 -080093 Class members:
Gabe Black3b567202015-09-23 14:07:59 -070094 fetcher: An object which knows how to fetch the artifact.
Gilad Arnold950569b2013-08-27 14:38:01 -070095 name: Name given for artifact; in fact, it is a pattern that captures the
96 names of files contained in the artifact. This can either be an
97 ordinary shell-style glob (the default), or a regular expression (if
98 is_regex_name is True).
99 is_regex_name: Whether the name value is a regex (default: glob).
Chris Sosa76e44b92013-01-31 12:11:38 -0800100 build: The version of the build i.e. R26-2342.0.0.
101 marker_name: Name used to define the lock marker for the artifacts to
102 prevent it from being re-downloaded. By default based on name
103 but can be overriden by children.
Dan Shi6e50c722013-08-19 15:05:06 -0700104 exception_file_path: Path to a file containing the serialized exception,
105 which was raised in Process method. The file is located
106 in the parent folder of install_dir, since the
107 install_dir will be deleted if the build does not
108 existed.
joychen0a8e34e2013-06-24 17:58:36 -0700109 install_path: Path to artifact.
Gabe Black3b567202015-09-23 14:07:59 -0700110 install_subdir: Directory within install_path where the artifact is actually
111 stored.
Chris Sosa76e44b92013-01-31 12:11:38 -0800112 install_dir: The final location where the artifact should be staged to.
113 single_name: If True the name given should only match one item. Note, if not
114 True, self.name will become a list of items returned.
Gilad Arnold1638d822013-11-07 23:38:16 -0800115 installed_files: A list of files that were the final result of downloading
116 and setting up the artifact.
117 store_installed_files: Whether the list of installed files is stored in the
118 marker file.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700119 """
Gilad Arnold950569b2013-08-27 14:38:01 -0700120
Gabe Black3b567202015-09-23 14:07:59 -0700121 __metaclass__ = ArtifactMeta
122
123 def __init__(self, name, install_dir, build, install_subdir='',
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800124 is_regex_name=False, optional_name=None):
Gilad Arnold950569b2013-08-27 14:38:01 -0700125 """Constructor.
126
127 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800128 install_dir: Where to install the artifact.
Chris Sosa76e44b92013-01-31 12:11:38 -0800129 name: Identifying name to be used to find/store the artifact.
130 build: The name of the build e.g. board/release.
Gabe Black3b567202015-09-23 14:07:59 -0700131 install_subdir: Directory within install_path where the artifact is
132 actually stored.
Gilad Arnold950569b2013-08-27 14:38:01 -0700133 is_regex_name: Whether the name pattern is a regex (default: glob).
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800134 optional_name: An alternative name to find the artifact, which can lead
135 to faster download. Unlike |name|, there is no guarantee that an
136 artifact named |optional_name| is/will be on Google Storage. If it
137 exists, we download it. Otherwise, we fall back to wait for |name|.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700138 """
Gabe Black3b567202015-09-23 14:07:59 -0700139 super(Artifact, self).__init__()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700140
Chris Sosa76e44b92013-01-31 12:11:38 -0800141 # In-memory lock to keep the devserver from colliding with itself while
142 # attempting to stage the same artifact.
143 self._process_lock = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700144
Chris Sosa76e44b92013-01-31 12:11:38 -0800145 self.name = name
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800146 self.optional_name = optional_name
Gilad Arnold950569b2013-08-27 14:38:01 -0700147 self.is_regex_name = is_regex_name
Chris Sosa76e44b92013-01-31 12:11:38 -0800148 self.build = build
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700149
Chris Sosa76e44b92013-01-31 12:11:38 -0800150 self.marker_name = '.' + self._SanitizeName(name)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700151
Dan Shi6e50c722013-08-19 15:05:06 -0700152 exception_file_name = ('.' + self._SanitizeName(build) + self.marker_name +
153 '.exception')
154 # The exception file needs to be located in parent folder, since the
155 # install_dir will be deleted is the build does not exist.
156 self.exception_file_path = os.path.join(os.path.dirname(install_dir),
Gilad Arnold950569b2013-08-27 14:38:01 -0700157 exception_file_name)
Dan Shi6e50c722013-08-19 15:05:06 -0700158
joychen0a8e34e2013-06-24 17:58:36 -0700159 self.install_path = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700160
Chris Sosa76e44b92013-01-31 12:11:38 -0800161 self.install_dir = install_dir
Gabe Black3b567202015-09-23 14:07:59 -0700162 self.install_subdir = install_subdir
Chris Sosa76e44b92013-01-31 12:11:38 -0800163
164 self.single_name = True
165
Gilad Arnold1638d822013-11-07 23:38:16 -0800166 self.installed_files = []
167 self.store_installed_files = True
168
Chris Sosa76e44b92013-01-31 12:11:38 -0800169 @staticmethod
170 def _SanitizeName(name):
171 """Sanitizes name to be used for creating a file on the filesystem.
172
173 '.','/' and '*' have special meaning in FS lingo. Replace them with words.
Gilad Arnold950569b2013-08-27 14:38:01 -0700174
175 Args:
176 name: A file name/path.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800177
Gilad Arnold950569b2013-08-27 14:38:01 -0700178 Returns:
179 The sanitized name/path.
Chris Sosa76e44b92013-01-31 12:11:38 -0800180 """
181 return name.replace('*', 'STAR').replace('.', 'DOT').replace('/', 'SLASH')
182
Dan Shif8eb0d12013-08-01 17:52:06 -0700183 def ArtifactStaged(self):
Gilad Arnold1638d822013-11-07 23:38:16 -0800184 """Returns True if artifact is already staged.
185
186 This checks for (1) presence of the artifact marker file, and (2) the
187 presence of each installed file listed in this marker. Both must hold for
188 the artifact to be considered staged. Note that this method is safe for use
189 even if the artifacts were not stageed by this instance, as it is assumed
Gabe Black3b567202015-09-23 14:07:59 -0700190 that any Artifact instance that did the staging wrote the list of
Gilad Arnold1638d822013-11-07 23:38:16 -0800191 files actually installed into the marker.
192 """
193 marker_file = os.path.join(self.install_dir, self.marker_name)
194
195 # If the marker is missing, it's definitely not staged.
196 if not os.path.exists(marker_file):
197 return False
198
199 # We want to ensure that every file listed in the marker is actually there.
200 if self.store_installed_files:
201 with open(marker_file) as f:
202 files = [line.strip() for line in f]
203
204 # Check to see if any of the purportedly installed files are missing, in
205 # which case the marker is outdated and should be removed.
206 missing_files = [fname for fname in files if not os.path.exists(fname)]
207 if missing_files:
208 self._Log('***ATTENTION*** %s files listed in %s are missing:\n%s',
209 'All' if len(files) == len(missing_files) else 'Some',
210 marker_file, '\n'.join(missing_files))
211 os.remove(marker_file)
212 return False
213
214 return True
Chris Sosa76e44b92013-01-31 12:11:38 -0800215
216 def _MarkArtifactStaged(self):
217 """Marks the artifact as staged."""
218 with open(os.path.join(self.install_dir, self.marker_name), 'w') as f:
Gilad Arnold1638d822013-11-07 23:38:16 -0800219 f.write('\n'.join(self.installed_files))
Chris Sosa76e44b92013-01-31 12:11:38 -0800220
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800221 def _UpdateName(self, names):
222 if self.single_name and len(names) > 1:
223 raise ArtifactDownloadError('Too many artifacts match %s' % self.name)
Chris Sosa76e44b92013-01-31 12:11:38 -0800224
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800225 self.name = names[0]
Chris Sosa76e44b92013-01-31 12:11:38 -0800226
joychen0a8e34e2013-06-24 17:58:36 -0700227 def _Setup(self):
Gilad Arnold1638d822013-11-07 23:38:16 -0800228 """Process the downloaded content, update the list of installed files."""
229 # In this primitive case, what was downloaded (has to be a single file) is
230 # what's installed.
231 self.installed_files = [self.install_path]
joychen0a8e34e2013-06-24 17:58:36 -0700232
Dan Shi6e50c722013-08-19 15:05:06 -0700233 def _ClearException(self):
234 """Delete any existing exception saved for this artifact."""
235 if os.path.exists(self.exception_file_path):
236 os.remove(self.exception_file_path)
237
238 def _SaveException(self, e):
239 """Save the exception to a file for downloader.IsStaged to retrieve.
240
Gilad Arnold950569b2013-08-27 14:38:01 -0700241 Args:
242 e: Exception object to be saved.
Dan Shi6e50c722013-08-19 15:05:06 -0700243 """
244 with open(self.exception_file_path, 'w') as f:
245 pickle.dump(e, f)
246
247 def GetException(self):
248 """Retrieve any exception that was raised in Process method.
249
Gilad Arnold950569b2013-08-27 14:38:01 -0700250 Returns:
251 An Exception object that was raised when trying to process the artifact.
252 Return None if no exception was found.
Dan Shi6e50c722013-08-19 15:05:06 -0700253 """
254 if not os.path.exists(self.exception_file_path):
255 return None
256 with open(self.exception_file_path, 'r') as f:
257 return pickle.load(f)
Chris Sosa76e44b92013-01-31 12:11:38 -0800258
Gabe Black3b567202015-09-23 14:07:59 -0700259 def Process(self, downloader, no_wait):
Chris Sosa76e44b92013-01-31 12:11:38 -0800260 """Main call point to all artifacts. Downloads and Stages artifact.
261
262 Downloads and Stages artifact from Google Storage to the install directory
263 specified in the constructor. It multi-thread safe and does not overwrite
264 the artifact if it's already been downloaded or being downloaded. After
265 processing, leaves behind a marker to indicate to future invocations that
266 the artifact has already been staged based on the name of the artifact.
267
268 Do not override as it modifies important private variables, ensures thread
269 safety, and maintains cache semantics.
270
271 Note: this may be a blocking call when the artifact is already in the
272 process of being staged.
273
274 Args:
Gabe Black3b567202015-09-23 14:07:59 -0700275 downloader: A downloader instance containing the logic to download
276 artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800277 no_wait: If True, don't block waiting for artifact to exist if we fail to
278 immediately find it.
279
280 Raises:
281 ArtifactDownloadError: If the artifact fails to download from Google
282 Storage for any reason or that the regexp
283 defined by name is not specific enough.
284 """
285 if not self._process_lock:
286 self._process_lock = _build_artifact_locks.lock(
287 os.path.join(self.install_dir, self.name))
288
Gabe Black3b567202015-09-23 14:07:59 -0700289 real_install_dir = os.path.join(self.install_dir, self.install_subdir)
Chris Sosa76e44b92013-01-31 12:11:38 -0800290 with self._process_lock:
Gabe Black3b567202015-09-23 14:07:59 -0700291 common_util.MkDirP(real_install_dir)
Dan Shif8eb0d12013-08-01 17:52:06 -0700292 if not self.ArtifactStaged():
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800293 # Delete any existing exception saved for this artifact.
294 self._ClearException()
295 found_artifact = False
296 if self.optional_name:
297 try:
Gabe Black3b567202015-09-23 14:07:59 -0700298 # Check if the artifact named |optional_name| exists.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800299 # Because this artifact may not always exist, don't bother
300 # to wait for it (set timeout=1).
Gabe Black3b567202015-09-23 14:07:59 -0700301 new_names = downloader.Wait(
302 self.optional_name, self.is_regex_name, timeout=1)
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800303 self._UpdateName(new_names)
304
305 except ArtifactDownloadError:
306 self._Log('Unable to download %s; fall back to download %s',
307 self.optional_name, self.name)
308 else:
309 found_artifact = True
310
Dan Shi6e50c722013-08-19 15:05:06 -0700311 try:
Dan Shi6e50c722013-08-19 15:05:06 -0700312 # If the artifact should already have been uploaded, don't waste
313 # cycles waiting around for it to exist.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800314 if not found_artifact:
315 timeout = 1 if no_wait else 10
Gabe Black3b567202015-09-23 14:07:59 -0700316 new_names = downloader.Wait(
317 self.name, self.is_regex_name, timeout)
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800318 self._UpdateName(new_names)
319
320 self._Log('Downloading file %s', self.name)
Gabe Black3b567202015-09-23 14:07:59 -0700321 self.install_path = downloader.Fetch(self.name, real_install_dir)
Dan Shi6e50c722013-08-19 15:05:06 -0700322 self._Setup()
323 self._MarkArtifactStaged()
324 except Exception as e:
325 # Save the exception to a file for downloader.IsStaged to retrieve.
326 self._SaveException(e)
Gilad Arnold02dc6552013-11-14 11:27:54 -0800327
328 # Convert an unknown exception into an ArtifactDownloadError.
329 if type(e) is ArtifactDownloadError:
330 raise
331 else:
332 raise ArtifactDownloadError('An error occurred: %s' % e)
Chris Sosa76e44b92013-01-31 12:11:38 -0800333 else:
334 self._Log('%s is already staged.', self)
335
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700336 def __str__(self):
337 """String representation for the download."""
Gabe Black3b567202015-09-23 14:07:59 -0700338 return '%s->%s' % (self.name, self.install_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700339
Chris Sosab26b1202013-08-16 16:40:55 -0700340 def __repr__(self):
341 return str(self)
342
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700343
Gabe Black3b567202015-09-23 14:07:59 -0700344class AUTestPayload(Artifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700345 """Wrapper for AUTest delta payloads which need additional setup."""
Gilad Arnold950569b2013-08-27 14:38:01 -0700346
joychen0a8e34e2013-06-24 17:58:36 -0700347 def _Setup(self):
Gabe Black3b567202015-09-23 14:07:59 -0700348 super(AUTestPayload, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700349
Chris Sosa76e44b92013-01-31 12:11:38 -0800350 # Rename to update.gz.
Gabe Black3b567202015-09-23 14:07:59 -0700351 install_path = os.path.join(self.install_dir, self.install_subdir,
352 self.name)
353 new_install_path = os.path.join(self.install_dir, self.install_subdir,
joychen7c2054a2013-07-25 11:14:07 -0700354 devserver_constants.UPDATE_FILE)
Chris Sosa76e44b92013-01-31 12:11:38 -0800355 shutil.move(install_path, new_install_path)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700356
Gilad Arnold1638d822013-11-07 23:38:16 -0800357 # Reflect the rename in the list of installed files.
358 self.installed_files.remove(install_path)
359 self.installed_files = [new_install_path]
360
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700361
Gabe Black3b567202015-09-23 14:07:59 -0700362class DeltaPayloadBase(AUTestPayload):
Chris Sosa76e44b92013-01-31 12:11:38 -0800363 """Delta payloads from the archive_url.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700364
Gabe Black3b567202015-09-23 14:07:59 -0700365 These artifacts are super strange. They custom handle directories and
366 pull in all delta payloads. We can't specify exactly what we want
Chris Sosa76e44b92013-01-31 12:11:38 -0800367 because unlike other artifacts, this one does not conform to something a
368 client might know. The client doesn't know the version of n-1 or whether it
369 was even generated.
Gilad Arnold950569b2013-08-27 14:38:01 -0700370
371 IMPORTANT! Note that this artifact simply ignores the `name' argument because
Gabe Black3b567202015-09-23 14:07:59 -0700372 that name is derived internally.
Chris Sosa76e44b92013-01-31 12:11:38 -0800373 """
Gilad Arnold950569b2013-08-27 14:38:01 -0700374
joychen0a8e34e2013-06-24 17:58:36 -0700375 def _Setup(self):
Gabe Black3b567202015-09-23 14:07:59 -0700376 super(DeltaPayloadBase, self)._Setup()
377 # Setup symlink so that AU will work for this payload.
378 stateful_update_symlink = os.path.join(
379 self.install_dir, self.install_subdir,
380 devserver_constants.STATEFUL_FILE)
381 os.symlink(os.path.join(os.pardir, os.pardir,
382 devserver_constants.STATEFUL_FILE),
383 stateful_update_symlink)
384 self.installed_files.append(stateful_update_symlink)
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700385
Chris Sosa76e44b92013-01-31 12:11:38 -0800386
Gabe Black3b567202015-09-23 14:07:59 -0700387class BundledArtifact(Artifact):
Chris Sosa76e44b92013-01-31 12:11:38 -0800388 """A single build artifact bundle e.g. zip file or tar file."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800389
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800390 def __init__(self, *args, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700391 """Takes Artifact args with some additional ones.
Gilad Arnold950569b2013-08-27 14:38:01 -0700392
393 Args:
Gabe Black3b567202015-09-23 14:07:59 -0700394 *args: See Artifact documentation.
395 **kwargs: See Artifact documentation.
Gilad Arnold950569b2013-08-27 14:38:01 -0700396 files_to_extract: A list of files to extract. If set to None, extract
397 all files.
398 exclude: A list of files to exclude. If None, no files are excluded.
Chris Sosa76e44b92013-01-31 12:11:38 -0800399 """
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800400 self._files_to_extract = kwargs.pop('files_to_extract', None)
401 self._exclude = kwargs.pop('exclude', None)
Gabe Black3b567202015-09-23 14:07:59 -0700402 super(BundledArtifact, self).__init__(*args, **kwargs)
Chris Sosa76e44b92013-01-31 12:11:38 -0800403
404 # We modify the marker so that it is unique to what was staged.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800405 if self._files_to_extract:
Chris Sosa76e44b92013-01-31 12:11:38 -0800406 self.marker_name = self._SanitizeName(
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800407 '_'.join(['.' + self.name] + self._files_to_extract))
Chris Sosa76e44b92013-01-31 12:11:38 -0800408
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800409 def _RunUnzip(self, list_only):
410 # Unzip is weird. It expects its args before any excludes and expects its
411 # excludes in a list following the -x.
412 cmd = ['unzip', '-qql' if list_only else '-o', self.install_path]
413 if not list_only:
414 cmd += ['-d', self.install_dir]
Chris Sosa76e44b92013-01-31 12:11:38 -0800415
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800416 if self._files_to_extract:
417 cmd.extend(self._files_to_extract)
418
419 if self._exclude:
420 cmd.append('-x')
421 cmd.extend(self._exclude)
422
423 try:
424 return subprocess.check_output(cmd).strip('\n').splitlines()
425 except subprocess.CalledProcessError, e:
426 raise ArtifactDownloadError(
427 'An error occurred when attempting to unzip %s:\n%s' %
428 (self.install_path, e))
Chris Sosa76e44b92013-01-31 12:11:38 -0800429
joychen0a8e34e2013-06-24 17:58:36 -0700430 def _Setup(self):
Gilad Arnold1638d822013-11-07 23:38:16 -0800431 extract_result = self._Extract()
432 if self.store_installed_files:
433 # List both the archive and the extracted files.
434 self.installed_files.append(self.install_path)
435 self.installed_files.extend(extract_result)
Chris Sosa76e44b92013-01-31 12:11:38 -0800436
Chris Sosa76e44b92013-01-31 12:11:38 -0800437 def _Extract(self):
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800438 """Extracts files into the install path."""
439 if self.name.endswith('.zip'):
440 return self._ExtractZipfile()
441 else:
442 return self._ExtractTarball()
443
444 def _ExtractZipfile(self):
445 """Extracts a zip file using unzip."""
446 file_list = [os.path.join(self.install_dir, line[30:].strip())
447 for line in self._RunUnzip(True)
448 if not line.endswith('/')]
449 if file_list:
450 self._RunUnzip(False)
451
452 return file_list
453
454 def _ExtractTarball(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800455 """Extracts a tarball using tar.
456
457 Detects whether the tarball is compressed or not based on the file
458 extension and extracts the tarball into the install_path.
459 """
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700460 try:
Gilad Arnold1638d822013-11-07 23:38:16 -0800461 return common_util.ExtractTarball(self.install_path, self.install_dir,
462 files_to_extract=self._files_to_extract,
463 excluded_files=self._exclude,
464 return_extracted_files=True)
Simran Basi4baad082013-02-14 13:39:18 -0800465 except common_util.CommonUtilError as e:
466 raise ArtifactDownloadError(str(e))
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700467
468
Gabe Black3b567202015-09-23 14:07:59 -0700469class AutotestTarball(BundledArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700470 """Wrapper around the autotest tarball to download from gsutil."""
471
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800472 def __init__(self, *args, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700473 super(AutotestTarball, self).__init__(*args, **kwargs)
Gilad Arnold1638d822013-11-07 23:38:16 -0800474 # We don't store/check explicit file lists in Autotest tarball markers;
475 # this can get huge and unwieldy, and generally make little sense.
476 self.store_installed_files = False
477
joychen0a8e34e2013-06-24 17:58:36 -0700478 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800479 """Extracts the tarball into the install path excluding test suites."""
Gabe Black3b567202015-09-23 14:07:59 -0700480 super(AutotestTarball, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700481
Chris Sosa76e44b92013-01-31 12:11:38 -0800482 # Deal with older autotest packages that may not be bundled.
joychen3cb228e2013-06-12 12:13:13 -0700483 autotest_dir = os.path.join(self.install_dir,
484 devserver_constants.AUTOTEST_DIR)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700485 autotest_pkgs_dir = os.path.join(autotest_dir, 'packages')
486 if not os.path.exists(autotest_pkgs_dir):
487 os.makedirs(autotest_pkgs_dir)
488
489 if not os.path.exists(os.path.join(autotest_pkgs_dir, 'packages.checksum')):
Chris Sosa76e44b92013-01-31 12:11:38 -0800490 cmd = ['autotest/utils/packager.py', 'upload', '--repository',
491 autotest_pkgs_dir, '--all']
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700492 try:
joychen0a8e34e2013-06-24 17:58:36 -0700493 subprocess.check_call(cmd, cwd=self.install_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700494 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800495 raise ArtifactDownloadError(
496 'Failed to create autotest packages!:\n%s' % e)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700497 else:
Gilad Arnoldf5843132012-09-25 00:31:20 -0700498 self._Log('Using pre-generated packages from autotest')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700499
Chris Masone816e38c2012-05-02 12:22:36 -0700500
Gabe Black3b567202015-09-23 14:07:59 -0700501def _CreateNewArtifact(tag, base, name, *fixed_args, **fixed_kwargs):
502 """Get a data wrapper that describes an artifact's implementation.
Gilad Arnold950569b2013-08-27 14:38:01 -0700503
Gabe Black3b567202015-09-23 14:07:59 -0700504 Args:
505 tag: Tag of the artifact, defined in artifact_info.
506 base: Class of the artifact, e.g., BundledArtifact.
507 name: Name of the artifact, e.g., image.zip.
508 *fixed_args: Fixed arguments that are additional to the one used in base
509 class.
510 **fixed_kwargs: Fixed keyword arguments that are additional to the one used
511 in base class.
Chris Sosa76e44b92013-01-31 12:11:38 -0800512
Gabe Black3b567202015-09-23 14:07:59 -0700513 Returns:
514 A data wrapper that describes an artifact's implementation.
Chris Sosa76e44b92013-01-31 12:11:38 -0800515
Gabe Black3b567202015-09-23 14:07:59 -0700516 """
517 class NewArtifact(base):
518 """A data wrapper that describes an artifact's implementation."""
519 ARTIFACT_TAG = tag
520 ARTIFACT_NAME = name
521
522 def __init__(self, *args, **kwargs):
523 all_args = fixed_args + args
524 all_kwargs = {}
525 all_kwargs.update(fixed_kwargs)
526 all_kwargs.update(kwargs)
527 super(NewArtifact, self).__init__(self.ARTIFACT_NAME,
528 *all_args, **all_kwargs)
529
530 NewArtifact.__name__ = base.__name__
531 return NewArtifact
Chris Sosa968a1062013-08-02 17:42:50 -0700532
Chris Sosa76e44b92013-01-31 12:11:38 -0800533
Gabe Black3b567202015-09-23 14:07:59 -0700534# TODO(dshi): Refactor the code here to split out the logic of creating the
535# artifacts mapping to a different module.
536chromeos_artifact_map = {}
Chris Sosa76e44b92013-01-31 12:11:38 -0800537
Chris Sosa76e44b92013-01-31 12:11:38 -0800538
Gabe Black3b567202015-09-23 14:07:59 -0700539def _AddCrOSArtifact(tag, base, name, *fixed_args, **fixed_kwargs):
540 """Add a data wrapper that describes a ChromeOS artifact's implementation to
541 chromeos_artifact_map.
542 """
543 artifact = _CreateNewArtifact(tag, base, name, *fixed_args, **fixed_kwargs)
544 chromeos_artifact_map.setdefault(tag, []).append(artifact)
Chris Sosa76e44b92013-01-31 12:11:38 -0800545
beepsc3d0f872013-07-31 21:50:40 -0700546
Gabe Black3b567202015-09-23 14:07:59 -0700547_AddCrOSArtifact(artifact_info.FULL_PAYLOAD, AUTestPayload, '*_full_*')
548
549
550class DeltaPayloadNtoN(DeltaPayloadBase):
551 """ChromeOS Delta payload artifact for updating from version N to N."""
552 ARTIFACT_TAG = artifact_info.DELTA_PAYLOADS
553 ARTIFACT_NAME = 'NOT_APPLICABLE'
554
555 def __init__(self, install_dir, build, *args, **kwargs):
556 name = 'chromeos_%s*_delta_*' % build
557 install_subdir = os.path.join(_AU_BASE, build + _NTON_DIR_SUFFIX)
558 super(DeltaPayloadNtoN, self).__init__(name, install_dir, build, *args,
559 install_subdir=install_subdir,
560 **kwargs)
561
562
563class DeltaPayloadMtoN(DeltaPayloadBase):
564 """ChromeOS Delta payload artifact for updating from version M to N."""
565 ARTIFACT_TAG = artifact_info.DELTA_PAYLOADS
566 ARTIFACT_NAME = 'NOT_APPLICABLE'
567
568 def __init__(self, install_dir, build, *args, **kwargs):
569 name = ('chromeos_(?!%s).*_delta_.*' % re.escape(build))
570 install_subdir = os.path.join(_AU_BASE, build + _MTON_DIR_SUFFIX)
571 super(DeltaPayloadMtoN, self).__init__(name, install_dir, build, *args,
572 install_subdir=install_subdir,
573 is_regex_name=True, **kwargs)
574
575
576chromeos_artifact_map[artifact_info.DELTA_PAYLOADS] = [DeltaPayloadNtoN,
577 DeltaPayloadMtoN]
578
579
580_AddCrOSArtifact(artifact_info.STATEFUL_PAYLOAD, Artifact,
581 devserver_constants.STATEFUL_FILE)
582_AddCrOSArtifact(artifact_info.BASE_IMAGE, BundledArtifact, IMAGE_FILE,
583 optional_name=BASE_IMAGE_FILE,
584 files_to_extract=[devserver_constants.BASE_IMAGE_FILE])
585_AddCrOSArtifact(artifact_info.RECOVERY_IMAGE, BundledArtifact, IMAGE_FILE,
586 optional_name=RECOVERY_IMAGE_FILE,
587 files_to_extract=[devserver_constants.RECOVERY_IMAGE_FILE])
588_AddCrOSArtifact(artifact_info.DEV_IMAGE, BundledArtifact, IMAGE_FILE,
589 files_to_extract=[devserver_constants.IMAGE_FILE])
590_AddCrOSArtifact(artifact_info.TEST_IMAGE, BundledArtifact, IMAGE_FILE,
591 optional_name=TEST_IMAGE_FILE,
592 files_to_extract=[devserver_constants.TEST_IMAGE_FILE])
593_AddCrOSArtifact(artifact_info.AUTOTEST, AutotestTarball, AUTOTEST_FILE,
594 files_to_extract=None, exclude=['autotest/test_suites'])
595_AddCrOSArtifact(artifact_info.CONTROL_FILES, BundledArtifact,
596 CONTROL_FILES_FILE)
597_AddCrOSArtifact(artifact_info.AUTOTEST_PACKAGES, AutotestTarball,
598 AUTOTEST_PACKAGES_FILE)
599_AddCrOSArtifact(artifact_info.TEST_SUITES, BundledArtifact, TEST_SUITES_FILE)
600_AddCrOSArtifact(artifact_info.AU_SUITE, BundledArtifact, AU_SUITE_FILE)
601_AddCrOSArtifact(artifact_info.AUTOTEST_SERVER_PACKAGE, Artifact,
602 AUTOTEST_SERVER_PACKAGE_FILE)
603_AddCrOSArtifact(artifact_info.FIRMWARE, Artifact, FIRMWARE_FILE)
604_AddCrOSArtifact(artifact_info.SYMBOLS, BundledArtifact, DEBUG_SYMBOLS_FILE,
605 files_to_extract=['debug/breakpad'])
606_AddCrOSArtifact(artifact_info.FACTORY_IMAGE, BundledArtifact, FACTORY_FILE,
607 files_to_extract=[devserver_constants.FACTORY_IMAGE_FILE])
Chris Sosa76e44b92013-01-31 12:11:38 -0800608
Chris Sosa968a1062013-08-02 17:42:50 -0700609# Add all the paygen_au artifacts in one go.
Gabe Black3b567202015-09-23 14:07:59 -0700610for c in devserver_constants.CHANNELS:
611 _AddCrOSArtifact(artifact_info.PAYGEN_AU_SUITE_TEMPLATE % {'channel': c},
612 BundledArtifact,
613 PAYGEN_AU_SUITE_FILE_TEMPLATE % {'channel': c})
614
615android_artifact_map = {}
Chris Sosa968a1062013-08-02 17:42:50 -0700616
Chris Sosa76e44b92013-01-31 12:11:38 -0800617
Gabe Black3b567202015-09-23 14:07:59 -0700618def _AddAndroidArtifact(tag, base, name, *fixed_args, **fixed_kwargs):
619 """Add a data wrapper that describes an Android artifact's implementation to
620 android_artifact_map.
621 """
622 artifact = _CreateNewArtifact(tag, base, name, *fixed_args, **fixed_kwargs)
623 android_artifact_map.setdefault(tag, []).append(artifact)
624
625
Dan Shiba4e00f2015-10-27 12:03:53 -0700626_AddAndroidArtifact(artifact_info.ANDROID_ZIP_IMAGES, Artifact,
Gabe Black3b567202015-09-23 14:07:59 -0700627 ANDROID_IMAGE_ZIP, is_regex_name=True)
628_AddAndroidArtifact(artifact_info.ANDROID_RADIO_IMAGE, Artifact,
629 ANDROID_RADIO_IMAGE)
630_AddAndroidArtifact(artifact_info.ANDROID_BOOTLOADER_IMAGE, Artifact,
631 ANDROID_BOOTLOADER_IMAGE)
632_AddAndroidArtifact(artifact_info.ANDROID_FASTBOOT, Artifact, ANDROID_FASTBOOT)
633_AddAndroidArtifact(artifact_info.ANDROID_TEST_ZIP, BundledArtifact,
634 ANDROID_TEST_ZIP, is_regex_name=True)
Dan Shi74136ae2015-12-01 14:40:06 -0800635_AddAndroidArtifact(artifact_info.ANDROID_VENDOR_PARTITION_ZIP, Artifact,
636 ANDROID_VENDOR_PARTITION_ZIP, is_regex_name=True)
Gabe Black3b567202015-09-23 14:07:59 -0700637
638class BaseArtifactFactory(object):
Chris Sosa76e44b92013-01-31 12:11:38 -0800639 """A factory class that generates build artifacts from artifact names."""
640
Gabe Black3b567202015-09-23 14:07:59 -0700641 def __init__(self, artifact_map, download_dir, artifacts, files, build):
Chris Sosa76e44b92013-01-31 12:11:38 -0800642 """Initalizes the member variables for the factory.
643
644 Args:
Gabe Black3b567202015-09-23 14:07:59 -0700645 artifact_map: A map from artifact names to ImplDescription objects.
Gilad Arnold950569b2013-08-27 14:38:01 -0700646 download_dir: A directory to which artifacts are downloaded.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700647 artifacts: List of artifacts to stage. These artifacts must be
648 defined in artifact_info.py and have a mapping in the
649 ARTIFACT_IMPLEMENTATION_MAP.
650 files: List of files to stage. These files are just downloaded and staged
651 as files into the download_dir.
Chris Sosa76e44b92013-01-31 12:11:38 -0800652 build: The name of the build.
653 """
Gabe Black3b567202015-09-23 14:07:59 -0700654 self.artifact_map = artifact_map
joychen0a8e34e2013-06-24 17:58:36 -0700655 self.download_dir = download_dir
Chris Sosa6b0c6172013-08-05 17:01:33 -0700656 self.artifacts = artifacts
657 self.files = files
Chris Sosa76e44b92013-01-31 12:11:38 -0800658 self.build = build
659
Chris Sosa6b0c6172013-08-05 17:01:33 -0700660 def _Artifacts(self, names, is_artifact):
Gabe Black3b567202015-09-23 14:07:59 -0700661 """Returns the Artifacts from |names|.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700662
663 If is_artifact is true, then these names define artifacts that must exist in
664 the ARTIFACT_IMPLEMENTATION_MAP. Otherwise, treat as filenames to stage as
665 basic BuildArtifacts.
666
Gilad Arnold950569b2013-08-27 14:38:01 -0700667 Args:
668 names: A sequence of artifact names.
669 is_artifact: Whether this is a named (True) or file (False) artifact.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800670
Gilad Arnold950569b2013-08-27 14:38:01 -0700671 Returns:
Gabe Black3b567202015-09-23 14:07:59 -0700672 An iterable of Artifacts.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800673
Gilad Arnold950569b2013-08-27 14:38:01 -0700674 Raises:
Gabe Black3b567202015-09-23 14:07:59 -0700675 KeyError: if artifact doesn't exist in the artifact map.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700676 """
Gabe Black3b567202015-09-23 14:07:59 -0700677 if is_artifact:
678 classes = itertools.chain(*(self.artifact_map[name] for name in names))
679 return list(cls(self.download_dir, self.build) for cls in classes)
680 else:
681 return list(Artifact(name, self.download_dir, self.build)
682 for name in names)
Chris Sosa76e44b92013-01-31 12:11:38 -0800683
684 def RequiredArtifacts(self):
Gilad Arnold950569b2013-08-27 14:38:01 -0700685 """Returns BuildArtifacts for the factory's artifacts.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700686
Gilad Arnold950569b2013-08-27 14:38:01 -0700687 Returns:
688 An iterable of BuildArtifacts.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800689
Gilad Arnold950569b2013-08-27 14:38:01 -0700690 Raises:
691 KeyError: if artifact doesn't exist in ARTIFACT_IMPLEMENTATION_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700692 """
693 artifacts = []
694 if self.artifacts:
695 artifacts.extend(self._Artifacts(self.artifacts, True))
696 if self.files:
697 artifacts.extend(self._Artifacts(self.files, False))
698
699 return artifacts
Chris Sosa76e44b92013-01-31 12:11:38 -0800700
701 def OptionalArtifacts(self):
Gilad Arnold950569b2013-08-27 14:38:01 -0700702 """Returns BuildArtifacts that should be cached.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700703
Gilad Arnold950569b2013-08-27 14:38:01 -0700704 Returns:
705 An iterable of BuildArtifacts.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800706
Gilad Arnold950569b2013-08-27 14:38:01 -0700707 Raises:
708 KeyError: if an optional artifact doesn't exist in
709 ARTIFACT_IMPLEMENTATION_MAP yet defined in
710 artifact_info.REQUESTED_TO_OPTIONAL_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700711 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800712 optional_names = set()
713 for artifact_name, optional_list in (
714 artifact_info.REQUESTED_TO_OPTIONAL_MAP.iteritems()):
715 # We are already downloading it.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700716 if artifact_name in self.artifacts:
Chris Sosa76e44b92013-01-31 12:11:38 -0800717 optional_names = optional_names.union(optional_list)
718
Chris Sosa6b0c6172013-08-05 17:01:33 -0700719 return self._Artifacts(optional_names - set(self.artifacts), True)
Chris Sosa968a1062013-08-02 17:42:50 -0700720
721
Gabe Black3b567202015-09-23 14:07:59 -0700722class ChromeOSArtifactFactory(BaseArtifactFactory):
723 """A factory class that generates ChromeOS build artifacts from names."""
724
725 def __init__(self, download_dir, artifacts, files, build):
726 """Pass the ChromeOS artifact map to the base class."""
727 super(ChromeOSArtifactFactory, self).__init__(
728 chromeos_artifact_map, download_dir, artifacts, files, build)
729
730
731class AndroidArtifactFactory(BaseArtifactFactory):
732 """A factory class that generates Android build artifacts from names."""
733
734 def __init__(self, download_dir, artifacts, files, build):
735 """Pass the Android artifact map to the base class."""
736 super(AndroidArtifactFactory, self).__init__(
737 android_artifact_map, download_dir, artifacts, files, build)
738
739
Chris Sosa968a1062013-08-02 17:42:50 -0700740# A simple main to verify correctness of the artifact map when making simple
741# name changes.
742if __name__ == '__main__':
Gabe Black3b567202015-09-23 14:07:59 -0700743 print('ARTIFACT IMPLEMENTATION MAPs (for debugging)')
744 print('FORMAT: ARTIFACT -> IMPLEMENTATION (<type>_file)')
745 for label, mapping in (('CHROMEOS', chromeos_artifact_map),
746 ('ANDROID', android_artifact_map)):
747 print('%s:' % label)
748 for key, value in sorted(mapping.items()):
749 print(' %s -> %s' % (key, ', '.join(str(val) for val in value)))