blob: 6ebc1ea0e1f65d022d08ae145bccf00946839abd [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'
Dan Shi6c2b2a22016-03-04 15:52:19 -080054ANDROID_AUTOTEST_SERVER_PACKAGE = r'[^-]*-autotest_server_package-.*\.tar.bz2'
55ANDROID_TEST_SUITES = r'[^-]*-test_suites-.*\.tar.bz2'
56ANDROID_CONTROL_FILES = r'[^-]*-autotest_control_files-.*\.tar'
Chris Sosa76e44b92013-01-31 12:11:38 -080057
58_build_artifact_locks = common_util.LockDict()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070059
60
61class ArtifactDownloadError(Exception):
62 """Error used to signify an issue processing an artifact."""
63 pass
64
65
Gabe Black3b567202015-09-23 14:07:59 -070066class ArtifactMeta(type):
67 """metaclass for an artifact type.
68
69 This metaclass is for class Artifact and its subclasses to have a meaningful
70 string composed of class name and the corresponding artifact name, e.g.,
71 `Artifact_full_payload`. This helps to better logging, refer to logging in
72 method Downloader.Download.
73 """
74
75 ARTIFACT_NAME = None
76
77 def __str__(cls):
78 return '%s_%s' % (cls.__name__, cls.ARTIFACT_NAME)
79
80 def __repr__(cls):
81 return str(cls)
82
83
84class Artifact(log_util.Loggable):
85 """Wrapper around an artifact to download using a fetcher.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070086
87 The purpose of this class is to download objects from Google Storage
88 and install them to a local directory. There are two main functions, one to
89 download/prepare the artifacts in to a temporary staging area and the second
90 to stage it into its final destination.
Chris Sosa76e44b92013-01-31 12:11:38 -080091
Gilad Arnold950569b2013-08-27 14:38:01 -070092 IMPORTANT! (i) `name' is a glob expression by default (and not a regex), be
93 attentive when adding new artifacts; (ii) name matching semantics differ
94 between a glob (full name string match) and a regex (partial match).
95
Chris Sosa76e44b92013-01-31 12:11:38 -080096 Class members:
Gabe Black3b567202015-09-23 14:07:59 -070097 fetcher: An object which knows how to fetch the artifact.
Gilad Arnold950569b2013-08-27 14:38:01 -070098 name: Name given for artifact; in fact, it is a pattern that captures the
99 names of files contained in the artifact. This can either be an
100 ordinary shell-style glob (the default), or a regular expression (if
101 is_regex_name is True).
102 is_regex_name: Whether the name value is a regex (default: glob).
Chris Sosa76e44b92013-01-31 12:11:38 -0800103 build: The version of the build i.e. R26-2342.0.0.
104 marker_name: Name used to define the lock marker for the artifacts to
105 prevent it from being re-downloaded. By default based on name
106 but can be overriden by children.
Dan Shi6e50c722013-08-19 15:05:06 -0700107 exception_file_path: Path to a file containing the serialized exception,
108 which was raised in Process method. The file is located
109 in the parent folder of install_dir, since the
110 install_dir will be deleted if the build does not
111 existed.
joychen0a8e34e2013-06-24 17:58:36 -0700112 install_path: Path to artifact.
Gabe Black3b567202015-09-23 14:07:59 -0700113 install_subdir: Directory within install_path where the artifact is actually
114 stored.
Chris Sosa76e44b92013-01-31 12:11:38 -0800115 install_dir: The final location where the artifact should be staged to.
116 single_name: If True the name given should only match one item. Note, if not
117 True, self.name will become a list of items returned.
Gilad Arnold1638d822013-11-07 23:38:16 -0800118 installed_files: A list of files that were the final result of downloading
119 and setting up the artifact.
120 store_installed_files: Whether the list of installed files is stored in the
121 marker file.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700122 """
Gilad Arnold950569b2013-08-27 14:38:01 -0700123
Gabe Black3b567202015-09-23 14:07:59 -0700124 __metaclass__ = ArtifactMeta
125
126 def __init__(self, name, install_dir, build, install_subdir='',
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800127 is_regex_name=False, optional_name=None):
Gilad Arnold950569b2013-08-27 14:38:01 -0700128 """Constructor.
129
130 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800131 install_dir: Where to install the artifact.
Chris Sosa76e44b92013-01-31 12:11:38 -0800132 name: Identifying name to be used to find/store the artifact.
133 build: The name of the build e.g. board/release.
Gabe Black3b567202015-09-23 14:07:59 -0700134 install_subdir: Directory within install_path where the artifact is
135 actually stored.
Gilad Arnold950569b2013-08-27 14:38:01 -0700136 is_regex_name: Whether the name pattern is a regex (default: glob).
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800137 optional_name: An alternative name to find the artifact, which can lead
138 to faster download. Unlike |name|, there is no guarantee that an
139 artifact named |optional_name| is/will be on Google Storage. If it
140 exists, we download it. Otherwise, we fall back to wait for |name|.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700141 """
Gabe Black3b567202015-09-23 14:07:59 -0700142 super(Artifact, self).__init__()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700143
Chris Sosa76e44b92013-01-31 12:11:38 -0800144 # In-memory lock to keep the devserver from colliding with itself while
145 # attempting to stage the same artifact.
146 self._process_lock = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700147
Chris Sosa76e44b92013-01-31 12:11:38 -0800148 self.name = name
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800149 self.optional_name = optional_name
Gilad Arnold950569b2013-08-27 14:38:01 -0700150 self.is_regex_name = is_regex_name
Chris Sosa76e44b92013-01-31 12:11:38 -0800151 self.build = build
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700152
Chris Sosa76e44b92013-01-31 12:11:38 -0800153 self.marker_name = '.' + self._SanitizeName(name)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700154
Dan Shi6e50c722013-08-19 15:05:06 -0700155 exception_file_name = ('.' + self._SanitizeName(build) + self.marker_name +
156 '.exception')
157 # The exception file needs to be located in parent folder, since the
158 # install_dir will be deleted is the build does not exist.
159 self.exception_file_path = os.path.join(os.path.dirname(install_dir),
Gilad Arnold950569b2013-08-27 14:38:01 -0700160 exception_file_name)
Dan Shi6e50c722013-08-19 15:05:06 -0700161
joychen0a8e34e2013-06-24 17:58:36 -0700162 self.install_path = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700163
Chris Sosa76e44b92013-01-31 12:11:38 -0800164 self.install_dir = install_dir
Gabe Black3b567202015-09-23 14:07:59 -0700165 self.install_subdir = install_subdir
Chris Sosa76e44b92013-01-31 12:11:38 -0800166
167 self.single_name = True
168
Gilad Arnold1638d822013-11-07 23:38:16 -0800169 self.installed_files = []
170 self.store_installed_files = True
171
Chris Sosa76e44b92013-01-31 12:11:38 -0800172 @staticmethod
173 def _SanitizeName(name):
174 """Sanitizes name to be used for creating a file on the filesystem.
175
176 '.','/' and '*' have special meaning in FS lingo. Replace them with words.
Gilad Arnold950569b2013-08-27 14:38:01 -0700177
178 Args:
179 name: A file name/path.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800180
Gilad Arnold950569b2013-08-27 14:38:01 -0700181 Returns:
182 The sanitized name/path.
Chris Sosa76e44b92013-01-31 12:11:38 -0800183 """
184 return name.replace('*', 'STAR').replace('.', 'DOT').replace('/', 'SLASH')
185
Dan Shif8eb0d12013-08-01 17:52:06 -0700186 def ArtifactStaged(self):
Gilad Arnold1638d822013-11-07 23:38:16 -0800187 """Returns True if artifact is already staged.
188
189 This checks for (1) presence of the artifact marker file, and (2) the
190 presence of each installed file listed in this marker. Both must hold for
191 the artifact to be considered staged. Note that this method is safe for use
192 even if the artifacts were not stageed by this instance, as it is assumed
Gabe Black3b567202015-09-23 14:07:59 -0700193 that any Artifact instance that did the staging wrote the list of
Gilad Arnold1638d822013-11-07 23:38:16 -0800194 files actually installed into the marker.
195 """
196 marker_file = os.path.join(self.install_dir, self.marker_name)
197
198 # If the marker is missing, it's definitely not staged.
199 if not os.path.exists(marker_file):
200 return False
201
202 # We want to ensure that every file listed in the marker is actually there.
203 if self.store_installed_files:
204 with open(marker_file) as f:
205 files = [line.strip() for line in f]
206
207 # Check to see if any of the purportedly installed files are missing, in
208 # which case the marker is outdated and should be removed.
209 missing_files = [fname for fname in files if not os.path.exists(fname)]
210 if missing_files:
211 self._Log('***ATTENTION*** %s files listed in %s are missing:\n%s',
212 'All' if len(files) == len(missing_files) else 'Some',
213 marker_file, '\n'.join(missing_files))
214 os.remove(marker_file)
215 return False
216
217 return True
Chris Sosa76e44b92013-01-31 12:11:38 -0800218
219 def _MarkArtifactStaged(self):
220 """Marks the artifact as staged."""
221 with open(os.path.join(self.install_dir, self.marker_name), 'w') as f:
Gilad Arnold1638d822013-11-07 23:38:16 -0800222 f.write('\n'.join(self.installed_files))
Chris Sosa76e44b92013-01-31 12:11:38 -0800223
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800224 def _UpdateName(self, names):
225 if self.single_name and len(names) > 1:
226 raise ArtifactDownloadError('Too many artifacts match %s' % self.name)
Chris Sosa76e44b92013-01-31 12:11:38 -0800227
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800228 self.name = names[0]
Chris Sosa76e44b92013-01-31 12:11:38 -0800229
joychen0a8e34e2013-06-24 17:58:36 -0700230 def _Setup(self):
Gilad Arnold1638d822013-11-07 23:38:16 -0800231 """Process the downloaded content, update the list of installed files."""
232 # In this primitive case, what was downloaded (has to be a single file) is
233 # what's installed.
234 self.installed_files = [self.install_path]
joychen0a8e34e2013-06-24 17:58:36 -0700235
Dan Shi6e50c722013-08-19 15:05:06 -0700236 def _ClearException(self):
237 """Delete any existing exception saved for this artifact."""
238 if os.path.exists(self.exception_file_path):
239 os.remove(self.exception_file_path)
240
241 def _SaveException(self, e):
242 """Save the exception to a file for downloader.IsStaged to retrieve.
243
Gilad Arnold950569b2013-08-27 14:38:01 -0700244 Args:
245 e: Exception object to be saved.
Dan Shi6e50c722013-08-19 15:05:06 -0700246 """
247 with open(self.exception_file_path, 'w') as f:
248 pickle.dump(e, f)
249
250 def GetException(self):
251 """Retrieve any exception that was raised in Process method.
252
Gilad Arnold950569b2013-08-27 14:38:01 -0700253 Returns:
254 An Exception object that was raised when trying to process the artifact.
255 Return None if no exception was found.
Dan Shi6e50c722013-08-19 15:05:06 -0700256 """
257 if not os.path.exists(self.exception_file_path):
258 return None
259 with open(self.exception_file_path, 'r') as f:
260 return pickle.load(f)
Chris Sosa76e44b92013-01-31 12:11:38 -0800261
Gabe Black3b567202015-09-23 14:07:59 -0700262 def Process(self, downloader, no_wait):
Chris Sosa76e44b92013-01-31 12:11:38 -0800263 """Main call point to all artifacts. Downloads and Stages artifact.
264
265 Downloads and Stages artifact from Google Storage to the install directory
266 specified in the constructor. It multi-thread safe and does not overwrite
267 the artifact if it's already been downloaded or being downloaded. After
268 processing, leaves behind a marker to indicate to future invocations that
269 the artifact has already been staged based on the name of the artifact.
270
271 Do not override as it modifies important private variables, ensures thread
272 safety, and maintains cache semantics.
273
274 Note: this may be a blocking call when the artifact is already in the
275 process of being staged.
276
277 Args:
Gabe Black3b567202015-09-23 14:07:59 -0700278 downloader: A downloader instance containing the logic to download
279 artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800280 no_wait: If True, don't block waiting for artifact to exist if we fail to
281 immediately find it.
282
283 Raises:
284 ArtifactDownloadError: If the artifact fails to download from Google
285 Storage for any reason or that the regexp
286 defined by name is not specific enough.
287 """
288 if not self._process_lock:
289 self._process_lock = _build_artifact_locks.lock(
290 os.path.join(self.install_dir, self.name))
291
Gabe Black3b567202015-09-23 14:07:59 -0700292 real_install_dir = os.path.join(self.install_dir, self.install_subdir)
Chris Sosa76e44b92013-01-31 12:11:38 -0800293 with self._process_lock:
Gabe Black3b567202015-09-23 14:07:59 -0700294 common_util.MkDirP(real_install_dir)
Dan Shif8eb0d12013-08-01 17:52:06 -0700295 if not self.ArtifactStaged():
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800296 # Delete any existing exception saved for this artifact.
297 self._ClearException()
298 found_artifact = False
299 if self.optional_name:
300 try:
Gabe Black3b567202015-09-23 14:07:59 -0700301 # Check if the artifact named |optional_name| exists.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800302 # Because this artifact may not always exist, don't bother
303 # to wait for it (set timeout=1).
Gabe Black3b567202015-09-23 14:07:59 -0700304 new_names = downloader.Wait(
305 self.optional_name, self.is_regex_name, timeout=1)
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800306 self._UpdateName(new_names)
307
308 except ArtifactDownloadError:
309 self._Log('Unable to download %s; fall back to download %s',
310 self.optional_name, self.name)
311 else:
312 found_artifact = True
313
Dan Shi6e50c722013-08-19 15:05:06 -0700314 try:
Dan Shi6e50c722013-08-19 15:05:06 -0700315 # If the artifact should already have been uploaded, don't waste
316 # cycles waiting around for it to exist.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800317 if not found_artifact:
318 timeout = 1 if no_wait else 10
Gabe Black3b567202015-09-23 14:07:59 -0700319 new_names = downloader.Wait(
320 self.name, self.is_regex_name, timeout)
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800321 self._UpdateName(new_names)
322
323 self._Log('Downloading file %s', self.name)
Gabe Black3b567202015-09-23 14:07:59 -0700324 self.install_path = downloader.Fetch(self.name, real_install_dir)
Dan Shi6e50c722013-08-19 15:05:06 -0700325 self._Setup()
326 self._MarkArtifactStaged()
327 except Exception as e:
328 # Save the exception to a file for downloader.IsStaged to retrieve.
329 self._SaveException(e)
Gilad Arnold02dc6552013-11-14 11:27:54 -0800330
331 # Convert an unknown exception into an ArtifactDownloadError.
332 if type(e) is ArtifactDownloadError:
333 raise
334 else:
335 raise ArtifactDownloadError('An error occurred: %s' % e)
Chris Sosa76e44b92013-01-31 12:11:38 -0800336 else:
337 self._Log('%s is already staged.', self)
338
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700339 def __str__(self):
340 """String representation for the download."""
Gabe Black3b567202015-09-23 14:07:59 -0700341 return '%s->%s' % (self.name, self.install_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700342
Chris Sosab26b1202013-08-16 16:40:55 -0700343 def __repr__(self):
344 return str(self)
345
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700346
Gabe Black3b567202015-09-23 14:07:59 -0700347class AUTestPayload(Artifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700348 """Wrapper for AUTest delta payloads which need additional setup."""
Gilad Arnold950569b2013-08-27 14:38:01 -0700349
joychen0a8e34e2013-06-24 17:58:36 -0700350 def _Setup(self):
Gabe Black3b567202015-09-23 14:07:59 -0700351 super(AUTestPayload, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700352
Chris Sosa76e44b92013-01-31 12:11:38 -0800353 # Rename to update.gz.
Gabe Black3b567202015-09-23 14:07:59 -0700354 install_path = os.path.join(self.install_dir, self.install_subdir,
355 self.name)
356 new_install_path = os.path.join(self.install_dir, self.install_subdir,
joychen7c2054a2013-07-25 11:14:07 -0700357 devserver_constants.UPDATE_FILE)
Chris Sosa76e44b92013-01-31 12:11:38 -0800358 shutil.move(install_path, new_install_path)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700359
Gilad Arnold1638d822013-11-07 23:38:16 -0800360 # Reflect the rename in the list of installed files.
361 self.installed_files.remove(install_path)
362 self.installed_files = [new_install_path]
363
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700364
Gabe Black3b567202015-09-23 14:07:59 -0700365class DeltaPayloadBase(AUTestPayload):
Chris Sosa76e44b92013-01-31 12:11:38 -0800366 """Delta payloads from the archive_url.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700367
Gabe Black3b567202015-09-23 14:07:59 -0700368 These artifacts are super strange. They custom handle directories and
369 pull in all delta payloads. We can't specify exactly what we want
Chris Sosa76e44b92013-01-31 12:11:38 -0800370 because unlike other artifacts, this one does not conform to something a
371 client might know. The client doesn't know the version of n-1 or whether it
372 was even generated.
Gilad Arnold950569b2013-08-27 14:38:01 -0700373
374 IMPORTANT! Note that this artifact simply ignores the `name' argument because
Gabe Black3b567202015-09-23 14:07:59 -0700375 that name is derived internally.
Chris Sosa76e44b92013-01-31 12:11:38 -0800376 """
Gilad Arnold950569b2013-08-27 14:38:01 -0700377
joychen0a8e34e2013-06-24 17:58:36 -0700378 def _Setup(self):
Gabe Black3b567202015-09-23 14:07:59 -0700379 super(DeltaPayloadBase, self)._Setup()
380 # Setup symlink so that AU will work for this payload.
381 stateful_update_symlink = os.path.join(
382 self.install_dir, self.install_subdir,
383 devserver_constants.STATEFUL_FILE)
384 os.symlink(os.path.join(os.pardir, os.pardir,
385 devserver_constants.STATEFUL_FILE),
386 stateful_update_symlink)
387 self.installed_files.append(stateful_update_symlink)
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700388
Chris Sosa76e44b92013-01-31 12:11:38 -0800389
Gabe Black3b567202015-09-23 14:07:59 -0700390class BundledArtifact(Artifact):
Chris Sosa76e44b92013-01-31 12:11:38 -0800391 """A single build artifact bundle e.g. zip file or tar file."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800392
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800393 def __init__(self, *args, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700394 """Takes Artifact args with some additional ones.
Gilad Arnold950569b2013-08-27 14:38:01 -0700395
396 Args:
Gabe Black3b567202015-09-23 14:07:59 -0700397 *args: See Artifact documentation.
398 **kwargs: See Artifact documentation.
Gilad Arnold950569b2013-08-27 14:38:01 -0700399 files_to_extract: A list of files to extract. If set to None, extract
400 all files.
401 exclude: A list of files to exclude. If None, no files are excluded.
Chris Sosa76e44b92013-01-31 12:11:38 -0800402 """
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800403 self._files_to_extract = kwargs.pop('files_to_extract', None)
404 self._exclude = kwargs.pop('exclude', None)
Gabe Black3b567202015-09-23 14:07:59 -0700405 super(BundledArtifact, self).__init__(*args, **kwargs)
Chris Sosa76e44b92013-01-31 12:11:38 -0800406
407 # We modify the marker so that it is unique to what was staged.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800408 if self._files_to_extract:
Chris Sosa76e44b92013-01-31 12:11:38 -0800409 self.marker_name = self._SanitizeName(
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800410 '_'.join(['.' + self.name] + self._files_to_extract))
Chris Sosa76e44b92013-01-31 12:11:38 -0800411
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800412 def _RunUnzip(self, list_only):
413 # Unzip is weird. It expects its args before any excludes and expects its
414 # excludes in a list following the -x.
415 cmd = ['unzip', '-qql' if list_only else '-o', self.install_path]
416 if not list_only:
417 cmd += ['-d', self.install_dir]
Chris Sosa76e44b92013-01-31 12:11:38 -0800418
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800419 if self._files_to_extract:
420 cmd.extend(self._files_to_extract)
421
422 if self._exclude:
423 cmd.append('-x')
424 cmd.extend(self._exclude)
425
426 try:
427 return subprocess.check_output(cmd).strip('\n').splitlines()
428 except subprocess.CalledProcessError, e:
429 raise ArtifactDownloadError(
430 'An error occurred when attempting to unzip %s:\n%s' %
431 (self.install_path, e))
Chris Sosa76e44b92013-01-31 12:11:38 -0800432
joychen0a8e34e2013-06-24 17:58:36 -0700433 def _Setup(self):
Gilad Arnold1638d822013-11-07 23:38:16 -0800434 extract_result = self._Extract()
435 if self.store_installed_files:
436 # List both the archive and the extracted files.
437 self.installed_files.append(self.install_path)
438 self.installed_files.extend(extract_result)
Chris Sosa76e44b92013-01-31 12:11:38 -0800439
Chris Sosa76e44b92013-01-31 12:11:38 -0800440 def _Extract(self):
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800441 """Extracts files into the install path."""
442 if self.name.endswith('.zip'):
443 return self._ExtractZipfile()
444 else:
445 return self._ExtractTarball()
446
447 def _ExtractZipfile(self):
448 """Extracts a zip file using unzip."""
449 file_list = [os.path.join(self.install_dir, line[30:].strip())
450 for line in self._RunUnzip(True)
451 if not line.endswith('/')]
452 if file_list:
453 self._RunUnzip(False)
454
455 return file_list
456
457 def _ExtractTarball(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800458 """Extracts a tarball using tar.
459
460 Detects whether the tarball is compressed or not based on the file
461 extension and extracts the tarball into the install_path.
462 """
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700463 try:
Gilad Arnold1638d822013-11-07 23:38:16 -0800464 return common_util.ExtractTarball(self.install_path, self.install_dir,
465 files_to_extract=self._files_to_extract,
466 excluded_files=self._exclude,
467 return_extracted_files=True)
Simran Basi4baad082013-02-14 13:39:18 -0800468 except common_util.CommonUtilError as e:
469 raise ArtifactDownloadError(str(e))
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700470
471
Gabe Black3b567202015-09-23 14:07:59 -0700472class AutotestTarball(BundledArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700473 """Wrapper around the autotest tarball to download from gsutil."""
474
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800475 def __init__(self, *args, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700476 super(AutotestTarball, self).__init__(*args, **kwargs)
Gilad Arnold1638d822013-11-07 23:38:16 -0800477 # We don't store/check explicit file lists in Autotest tarball markers;
478 # this can get huge and unwieldy, and generally make little sense.
479 self.store_installed_files = False
480
joychen0a8e34e2013-06-24 17:58:36 -0700481 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800482 """Extracts the tarball into the install path excluding test suites."""
Gabe Black3b567202015-09-23 14:07:59 -0700483 super(AutotestTarball, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700484
Chris Sosa76e44b92013-01-31 12:11:38 -0800485 # Deal with older autotest packages that may not be bundled.
joychen3cb228e2013-06-12 12:13:13 -0700486 autotest_dir = os.path.join(self.install_dir,
487 devserver_constants.AUTOTEST_DIR)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700488 autotest_pkgs_dir = os.path.join(autotest_dir, 'packages')
489 if not os.path.exists(autotest_pkgs_dir):
490 os.makedirs(autotest_pkgs_dir)
491
492 if not os.path.exists(os.path.join(autotest_pkgs_dir, 'packages.checksum')):
Chris Sosa76e44b92013-01-31 12:11:38 -0800493 cmd = ['autotest/utils/packager.py', 'upload', '--repository',
494 autotest_pkgs_dir, '--all']
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700495 try:
joychen0a8e34e2013-06-24 17:58:36 -0700496 subprocess.check_call(cmd, cwd=self.install_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700497 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800498 raise ArtifactDownloadError(
499 'Failed to create autotest packages!:\n%s' % e)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700500 else:
Gilad Arnoldf5843132012-09-25 00:31:20 -0700501 self._Log('Using pre-generated packages from autotest')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700502
Chris Masone816e38c2012-05-02 12:22:36 -0700503
Gabe Black3b567202015-09-23 14:07:59 -0700504def _CreateNewArtifact(tag, base, name, *fixed_args, **fixed_kwargs):
505 """Get a data wrapper that describes an artifact's implementation.
Gilad Arnold950569b2013-08-27 14:38:01 -0700506
Gabe Black3b567202015-09-23 14:07:59 -0700507 Args:
508 tag: Tag of the artifact, defined in artifact_info.
509 base: Class of the artifact, e.g., BundledArtifact.
510 name: Name of the artifact, e.g., image.zip.
511 *fixed_args: Fixed arguments that are additional to the one used in base
512 class.
513 **fixed_kwargs: Fixed keyword arguments that are additional to the one used
514 in base class.
Chris Sosa76e44b92013-01-31 12:11:38 -0800515
Gabe Black3b567202015-09-23 14:07:59 -0700516 Returns:
517 A data wrapper that describes an artifact's implementation.
Chris Sosa76e44b92013-01-31 12:11:38 -0800518
Gabe Black3b567202015-09-23 14:07:59 -0700519 """
520 class NewArtifact(base):
521 """A data wrapper that describes an artifact's implementation."""
522 ARTIFACT_TAG = tag
523 ARTIFACT_NAME = name
524
525 def __init__(self, *args, **kwargs):
526 all_args = fixed_args + args
527 all_kwargs = {}
528 all_kwargs.update(fixed_kwargs)
529 all_kwargs.update(kwargs)
530 super(NewArtifact, self).__init__(self.ARTIFACT_NAME,
531 *all_args, **all_kwargs)
532
533 NewArtifact.__name__ = base.__name__
534 return NewArtifact
Chris Sosa968a1062013-08-02 17:42:50 -0700535
Chris Sosa76e44b92013-01-31 12:11:38 -0800536
Gabe Black3b567202015-09-23 14:07:59 -0700537# TODO(dshi): Refactor the code here to split out the logic of creating the
538# artifacts mapping to a different module.
539chromeos_artifact_map = {}
Chris Sosa76e44b92013-01-31 12:11:38 -0800540
Chris Sosa76e44b92013-01-31 12:11:38 -0800541
Gabe Black3b567202015-09-23 14:07:59 -0700542def _AddCrOSArtifact(tag, base, name, *fixed_args, **fixed_kwargs):
543 """Add a data wrapper that describes a ChromeOS artifact's implementation to
544 chromeos_artifact_map.
545 """
546 artifact = _CreateNewArtifact(tag, base, name, *fixed_args, **fixed_kwargs)
547 chromeos_artifact_map.setdefault(tag, []).append(artifact)
Chris Sosa76e44b92013-01-31 12:11:38 -0800548
beepsc3d0f872013-07-31 21:50:40 -0700549
Gabe Black3b567202015-09-23 14:07:59 -0700550_AddCrOSArtifact(artifact_info.FULL_PAYLOAD, AUTestPayload, '*_full_*')
551
552
553class DeltaPayloadNtoN(DeltaPayloadBase):
554 """ChromeOS Delta payload artifact for updating from version N to N."""
555 ARTIFACT_TAG = artifact_info.DELTA_PAYLOADS
556 ARTIFACT_NAME = 'NOT_APPLICABLE'
557
558 def __init__(self, install_dir, build, *args, **kwargs):
559 name = 'chromeos_%s*_delta_*' % build
560 install_subdir = os.path.join(_AU_BASE, build + _NTON_DIR_SUFFIX)
561 super(DeltaPayloadNtoN, self).__init__(name, install_dir, build, *args,
562 install_subdir=install_subdir,
563 **kwargs)
564
565
566class DeltaPayloadMtoN(DeltaPayloadBase):
567 """ChromeOS Delta payload artifact for updating from version M to N."""
568 ARTIFACT_TAG = artifact_info.DELTA_PAYLOADS
569 ARTIFACT_NAME = 'NOT_APPLICABLE'
570
571 def __init__(self, install_dir, build, *args, **kwargs):
572 name = ('chromeos_(?!%s).*_delta_.*' % re.escape(build))
573 install_subdir = os.path.join(_AU_BASE, build + _MTON_DIR_SUFFIX)
574 super(DeltaPayloadMtoN, self).__init__(name, install_dir, build, *args,
575 install_subdir=install_subdir,
576 is_regex_name=True, **kwargs)
577
578
579chromeos_artifact_map[artifact_info.DELTA_PAYLOADS] = [DeltaPayloadNtoN,
580 DeltaPayloadMtoN]
581
582
583_AddCrOSArtifact(artifact_info.STATEFUL_PAYLOAD, Artifact,
584 devserver_constants.STATEFUL_FILE)
585_AddCrOSArtifact(artifact_info.BASE_IMAGE, BundledArtifact, IMAGE_FILE,
586 optional_name=BASE_IMAGE_FILE,
587 files_to_extract=[devserver_constants.BASE_IMAGE_FILE])
588_AddCrOSArtifact(artifact_info.RECOVERY_IMAGE, BundledArtifact, IMAGE_FILE,
589 optional_name=RECOVERY_IMAGE_FILE,
590 files_to_extract=[devserver_constants.RECOVERY_IMAGE_FILE])
591_AddCrOSArtifact(artifact_info.DEV_IMAGE, BundledArtifact, IMAGE_FILE,
592 files_to_extract=[devserver_constants.IMAGE_FILE])
593_AddCrOSArtifact(artifact_info.TEST_IMAGE, BundledArtifact, IMAGE_FILE,
594 optional_name=TEST_IMAGE_FILE,
595 files_to_extract=[devserver_constants.TEST_IMAGE_FILE])
596_AddCrOSArtifact(artifact_info.AUTOTEST, AutotestTarball, AUTOTEST_FILE,
597 files_to_extract=None, exclude=['autotest/test_suites'])
598_AddCrOSArtifact(artifact_info.CONTROL_FILES, BundledArtifact,
599 CONTROL_FILES_FILE)
600_AddCrOSArtifact(artifact_info.AUTOTEST_PACKAGES, AutotestTarball,
601 AUTOTEST_PACKAGES_FILE)
602_AddCrOSArtifact(artifact_info.TEST_SUITES, BundledArtifact, TEST_SUITES_FILE)
603_AddCrOSArtifact(artifact_info.AU_SUITE, BundledArtifact, AU_SUITE_FILE)
604_AddCrOSArtifact(artifact_info.AUTOTEST_SERVER_PACKAGE, Artifact,
605 AUTOTEST_SERVER_PACKAGE_FILE)
606_AddCrOSArtifact(artifact_info.FIRMWARE, Artifact, FIRMWARE_FILE)
607_AddCrOSArtifact(artifact_info.SYMBOLS, BundledArtifact, DEBUG_SYMBOLS_FILE,
608 files_to_extract=['debug/breakpad'])
609_AddCrOSArtifact(artifact_info.FACTORY_IMAGE, BundledArtifact, FACTORY_FILE,
610 files_to_extract=[devserver_constants.FACTORY_IMAGE_FILE])
Chris Sosa76e44b92013-01-31 12:11:38 -0800611
Chris Sosa968a1062013-08-02 17:42:50 -0700612# Add all the paygen_au artifacts in one go.
Gabe Black3b567202015-09-23 14:07:59 -0700613for c in devserver_constants.CHANNELS:
614 _AddCrOSArtifact(artifact_info.PAYGEN_AU_SUITE_TEMPLATE % {'channel': c},
615 BundledArtifact,
616 PAYGEN_AU_SUITE_FILE_TEMPLATE % {'channel': c})
617
618android_artifact_map = {}
Chris Sosa968a1062013-08-02 17:42:50 -0700619
Chris Sosa76e44b92013-01-31 12:11:38 -0800620
Gabe Black3b567202015-09-23 14:07:59 -0700621def _AddAndroidArtifact(tag, base, name, *fixed_args, **fixed_kwargs):
622 """Add a data wrapper that describes an Android artifact's implementation to
623 android_artifact_map.
624 """
625 artifact = _CreateNewArtifact(tag, base, name, *fixed_args, **fixed_kwargs)
626 android_artifact_map.setdefault(tag, []).append(artifact)
627
628
Dan Shiba4e00f2015-10-27 12:03:53 -0700629_AddAndroidArtifact(artifact_info.ANDROID_ZIP_IMAGES, Artifact,
Gabe Black3b567202015-09-23 14:07:59 -0700630 ANDROID_IMAGE_ZIP, is_regex_name=True)
631_AddAndroidArtifact(artifact_info.ANDROID_RADIO_IMAGE, Artifact,
632 ANDROID_RADIO_IMAGE)
633_AddAndroidArtifact(artifact_info.ANDROID_BOOTLOADER_IMAGE, Artifact,
634 ANDROID_BOOTLOADER_IMAGE)
635_AddAndroidArtifact(artifact_info.ANDROID_FASTBOOT, Artifact, ANDROID_FASTBOOT)
636_AddAndroidArtifact(artifact_info.ANDROID_TEST_ZIP, BundledArtifact,
637 ANDROID_TEST_ZIP, is_regex_name=True)
Dan Shi74136ae2015-12-01 14:40:06 -0800638_AddAndroidArtifact(artifact_info.ANDROID_VENDOR_PARTITION_ZIP, Artifact,
639 ANDROID_VENDOR_PARTITION_ZIP, is_regex_name=True)
Dan Shi6c2b2a22016-03-04 15:52:19 -0800640_AddAndroidArtifact(artifact_info.AUTOTEST_SERVER_PACKAGE, Artifact,
641 ANDROID_AUTOTEST_SERVER_PACKAGE, is_regex_name=True)
642_AddAndroidArtifact(artifact_info.TEST_SUITES, BundledArtifact,
643 ANDROID_TEST_SUITES, is_regex_name=True)
644_AddAndroidArtifact(artifact_info.CONTROL_FILES, BundledArtifact,
645 ANDROID_CONTROL_FILES, is_regex_name=True)
646
Gabe Black3b567202015-09-23 14:07:59 -0700647
648class BaseArtifactFactory(object):
Chris Sosa76e44b92013-01-31 12:11:38 -0800649 """A factory class that generates build artifacts from artifact names."""
650
Dan Shi6c2b2a22016-03-04 15:52:19 -0800651 def __init__(self, artifact_map, download_dir, artifacts, files, build,
652 requested_to_optional_map):
Chris Sosa76e44b92013-01-31 12:11:38 -0800653 """Initalizes the member variables for the factory.
654
655 Args:
Gabe Black3b567202015-09-23 14:07:59 -0700656 artifact_map: A map from artifact names to ImplDescription objects.
Gilad Arnold950569b2013-08-27 14:38:01 -0700657 download_dir: A directory to which artifacts are downloaded.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700658 artifacts: List of artifacts to stage. These artifacts must be
659 defined in artifact_info.py and have a mapping in the
660 ARTIFACT_IMPLEMENTATION_MAP.
661 files: List of files to stage. These files are just downloaded and staged
662 as files into the download_dir.
Chris Sosa76e44b92013-01-31 12:11:38 -0800663 build: The name of the build.
Dan Shi6c2b2a22016-03-04 15:52:19 -0800664 requested_to_optional_map: A map between an artifact X to a list of
665 artifacts Y. If X is requested, all items in Y should also get
666 triggered for download.
Chris Sosa76e44b92013-01-31 12:11:38 -0800667 """
Gabe Black3b567202015-09-23 14:07:59 -0700668 self.artifact_map = artifact_map
joychen0a8e34e2013-06-24 17:58:36 -0700669 self.download_dir = download_dir
Chris Sosa6b0c6172013-08-05 17:01:33 -0700670 self.artifacts = artifacts
671 self.files = files
Chris Sosa76e44b92013-01-31 12:11:38 -0800672 self.build = build
Dan Shi6c2b2a22016-03-04 15:52:19 -0800673 self.requested_to_optional_map = requested_to_optional_map
Chris Sosa76e44b92013-01-31 12:11:38 -0800674
Chris Sosa6b0c6172013-08-05 17:01:33 -0700675 def _Artifacts(self, names, is_artifact):
Gabe Black3b567202015-09-23 14:07:59 -0700676 """Returns the Artifacts from |names|.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700677
678 If is_artifact is true, then these names define artifacts that must exist in
679 the ARTIFACT_IMPLEMENTATION_MAP. Otherwise, treat as filenames to stage as
680 basic BuildArtifacts.
681
Gilad Arnold950569b2013-08-27 14:38:01 -0700682 Args:
683 names: A sequence of artifact names.
684 is_artifact: Whether this is a named (True) or file (False) artifact.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800685
Gilad Arnold950569b2013-08-27 14:38:01 -0700686 Returns:
Gabe Black3b567202015-09-23 14:07:59 -0700687 An iterable of Artifacts.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800688
Gilad Arnold950569b2013-08-27 14:38:01 -0700689 Raises:
Gabe Black3b567202015-09-23 14:07:59 -0700690 KeyError: if artifact doesn't exist in the artifact map.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700691 """
Gabe Black3b567202015-09-23 14:07:59 -0700692 if is_artifact:
693 classes = itertools.chain(*(self.artifact_map[name] for name in names))
694 return list(cls(self.download_dir, self.build) for cls in classes)
695 else:
696 return list(Artifact(name, self.download_dir, self.build)
697 for name in names)
Chris Sosa76e44b92013-01-31 12:11:38 -0800698
699 def RequiredArtifacts(self):
Gilad Arnold950569b2013-08-27 14:38:01 -0700700 """Returns BuildArtifacts for the factory's artifacts.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700701
Gilad Arnold950569b2013-08-27 14:38:01 -0700702 Returns:
703 An iterable of BuildArtifacts.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800704
Gilad Arnold950569b2013-08-27 14:38:01 -0700705 Raises:
706 KeyError: if artifact doesn't exist in ARTIFACT_IMPLEMENTATION_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700707 """
708 artifacts = []
709 if self.artifacts:
710 artifacts.extend(self._Artifacts(self.artifacts, True))
711 if self.files:
712 artifacts.extend(self._Artifacts(self.files, False))
713
714 return artifacts
Chris Sosa76e44b92013-01-31 12:11:38 -0800715
716 def OptionalArtifacts(self):
Gilad Arnold950569b2013-08-27 14:38:01 -0700717 """Returns BuildArtifacts that should be cached.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700718
Gilad Arnold950569b2013-08-27 14:38:01 -0700719 Returns:
720 An iterable of BuildArtifacts.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800721
Gilad Arnold950569b2013-08-27 14:38:01 -0700722 Raises:
723 KeyError: if an optional artifact doesn't exist in
724 ARTIFACT_IMPLEMENTATION_MAP yet defined in
725 artifact_info.REQUESTED_TO_OPTIONAL_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700726 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800727 optional_names = set()
Dan Shi6c2b2a22016-03-04 15:52:19 -0800728 for artifact_name, optional_list in self.requested_to_optional_map.items():
Chris Sosa76e44b92013-01-31 12:11:38 -0800729 # We are already downloading it.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700730 if artifact_name in self.artifacts:
Chris Sosa76e44b92013-01-31 12:11:38 -0800731 optional_names = optional_names.union(optional_list)
732
Chris Sosa6b0c6172013-08-05 17:01:33 -0700733 return self._Artifacts(optional_names - set(self.artifacts), True)
Chris Sosa968a1062013-08-02 17:42:50 -0700734
735
Gabe Black3b567202015-09-23 14:07:59 -0700736class ChromeOSArtifactFactory(BaseArtifactFactory):
737 """A factory class that generates ChromeOS build artifacts from names."""
738
739 def __init__(self, download_dir, artifacts, files, build):
740 """Pass the ChromeOS artifact map to the base class."""
741 super(ChromeOSArtifactFactory, self).__init__(
Dan Shi6c2b2a22016-03-04 15:52:19 -0800742 chromeos_artifact_map, download_dir, artifacts, files, build,
743 artifact_info.CROS_REQUESTED_TO_OPTIONAL_MAP)
Gabe Black3b567202015-09-23 14:07:59 -0700744
745
746class AndroidArtifactFactory(BaseArtifactFactory):
747 """A factory class that generates Android build artifacts from names."""
748
749 def __init__(self, download_dir, artifacts, files, build):
750 """Pass the Android artifact map to the base class."""
751 super(AndroidArtifactFactory, self).__init__(
Dan Shi6c2b2a22016-03-04 15:52:19 -0800752 android_artifact_map, download_dir, artifacts, files, build,
753 artifact_info.ANDROID_REQUESTED_TO_OPTIONAL_MAP)
Gabe Black3b567202015-09-23 14:07:59 -0700754
755
Chris Sosa968a1062013-08-02 17:42:50 -0700756# A simple main to verify correctness of the artifact map when making simple
757# name changes.
758if __name__ == '__main__':
Gabe Black3b567202015-09-23 14:07:59 -0700759 print('ARTIFACT IMPLEMENTATION MAPs (for debugging)')
760 print('FORMAT: ARTIFACT -> IMPLEMENTATION (<type>_file)')
761 for label, mapping in (('CHROMEOS', chromeos_artifact_map),
762 ('ANDROID', android_artifact_map)):
763 print('%s:' % label)
764 for key, value in sorted(mapping.items()):
765 print(' %s -> %s' % (key, ', '.join(str(val) for val in value)))