blob: c80fe7dbb496edc496289d9312a487016f4c0366 [file] [log] [blame]
Gilad Arnold950569b2013-08-27 14:38:01 -07001#!/usr/bin/python
Chris Sosa968a1062013-08-02 17:42:50 -07002
Chris Sosa47a7d4e2012-03-28 11:26:55 -07003# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Module containing classes that wrap artifact downloads."""
8
Simran Basi4243a862014-12-12 12:48:33 -08009import glob
Chris Sosa47a7d4e2012-03-28 11:26:55 -070010import os
Dan Shi6e50c722013-08-19 15:05:06 -070011import pickle
Gilad Arnold950569b2013-08-27 14:38:01 -070012import re
Chris Sosa47a7d4e2012-03-28 11:26:55 -070013import shutil
14import subprocess
15
Chris Sosa76e44b92013-01-31 12:11:38 -080016import artifact_info
17import common_util
joychen3cb228e2013-06-12 12:13:13 -070018import devserver_constants
Chris Sosa47a7d4e2012-03-28 11:26:55 -070019import gsutil_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070020import log_util
Chris Sosa47a7d4e2012-03-28 11:26:55 -070021
22
Chris Sosa76e44b92013-01-31 12:11:38 -080023_AU_BASE = 'au'
24_NTON_DIR_SUFFIX = '_nton'
25_MTON_DIR_SUFFIX = '_mton'
26
27############ Actual filenames of artifacts in Google Storage ############
28
29AU_SUITE_FILE = 'au_control.tar.bz2'
Chris Sosa968a1062013-08-02 17:42:50 -070030PAYGEN_AU_SUITE_FILE_TEMPLATE = 'paygen_au_%(channel)s_control.tar.bz2'
Chris Sosa76e44b92013-01-31 12:11:38 -080031AUTOTEST_FILE = 'autotest.tar'
Simran Basiea0590d2014-10-29 11:31:26 -070032CONTROL_FILES_FILE = 'control_files.tar'
33AUTOTEST_PACKAGES_FILE = 'autotest_packages.tar'
Chris Sosa76e44b92013-01-31 12:11:38 -080034AUTOTEST_COMPRESSED_FILE = 'autotest.tar.bz2'
35DEBUG_SYMBOLS_FILE = 'debug.tgz'
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -080036FACTORY_FILE = 'ChromeOS-factory*.zip'
Chris Sosa76e44b92013-01-31 12:11:38 -080037FIRMWARE_FILE = 'firmware_from_source.tar.bz2'
38IMAGE_FILE = 'image.zip'
Chris Sosa76e44b92013-01-31 12:11:38 -080039TEST_SUITES_FILE = 'test_suites.tar.bz2'
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -080040BASE_IMAGE_FILE = 'chromiumos_base_image.tar.xz'
41TEST_IMAGE_FILE = 'chromiumos_test_image.tar.xz'
42RECOVERY_IMAGE_FILE = 'recovery_image.tar.xz'
43
Chris Sosa76e44b92013-01-31 12:11:38 -080044
45_build_artifact_locks = common_util.LockDict()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070046
47
48class ArtifactDownloadError(Exception):
49 """Error used to signify an issue processing an artifact."""
50 pass
51
52
Gilad Arnoldc65330c2012-09-20 15:17:48 -070053class BuildArtifact(log_util.Loggable):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070054 """Wrapper around an artifact to download from gsutil.
55
56 The purpose of this class is to download objects from Google Storage
57 and install them to a local directory. There are two main functions, one to
58 download/prepare the artifacts in to a temporary staging area and the second
59 to stage it into its final destination.
Chris Sosa76e44b92013-01-31 12:11:38 -080060
Gilad Arnold950569b2013-08-27 14:38:01 -070061 IMPORTANT! (i) `name' is a glob expression by default (and not a regex), be
62 attentive when adding new artifacts; (ii) name matching semantics differ
63 between a glob (full name string match) and a regex (partial match).
64
Chris Sosa76e44b92013-01-31 12:11:38 -080065 Class members:
Gilad Arnold950569b2013-08-27 14:38:01 -070066 archive_url: An archive URL.
67 name: Name given for artifact; in fact, it is a pattern that captures the
68 names of files contained in the artifact. This can either be an
69 ordinary shell-style glob (the default), or a regular expression (if
70 is_regex_name is True).
71 is_regex_name: Whether the name value is a regex (default: glob).
Chris Sosa76e44b92013-01-31 12:11:38 -080072 build: The version of the build i.e. R26-2342.0.0.
73 marker_name: Name used to define the lock marker for the artifacts to
74 prevent it from being re-downloaded. By default based on name
75 but can be overriden by children.
Dan Shi6e50c722013-08-19 15:05:06 -070076 exception_file_path: Path to a file containing the serialized exception,
77 which was raised in Process method. The file is located
78 in the parent folder of install_dir, since the
79 install_dir will be deleted if the build does not
80 existed.
joychen0a8e34e2013-06-24 17:58:36 -070081 install_path: Path to artifact.
Chris Sosa76e44b92013-01-31 12:11:38 -080082 install_dir: The final location where the artifact should be staged to.
83 single_name: If True the name given should only match one item. Note, if not
84 True, self.name will become a list of items returned.
Gilad Arnold1638d822013-11-07 23:38:16 -080085 installed_files: A list of files that were the final result of downloading
86 and setting up the artifact.
87 store_installed_files: Whether the list of installed files is stored in the
88 marker file.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070089 """
Gilad Arnold950569b2013-08-27 14:38:01 -070090
91 def __init__(self, install_dir, archive_url, name, build,
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -080092 is_regex_name=False, optional_name=None):
Gilad Arnold950569b2013-08-27 14:38:01 -070093 """Constructor.
94
95 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -080096 install_dir: Where to install the artifact.
Simran Basi4243a862014-12-12 12:48:33 -080097 archive_url: The Google Storage URL or local path to find the artifact.
Chris Sosa76e44b92013-01-31 12:11:38 -080098 name: Identifying name to be used to find/store the artifact.
99 build: The name of the build e.g. board/release.
Gilad Arnold950569b2013-08-27 14:38:01 -0700100 is_regex_name: Whether the name pattern is a regex (default: glob).
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800101 optional_name: An alternative name to find the artifact, which can lead
102 to faster download. Unlike |name|, there is no guarantee that an
103 artifact named |optional_name| is/will be on Google Storage. If it
104 exists, we download it. Otherwise, we fall back to wait for |name|.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700105 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800106 super(BuildArtifact, self).__init__()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700107
Chris Sosa76e44b92013-01-31 12:11:38 -0800108 # In-memory lock to keep the devserver from colliding with itself while
109 # attempting to stage the same artifact.
110 self._process_lock = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700111
Chris Sosa76e44b92013-01-31 12:11:38 -0800112 self.archive_url = archive_url
113 self.name = name
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800114 self.optional_name = optional_name
Gilad Arnold950569b2013-08-27 14:38:01 -0700115 self.is_regex_name = is_regex_name
Chris Sosa76e44b92013-01-31 12:11:38 -0800116 self.build = build
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700117
Chris Sosa76e44b92013-01-31 12:11:38 -0800118 self.marker_name = '.' + self._SanitizeName(name)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700119
Dan Shi6e50c722013-08-19 15:05:06 -0700120 exception_file_name = ('.' + self._SanitizeName(build) + self.marker_name +
121 '.exception')
122 # The exception file needs to be located in parent folder, since the
123 # install_dir will be deleted is the build does not exist.
124 self.exception_file_path = os.path.join(os.path.dirname(install_dir),
Gilad Arnold950569b2013-08-27 14:38:01 -0700125 exception_file_name)
Dan Shi6e50c722013-08-19 15:05:06 -0700126
joychen0a8e34e2013-06-24 17:58:36 -0700127 self.install_path = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700128
Chris Sosa76e44b92013-01-31 12:11:38 -0800129 self.install_dir = install_dir
130
131 self.single_name = True
132
Gilad Arnold1638d822013-11-07 23:38:16 -0800133 self.installed_files = []
134 self.store_installed_files = True
135
Chris Sosa76e44b92013-01-31 12:11:38 -0800136 @staticmethod
137 def _SanitizeName(name):
138 """Sanitizes name to be used for creating a file on the filesystem.
139
140 '.','/' and '*' have special meaning in FS lingo. Replace them with words.
Gilad Arnold950569b2013-08-27 14:38:01 -0700141
142 Args:
143 name: A file name/path.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800144
Gilad Arnold950569b2013-08-27 14:38:01 -0700145 Returns:
146 The sanitized name/path.
Chris Sosa76e44b92013-01-31 12:11:38 -0800147 """
148 return name.replace('*', 'STAR').replace('.', 'DOT').replace('/', 'SLASH')
149
Dan Shif8eb0d12013-08-01 17:52:06 -0700150 def ArtifactStaged(self):
Gilad Arnold1638d822013-11-07 23:38:16 -0800151 """Returns True if artifact is already staged.
152
153 This checks for (1) presence of the artifact marker file, and (2) the
154 presence of each installed file listed in this marker. Both must hold for
155 the artifact to be considered staged. Note that this method is safe for use
156 even if the artifacts were not stageed by this instance, as it is assumed
157 that any BuildArtifact instance that did the staging wrote the list of
158 files actually installed into the marker.
159 """
160 marker_file = os.path.join(self.install_dir, self.marker_name)
161
162 # If the marker is missing, it's definitely not staged.
163 if not os.path.exists(marker_file):
164 return False
165
166 # We want to ensure that every file listed in the marker is actually there.
167 if self.store_installed_files:
168 with open(marker_file) as f:
169 files = [line.strip() for line in f]
170
171 # Check to see if any of the purportedly installed files are missing, in
172 # which case the marker is outdated and should be removed.
173 missing_files = [fname for fname in files if not os.path.exists(fname)]
174 if missing_files:
175 self._Log('***ATTENTION*** %s files listed in %s are missing:\n%s',
176 'All' if len(files) == len(missing_files) else 'Some',
177 marker_file, '\n'.join(missing_files))
178 os.remove(marker_file)
179 return False
180
181 return True
Chris Sosa76e44b92013-01-31 12:11:38 -0800182
183 def _MarkArtifactStaged(self):
184 """Marks the artifact as staged."""
185 with open(os.path.join(self.install_dir, self.marker_name), 'w') as f:
Gilad Arnold1638d822013-11-07 23:38:16 -0800186 f.write('\n'.join(self.installed_files))
Chris Sosa76e44b92013-01-31 12:11:38 -0800187
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800188 def _WaitForArtifactToExist(self, name, timeout):
Simran Basi4243a862014-12-12 12:48:33 -0800189 """Waits for artifact to exist and returns the appropriate names.
Chris Sosac4e87842013-08-16 18:04:14 -0700190
191 Args:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800192 name: Name to look at.
Simran Basi4243a862014-12-12 12:48:33 -0800193 timeout: How long to wait for artifact to become available. Only matters
194 if self.archive_url is a Google Storage URL.
195
196 Returns:
197 A list of names that match.
198
199 Raises:
200 ArtifactDownloadError: An error occurred when obtaining artifact.
201 """
202 if self.archive_url.startswith('gs://'):
203 return self._WaitForGSArtifactToExist(name, timeout)
204 return self._VerifyLocalArtifactExists(name)
205
206 def _WaitForGSArtifactToExist(self, name, timeout):
207 """Waits for artifact to exist and returns the appropriate names.
208
209 Args:
210 name: Name to look at.
211 timeout: How long to wait for the artifact to become available.
212
213 Returns:
214 A list of names that match.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800215
Gilad Arnold950569b2013-08-27 14:38:01 -0700216 Raises:
217 ArtifactDownloadError: An error occurred when obtaining artifact.
Chris Sosac4e87842013-08-16 18:04:14 -0700218 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800219 names = gsutil_util.GetGSNamesWithWait(
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800220 name, self.archive_url, str(self), timeout=timeout,
Gilad Arnold950569b2013-08-27 14:38:01 -0700221 is_regex_pattern=self.is_regex_name)
Chris Sosa76e44b92013-01-31 12:11:38 -0800222 if not names:
Don Garrettef484fb2013-11-22 16:56:18 -0800223 raise ArtifactDownloadError('Could not find %s in Google Storage at %s' %
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800224 (name, self.archive_url))
225 return names
Chris Sosa76e44b92013-01-31 12:11:38 -0800226
Simran Basi4243a862014-12-12 12:48:33 -0800227 def _VerifyLocalArtifactExists(self, name):
228 """Verifies the local artifact exists and returns the appropriate names.
229
230 Args:
231 name: Name to look at.
232
233 Returns:
234 A list of names that match.
235
236 Raises:
237 ArtifactDownloadError: An error occurred when obtaining artifact.
238 """
239 local_path = os.path.join(self.archive_url, name)
240 if self.is_regex_name:
241 filter_re = re.compile(name)
242 for filename in os.listdir(self.archive_url):
243 if filter_re.match(filename):
244 return [filename]
245 else:
246 glob_search = glob.glob(local_path)
247 if glob_search and len(glob_search) == 1:
248 return [os.path.basename(glob_search[0])]
249 raise ArtifactDownloadError('Artifact not found.')
250
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800251 def _UpdateName(self, names):
252 if self.single_name and len(names) > 1:
253 raise ArtifactDownloadError('Too many artifacts match %s' % self.name)
Chris Sosa76e44b92013-01-31 12:11:38 -0800254
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800255 self.name = names[0]
Chris Sosa76e44b92013-01-31 12:11:38 -0800256
257 def _Download(self):
joychen0a8e34e2013-06-24 17:58:36 -0700258 """Downloads artifact from Google Storage to a local directory."""
joychen0a8e34e2013-06-24 17:58:36 -0700259 self.install_path = os.path.join(self.install_dir, self.name)
Simran Basi4243a862014-12-12 12:48:33 -0800260 if self.archive_url.startswith('gs://'):
261 gs_path = '/'.join([self.archive_url, self.name])
262 gsutil_util.DownloadFromGS(gs_path, self.install_path)
263 else:
264 # It's a local path so just copy it into the staged directory.
265 shutil.copyfile(os.path.join(self.archive_url, self.name),
266 self.install_path)
267
Chris Sosa76e44b92013-01-31 12:11:38 -0800268
joychen0a8e34e2013-06-24 17:58:36 -0700269 def _Setup(self):
Gilad Arnold1638d822013-11-07 23:38:16 -0800270 """Process the downloaded content, update the list of installed files."""
271 # In this primitive case, what was downloaded (has to be a single file) is
272 # what's installed.
273 self.installed_files = [self.install_path]
joychen0a8e34e2013-06-24 17:58:36 -0700274
Dan Shi6e50c722013-08-19 15:05:06 -0700275 def _ClearException(self):
276 """Delete any existing exception saved for this artifact."""
277 if os.path.exists(self.exception_file_path):
278 os.remove(self.exception_file_path)
279
280 def _SaveException(self, e):
281 """Save the exception to a file for downloader.IsStaged to retrieve.
282
Gilad Arnold950569b2013-08-27 14:38:01 -0700283 Args:
284 e: Exception object to be saved.
Dan Shi6e50c722013-08-19 15:05:06 -0700285 """
286 with open(self.exception_file_path, 'w') as f:
287 pickle.dump(e, f)
288
289 def GetException(self):
290 """Retrieve any exception that was raised in Process method.
291
Gilad Arnold950569b2013-08-27 14:38:01 -0700292 Returns:
293 An Exception object that was raised when trying to process the artifact.
294 Return None if no exception was found.
Dan Shi6e50c722013-08-19 15:05:06 -0700295 """
296 if not os.path.exists(self.exception_file_path):
297 return None
298 with open(self.exception_file_path, 'r') as f:
299 return pickle.load(f)
Chris Sosa76e44b92013-01-31 12:11:38 -0800300
301 def Process(self, no_wait):
302 """Main call point to all artifacts. Downloads and Stages artifact.
303
304 Downloads and Stages artifact from Google Storage to the install directory
305 specified in the constructor. It multi-thread safe and does not overwrite
306 the artifact if it's already been downloaded or being downloaded. After
307 processing, leaves behind a marker to indicate to future invocations that
308 the artifact has already been staged based on the name of the artifact.
309
310 Do not override as it modifies important private variables, ensures thread
311 safety, and maintains cache semantics.
312
313 Note: this may be a blocking call when the artifact is already in the
314 process of being staged.
315
316 Args:
317 no_wait: If True, don't block waiting for artifact to exist if we fail to
318 immediately find it.
319
320 Raises:
321 ArtifactDownloadError: If the artifact fails to download from Google
322 Storage for any reason or that the regexp
323 defined by name is not specific enough.
324 """
325 if not self._process_lock:
326 self._process_lock = _build_artifact_locks.lock(
327 os.path.join(self.install_dir, self.name))
328
329 with self._process_lock:
330 common_util.MkDirP(self.install_dir)
Dan Shif8eb0d12013-08-01 17:52:06 -0700331 if not self.ArtifactStaged():
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800332 # Delete any existing exception saved for this artifact.
333 self._ClearException()
334 found_artifact = False
335 if self.optional_name:
336 try:
337 # Check if the artifact named |optional_name| exists on GS.
338 # Because this artifact may not always exist, don't bother
339 # to wait for it (set timeout=1).
340 new_names = self._WaitForArtifactToExist(
341 self.optional_name, timeout=1)
342 self._UpdateName(new_names)
343
344 except ArtifactDownloadError:
345 self._Log('Unable to download %s; fall back to download %s',
346 self.optional_name, self.name)
347 else:
348 found_artifact = True
349
Dan Shi6e50c722013-08-19 15:05:06 -0700350 try:
Dan Shi6e50c722013-08-19 15:05:06 -0700351 # If the artifact should already have been uploaded, don't waste
352 # cycles waiting around for it to exist.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800353 if not found_artifact:
354 timeout = 1 if no_wait else 10
355 new_names = self._WaitForArtifactToExist(self.name, timeout)
356 self._UpdateName(new_names)
357
358 self._Log('Downloading file %s', self.name)
Dan Shi6e50c722013-08-19 15:05:06 -0700359 self._Download()
360 self._Setup()
361 self._MarkArtifactStaged()
362 except Exception as e:
363 # Save the exception to a file for downloader.IsStaged to retrieve.
364 self._SaveException(e)
Gilad Arnold02dc6552013-11-14 11:27:54 -0800365
366 # Convert an unknown exception into an ArtifactDownloadError.
367 if type(e) is ArtifactDownloadError:
368 raise
369 else:
370 raise ArtifactDownloadError('An error occurred: %s' % e)
Chris Sosa76e44b92013-01-31 12:11:38 -0800371 else:
372 self._Log('%s is already staged.', self)
373
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700374 def __str__(self):
375 """String representation for the download."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800376 return '->'.join(['%s/%s' % (self.archive_url, self.name),
Gilad Arnold950569b2013-08-27 14:38:01 -0700377 self.install_dir])
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700378
Chris Sosab26b1202013-08-16 16:40:55 -0700379 def __repr__(self):
380 return str(self)
381
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700382
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700383class AUTestPayloadBuildArtifact(BuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700384 """Wrapper for AUTest delta payloads which need additional setup."""
Gilad Arnold950569b2013-08-27 14:38:01 -0700385
joychen0a8e34e2013-06-24 17:58:36 -0700386 def _Setup(self):
387 super(AUTestPayloadBuildArtifact, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700388
Chris Sosa76e44b92013-01-31 12:11:38 -0800389 # Rename to update.gz.
390 install_path = os.path.join(self.install_dir, self.name)
joychen3cb228e2013-06-12 12:13:13 -0700391 new_install_path = os.path.join(self.install_dir,
joychen7c2054a2013-07-25 11:14:07 -0700392 devserver_constants.UPDATE_FILE)
Chris Sosa76e44b92013-01-31 12:11:38 -0800393 shutil.move(install_path, new_install_path)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700394
Gilad Arnold1638d822013-11-07 23:38:16 -0800395 # Reflect the rename in the list of installed files.
396 self.installed_files.remove(install_path)
397 self.installed_files = [new_install_path]
398
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700399
Chris Sosa76e44b92013-01-31 12:11:38 -0800400# TODO(sosa): Change callers to make this artifact more sane.
401class DeltaPayloadsArtifact(BuildArtifact):
402 """Delta payloads from the archive_url.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700403
Chris Sosa76e44b92013-01-31 12:11:38 -0800404 This artifact is super strange. It custom handles directories and
405 pulls in all delta payloads. We can't specify exactly what we want
406 because unlike other artifacts, this one does not conform to something a
407 client might know. The client doesn't know the version of n-1 or whether it
408 was even generated.
Gilad Arnold950569b2013-08-27 14:38:01 -0700409
410 IMPORTANT! Note that this artifact simply ignores the `name' argument because
411 that name is derived internally in accordance with sub-artifacts. Also note
412 the different types of names (in fact, file name patterns) used for the
413 different sub-artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800414 """
Gilad Arnold950569b2013-08-27 14:38:01 -0700415
Chris Sosa76e44b92013-01-31 12:11:38 -0800416 def __init__(self, *args):
417 super(DeltaPayloadsArtifact, self).__init__(*args)
Gilad Arnold950569b2013-08-27 14:38:01 -0700418 # Override the name field, we know what it should be.
419 self.name = '*_delta_*'
420 self.is_regex_name = False
421 self.single_name = False # Expect multiple deltas
422
423 # We use a regular glob for the N-to-N delta payload.
424 nton_name = 'chromeos_%s*_delta_*' % self.build
425 # We use a regular expression for the M-to-N delta payload.
426 mton_name = ('chromeos_(?!%s).*_delta_.*' % re.escape(self.build))
427
Chris Sosa76e44b92013-01-31 12:11:38 -0800428 nton_install_dir = os.path.join(self.install_dir, _AU_BASE,
429 self.build + _NTON_DIR_SUFFIX)
430 mton_install_dir = os.path.join(self.install_dir, _AU_BASE,
Gilad Arnold950569b2013-08-27 14:38:01 -0700431 self.build + _MTON_DIR_SUFFIX)
Chris Sosa76e44b92013-01-31 12:11:38 -0800432 self._sub_artifacts = [
433 AUTestPayloadBuildArtifact(mton_install_dir, self.archive_url,
Gilad Arnold950569b2013-08-27 14:38:01 -0700434 mton_name, self.build, is_regex_name=True),
Chris Sosa76e44b92013-01-31 12:11:38 -0800435 AUTestPayloadBuildArtifact(nton_install_dir, self.archive_url,
436 nton_name, self.build)]
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700437
Chris Sosa76e44b92013-01-31 12:11:38 -0800438 def _Download(self):
joychen0a8e34e2013-06-24 17:58:36 -0700439 """With sub-artifacts we do everything in _Setup()."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800440 pass
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700441
joychen0a8e34e2013-06-24 17:58:36 -0700442 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800443 """Process each sub-artifact. Only error out if none can be found."""
444 for artifact in self._sub_artifacts:
445 try:
446 artifact.Process(no_wait=True)
447 # Setup symlink so that AU will work for this payload.
Gilad Arnold1638d822013-11-07 23:38:16 -0800448 stateful_update_symlink = os.path.join(
449 artifact.install_dir, devserver_constants.STATEFUL_FILE)
Chris Sosa76e44b92013-01-31 12:11:38 -0800450 os.symlink(
joychen25d25972013-07-30 14:54:16 -0700451 os.path.join(os.pardir, os.pardir,
joychen121fc9b2013-08-02 14:30:30 -0700452 devserver_constants.STATEFUL_FILE),
Gilad Arnold1638d822013-11-07 23:38:16 -0800453 stateful_update_symlink)
454
455 # Aggregate sub-artifact file lists, including stateful symlink.
456 self.installed_files += artifact.installed_files
457 self.installed_files.append(stateful_update_symlink)
Chris Sosa76e44b92013-01-31 12:11:38 -0800458 except ArtifactDownloadError as e:
459 self._Log('Could not process %s: %s', artifact, e)
Gilad Arnold02dc6552013-11-14 11:27:54 -0800460 raise
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700461
Chris Sosa76e44b92013-01-31 12:11:38 -0800462
463class BundledBuildArtifact(BuildArtifact):
464 """A single build artifact bundle e.g. zip file or tar file."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800465
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800466 def __init__(self, *args, **kwargs):
Gilad Arnold950569b2013-08-27 14:38:01 -0700467 """Takes BuildArtifact args with some additional ones.
468
469 Args:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800470 *args: See BuildArtifact documentation.
471 **kwargs: See BuildArtifact documentation.
Gilad Arnold950569b2013-08-27 14:38:01 -0700472 files_to_extract: A list of files to extract. If set to None, extract
473 all files.
474 exclude: A list of files to exclude. If None, no files are excluded.
Chris Sosa76e44b92013-01-31 12:11:38 -0800475 """
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800476 self._files_to_extract = kwargs.pop('files_to_extract', None)
477 self._exclude = kwargs.pop('exclude', None)
478 super(BundledBuildArtifact, self).__init__(*args, **kwargs)
Chris Sosa76e44b92013-01-31 12:11:38 -0800479
480 # We modify the marker so that it is unique to what was staged.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800481 if self._files_to_extract:
Chris Sosa76e44b92013-01-31 12:11:38 -0800482 self.marker_name = self._SanitizeName(
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800483 '_'.join(['.' + self.name] + self._files_to_extract))
Chris Sosa76e44b92013-01-31 12:11:38 -0800484
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800485 def _RunUnzip(self, list_only):
486 # Unzip is weird. It expects its args before any excludes and expects its
487 # excludes in a list following the -x.
488 cmd = ['unzip', '-qql' if list_only else '-o', self.install_path]
489 if not list_only:
490 cmd += ['-d', self.install_dir]
Chris Sosa76e44b92013-01-31 12:11:38 -0800491
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800492 if self._files_to_extract:
493 cmd.extend(self._files_to_extract)
494
495 if self._exclude:
496 cmd.append('-x')
497 cmd.extend(self._exclude)
498
499 try:
500 return subprocess.check_output(cmd).strip('\n').splitlines()
501 except subprocess.CalledProcessError, e:
502 raise ArtifactDownloadError(
503 'An error occurred when attempting to unzip %s:\n%s' %
504 (self.install_path, e))
Chris Sosa76e44b92013-01-31 12:11:38 -0800505
joychen0a8e34e2013-06-24 17:58:36 -0700506 def _Setup(self):
Gilad Arnold1638d822013-11-07 23:38:16 -0800507 extract_result = self._Extract()
508 if self.store_installed_files:
509 # List both the archive and the extracted files.
510 self.installed_files.append(self.install_path)
511 self.installed_files.extend(extract_result)
Chris Sosa76e44b92013-01-31 12:11:38 -0800512
Chris Sosa76e44b92013-01-31 12:11:38 -0800513 def _Extract(self):
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800514 """Extracts files into the install path."""
515 if self.name.endswith('.zip'):
516 return self._ExtractZipfile()
517 else:
518 return self._ExtractTarball()
519
520 def _ExtractZipfile(self):
521 """Extracts a zip file using unzip."""
522 file_list = [os.path.join(self.install_dir, line[30:].strip())
523 for line in self._RunUnzip(True)
524 if not line.endswith('/')]
525 if file_list:
526 self._RunUnzip(False)
527
528 return file_list
529
530 def _ExtractTarball(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800531 """Extracts a tarball using tar.
532
533 Detects whether the tarball is compressed or not based on the file
534 extension and extracts the tarball into the install_path.
535 """
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700536 try:
Gilad Arnold1638d822013-11-07 23:38:16 -0800537 return common_util.ExtractTarball(self.install_path, self.install_dir,
538 files_to_extract=self._files_to_extract,
539 excluded_files=self._exclude,
540 return_extracted_files=True)
Simran Basi4baad082013-02-14 13:39:18 -0800541 except common_util.CommonUtilError as e:
542 raise ArtifactDownloadError(str(e))
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700543
544
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800545class AutotestTarballBuildArtifact(BundledBuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700546 """Wrapper around the autotest tarball to download from gsutil."""
547
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800548 def __init__(self, *args, **kwargs):
549 super(AutotestTarballBuildArtifact, self).__init__(*args, **kwargs)
Gilad Arnold1638d822013-11-07 23:38:16 -0800550 # We don't store/check explicit file lists in Autotest tarball markers;
551 # this can get huge and unwieldy, and generally make little sense.
552 self.store_installed_files = False
553
joychen0a8e34e2013-06-24 17:58:36 -0700554 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800555 """Extracts the tarball into the install path excluding test suites."""
joychen0a8e34e2013-06-24 17:58:36 -0700556 super(AutotestTarballBuildArtifact, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700557
Chris Sosa76e44b92013-01-31 12:11:38 -0800558 # Deal with older autotest packages that may not be bundled.
joychen3cb228e2013-06-12 12:13:13 -0700559 autotest_dir = os.path.join(self.install_dir,
560 devserver_constants.AUTOTEST_DIR)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700561 autotest_pkgs_dir = os.path.join(autotest_dir, 'packages')
562 if not os.path.exists(autotest_pkgs_dir):
563 os.makedirs(autotest_pkgs_dir)
564
565 if not os.path.exists(os.path.join(autotest_pkgs_dir, 'packages.checksum')):
Chris Sosa76e44b92013-01-31 12:11:38 -0800566 cmd = ['autotest/utils/packager.py', 'upload', '--repository',
567 autotest_pkgs_dir, '--all']
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700568 try:
joychen0a8e34e2013-06-24 17:58:36 -0700569 subprocess.check_call(cmd, cwd=self.install_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700570 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800571 raise ArtifactDownloadError(
572 'Failed to create autotest packages!:\n%s' % e)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700573 else:
Gilad Arnoldf5843132012-09-25 00:31:20 -0700574 self._Log('Using pre-generated packages from autotest')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700575
Chris Masone816e38c2012-05-02 12:22:36 -0700576
Chris Sosa76e44b92013-01-31 12:11:38 -0800577class ImplDescription(object):
578 """Data wrapper that describes an artifact's implementation."""
Gilad Arnold950569b2013-08-27 14:38:01 -0700579
580 def __init__(self, artifact_class, name, *additional_args,
581 **additional_dargs):
582 """Constructor.
Chris Sosa76e44b92013-01-31 12:11:38 -0800583
584 Args:
585 artifact_class: BuildArtifact class to use for the artifact.
586 name: name to use to identify artifact (see BuildArtifact.name)
Gilad Arnold950569b2013-08-27 14:38:01 -0700587 *additional_args: Additional arguments to pass to artifact_class.
588 **additional_dargs: Additional named arguments to pass to artifact_class.
Chris Sosa76e44b92013-01-31 12:11:38 -0800589 """
590 self.artifact_class = artifact_class
591 self.name = name
592 self.additional_args = additional_args
Gilad Arnold950569b2013-08-27 14:38:01 -0700593 self.additional_dargs = additional_dargs
Chris Sosa76e44b92013-01-31 12:11:38 -0800594
Chris Sosa968a1062013-08-02 17:42:50 -0700595 def __repr__(self):
596 return '%s_%s' % (self.artifact_class, self.name)
597
Chris Sosa76e44b92013-01-31 12:11:38 -0800598
599# Maps artifact names to their implementation description.
600# Please note, it is good practice to use constants for these names if you're
601# going to re-use the names ANYWHERE else in the devserver code.
602ARTIFACT_IMPLEMENTATION_MAP = {
Gilad Arnold950569b2013-08-27 14:38:01 -0700603 artifact_info.FULL_PAYLOAD:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800604 ImplDescription(AUTestPayloadBuildArtifact, ('*_full_*')),
Gilad Arnold950569b2013-08-27 14:38:01 -0700605 artifact_info.DELTA_PAYLOADS:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800606 ImplDescription(DeltaPayloadsArtifact, ('DONTCARE')),
Gilad Arnold950569b2013-08-27 14:38:01 -0700607 artifact_info.STATEFUL_PAYLOAD:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800608 ImplDescription(BuildArtifact, (devserver_constants.STATEFUL_FILE)),
Chris Sosa76e44b92013-01-31 12:11:38 -0800609
Gilad Arnold950569b2013-08-27 14:38:01 -0700610 artifact_info.BASE_IMAGE:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800611 ImplDescription(BundledBuildArtifact, IMAGE_FILE,
612 optional_name=BASE_IMAGE_FILE,
Gilad Arnold69878b42013-09-18 13:39:22 -0700613 files_to_extract=[devserver_constants.BASE_IMAGE_FILE]),
Gilad Arnold950569b2013-08-27 14:38:01 -0700614 artifact_info.RECOVERY_IMAGE:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800615 ImplDescription(BundledBuildArtifact, IMAGE_FILE,
616 optional_name=RECOVERY_IMAGE_FILE,
Gilad Arnold69878b42013-09-18 13:39:22 -0700617 files_to_extract=[devserver_constants.RECOVERY_IMAGE_FILE]),
Chris Sosa75490802013-09-30 17:21:45 -0700618 artifact_info.DEV_IMAGE:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800619 ImplDescription(BundledBuildArtifact, IMAGE_FILE,
Chris Sosa75490802013-09-30 17:21:45 -0700620 files_to_extract=[devserver_constants.IMAGE_FILE]),
Gilad Arnold950569b2013-08-27 14:38:01 -0700621 artifact_info.TEST_IMAGE:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800622 ImplDescription(BundledBuildArtifact, IMAGE_FILE,
623 optional_name=TEST_IMAGE_FILE,
Gilad Arnold69878b42013-09-18 13:39:22 -0700624 files_to_extract=[devserver_constants.TEST_IMAGE_FILE]),
Chris Sosa76e44b92013-01-31 12:11:38 -0800625
Gilad Arnold950569b2013-08-27 14:38:01 -0700626 artifact_info.AUTOTEST:
627 ImplDescription(AutotestTarballBuildArtifact, AUTOTEST_FILE,
628 files_to_extract=None,
629 exclude=['autotest/test_suites']),
Simran Basiea0590d2014-10-29 11:31:26 -0700630 artifact_info.CONTROL_FILES:
631 ImplDescription(BundledBuildArtifact, CONTROL_FILES_FILE),
632 artifact_info.AUTOTEST_PACKAGES:
633 ImplDescription(AutotestTarballBuildArtifact, AUTOTEST_PACKAGES_FILE),
Gilad Arnold950569b2013-08-27 14:38:01 -0700634 artifact_info.TEST_SUITES:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800635 ImplDescription(BundledBuildArtifact, TEST_SUITES_FILE),
Gilad Arnold950569b2013-08-27 14:38:01 -0700636 artifact_info.AU_SUITE:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800637 ImplDescription(BundledBuildArtifact, AU_SUITE_FILE),
Chris Sosa76e44b92013-01-31 12:11:38 -0800638
Gilad Arnold950569b2013-08-27 14:38:01 -0700639 artifact_info.FIRMWARE:
640 ImplDescription(BuildArtifact, FIRMWARE_FILE),
641 artifact_info.SYMBOLS:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800642 ImplDescription(BundledBuildArtifact, DEBUG_SYMBOLS_FILE,
Gilad Arnold950569b2013-08-27 14:38:01 -0700643 files_to_extract=['debug/breakpad']),
beepsc3d0f872013-07-31 21:50:40 -0700644
Gilad Arnold950569b2013-08-27 14:38:01 -0700645 artifact_info.FACTORY_IMAGE:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800646 ImplDescription(BundledBuildArtifact, FACTORY_FILE,
Gilad Arnold69878b42013-09-18 13:39:22 -0700647 files_to_extract=[devserver_constants.FACTORY_IMAGE_FILE])
Chris Sosa76e44b92013-01-31 12:11:38 -0800648}
649
Chris Sosa968a1062013-08-02 17:42:50 -0700650# Add all the paygen_au artifacts in one go.
651ARTIFACT_IMPLEMENTATION_MAP.update({
Gilad Arnold950569b2013-08-27 14:38:01 -0700652 artifact_info.PAYGEN_AU_SUITE_TEMPLATE % {'channel': c}:
653 ImplDescription(
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800654 BundledBuildArtifact, PAYGEN_AU_SUITE_FILE_TEMPLATE % {'channel': c})
Gilad Arnold950569b2013-08-27 14:38:01 -0700655 for c in devserver_constants.CHANNELS
Chris Sosa968a1062013-08-02 17:42:50 -0700656})
657
Chris Sosa76e44b92013-01-31 12:11:38 -0800658
659class ArtifactFactory(object):
660 """A factory class that generates build artifacts from artifact names."""
661
Chris Sosa6b0c6172013-08-05 17:01:33 -0700662 def __init__(self, download_dir, archive_url, artifacts, files,
663 build):
Chris Sosa76e44b92013-01-31 12:11:38 -0800664 """Initalizes the member variables for the factory.
665
666 Args:
Gilad Arnold950569b2013-08-27 14:38:01 -0700667 download_dir: A directory to which artifacts are downloaded.
Chris Sosa76e44b92013-01-31 12:11:38 -0800668 archive_url: the Google Storage url of the bucket where the debug
669 symbols for the desired build are stored.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700670 artifacts: List of artifacts to stage. These artifacts must be
671 defined in artifact_info.py and have a mapping in the
672 ARTIFACT_IMPLEMENTATION_MAP.
673 files: List of files to stage. These files are just downloaded and staged
674 as files into the download_dir.
Chris Sosa76e44b92013-01-31 12:11:38 -0800675 build: The name of the build.
676 """
joychen0a8e34e2013-06-24 17:58:36 -0700677 self.download_dir = download_dir
Chris Sosa76e44b92013-01-31 12:11:38 -0800678 self.archive_url = archive_url
Chris Sosa6b0c6172013-08-05 17:01:33 -0700679 self.artifacts = artifacts
680 self.files = files
Chris Sosa76e44b92013-01-31 12:11:38 -0800681 self.build = build
682
683 @staticmethod
Chris Sosa6b0c6172013-08-05 17:01:33 -0700684 def _GetDescriptionComponents(name, is_artifact):
Gilad Arnold950569b2013-08-27 14:38:01 -0700685 """Returns components for constructing a BuildArtifact.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700686
Gilad Arnold950569b2013-08-27 14:38:01 -0700687 Args:
688 name: The artifact name / file pattern.
689 is_artifact: Whether this is a named (True) or file (False) artifact.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800690
Gilad Arnold950569b2013-08-27 14:38:01 -0700691 Returns:
692 A tuple consisting of the BuildArtifact subclass, name, and additional
693 list- and named-arguments.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800694
Gilad Arnold950569b2013-08-27 14:38:01 -0700695 Raises:
696 KeyError: if artifact doesn't exist in ARTIFACT_IMPLEMENTATION_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700697 """
698
699 if is_artifact:
700 description = ARTIFACT_IMPLEMENTATION_MAP[name]
701 else:
702 description = ImplDescription(BuildArtifact, name)
703
Chris Sosa76e44b92013-01-31 12:11:38 -0800704 return (description.artifact_class, description.name,
Gilad Arnold950569b2013-08-27 14:38:01 -0700705 description.additional_args, description.additional_dargs)
Chris Sosa76e44b92013-01-31 12:11:38 -0800706
Chris Sosa6b0c6172013-08-05 17:01:33 -0700707 def _Artifacts(self, names, is_artifact):
Gilad Arnold950569b2013-08-27 14:38:01 -0700708 """Returns the BuildArtifacts from |names|.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700709
710 If is_artifact is true, then these names define artifacts that must exist in
711 the ARTIFACT_IMPLEMENTATION_MAP. Otherwise, treat as filenames to stage as
712 basic BuildArtifacts.
713
Gilad Arnold950569b2013-08-27 14:38:01 -0700714 Args:
715 names: A sequence of artifact names.
716 is_artifact: Whether this is a named (True) or file (False) artifact.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800717
Gilad Arnold950569b2013-08-27 14:38:01 -0700718 Returns:
719 An iterable of BuildArtifacts.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800720
Gilad Arnold950569b2013-08-27 14:38:01 -0700721 Raises:
722 KeyError: if artifact doesn't exist in ARTIFACT_IMPLEMENTATION_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700723 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800724 artifacts = []
Chris Sosa6b0c6172013-08-05 17:01:33 -0700725 for name in names:
Gilad Arnold950569b2013-08-27 14:38:01 -0700726 artifact_class, path, args, dargs = self._GetDescriptionComponents(
Chris Sosa6b0c6172013-08-05 17:01:33 -0700727 name, is_artifact)
joychen0a8e34e2013-06-24 17:58:36 -0700728 artifacts.append(artifact_class(self.download_dir, self.archive_url, path,
Gilad Arnold950569b2013-08-27 14:38:01 -0700729 self.build, *args, **dargs))
Chris Sosa76e44b92013-01-31 12:11:38 -0800730
731 return artifacts
732
733 def RequiredArtifacts(self):
Gilad Arnold950569b2013-08-27 14:38:01 -0700734 """Returns BuildArtifacts for the factory's artifacts.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700735
Gilad Arnold950569b2013-08-27 14:38:01 -0700736 Returns:
737 An iterable of BuildArtifacts.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800738
Gilad Arnold950569b2013-08-27 14:38:01 -0700739 Raises:
740 KeyError: if artifact doesn't exist in ARTIFACT_IMPLEMENTATION_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700741 """
742 artifacts = []
743 if self.artifacts:
744 artifacts.extend(self._Artifacts(self.artifacts, True))
745 if self.files:
746 artifacts.extend(self._Artifacts(self.files, False))
747
748 return artifacts
Chris Sosa76e44b92013-01-31 12:11:38 -0800749
750 def OptionalArtifacts(self):
Gilad Arnold950569b2013-08-27 14:38:01 -0700751 """Returns BuildArtifacts that should be cached.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700752
Gilad Arnold950569b2013-08-27 14:38:01 -0700753 Returns:
754 An iterable of BuildArtifacts.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800755
Gilad Arnold950569b2013-08-27 14:38:01 -0700756 Raises:
757 KeyError: if an optional artifact doesn't exist in
758 ARTIFACT_IMPLEMENTATION_MAP yet defined in
759 artifact_info.REQUESTED_TO_OPTIONAL_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700760 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800761 optional_names = set()
762 for artifact_name, optional_list in (
763 artifact_info.REQUESTED_TO_OPTIONAL_MAP.iteritems()):
764 # We are already downloading it.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700765 if artifact_name in self.artifacts:
Chris Sosa76e44b92013-01-31 12:11:38 -0800766 optional_names = optional_names.union(optional_list)
767
Chris Sosa6b0c6172013-08-05 17:01:33 -0700768 return self._Artifacts(optional_names - set(self.artifacts), True)
Chris Sosa968a1062013-08-02 17:42:50 -0700769
770
771# A simple main to verify correctness of the artifact map when making simple
772# name changes.
773if __name__ == '__main__':
774 print 'ARTIFACT IMPLEMENTATION MAP (for debugging)'
775 print 'FORMAT: ARTIFACT -> IMPLEMENTATION (<class>_file)'
776 for key, value in sorted(ARTIFACT_IMPLEMENTATION_MAP.items()):
777 print '%s -> %s' % (key, value)