Upstream version 11.40.271.0
[platform/framework/web/crosswalk.git] / src / tools / auto_bisect / fetch_build.py
1 # Copyright 2014 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 """This module contains functions for fetching and extracting archived builds.
6
7 The builds may be stored in different places by different types of builders;
8 for example, builders on tryserver.chromium.perf stores builds in one place,
9 while builders on chromium.linux store builds in another.
10
11 This module can be either imported or run as a stand-alone script to download
12 and extract a build.
13
14 Usage: fetch_build.py <type> <revision> <output_dir> [options]
15 """
16
17 import argparse
18 import errno
19 import os
20 import shutil
21 import sys
22 import zipfile
23
24 # Telemetry (src/tools/telemetry) is expected to be in the PYTHONPATH.
25 from telemetry.util import cloud_storage
26
27 import bisect_utils
28
29 # Possible builder types.
30 PERF_BUILDER = 'perf'
31 FULL_BUILDER = 'full'
32
33
34 def FetchBuild(builder_type, revision, output_dir, target_arch='ia32',
35                target_platform='chromium', deps_patch_sha=None):
36   """Downloads and extracts a build for a particular revision.
37
38   If the build is successfully downloaded and extracted to |output_dir|, the
39   downloaded archive file is also deleted.
40
41   Args:
42     revision: Revision string, e.g. a git commit hash or SVN revision.
43     builder_type: Type of build archive.
44     target_arch: Architecture, e.g. "ia32".
45     target_platform: Platform name, e.g. "chromium" or "android".
46     deps_patch_sha: SHA1 hash of a DEPS file, if we want to fetch a build for
47         a Chromium revision with custom dependencies.
48
49   Raises:
50     IOError: Unzipping failed.
51     OSError: Directory creation or deletion failed.
52   """
53   build_archive = BuildArchive.Create(
54       builder_type, target_arch=target_arch, target_platform=target_platform)
55   bucket = build_archive.BucketName()
56   remote_path = build_archive.FilePath(revision, deps_patch_sha=deps_patch_sha)
57
58   filename = FetchFromCloudStorage(bucket, remote_path, output_dir)
59   if not filename:
60     raise RuntimeError('Failed to fetch gs://%s/%s.' % (bucket, remote_path))
61
62   Unzip(filename, output_dir)
63
64   if os.path.exists(filename):
65     os.remove(filename)
66
67
68 class BuildArchive(object):
69   """Represents a place where builds of some type are stored.
70
71   There are two pieces of information required to locate a file in Google
72   Cloud Storage, bucket name and file path. Subclasses of this class contain
73   specific logic about which bucket names and paths should be used to fetch
74   a build.
75   """
76
77   @staticmethod
78   def Create(builder_type, target_arch='ia32', target_platform='chromium'):
79     if builder_type == PERF_BUILDER:
80       return PerfBuildArchive(target_arch, target_platform)
81     if builder_type == FULL_BUILDER:
82       return FullBuildArchive(target_arch, target_platform)
83     raise NotImplementedError('Builder type "%s" not supported.' % builder_type)
84
85   def __init__(self, target_arch='ia32', target_platform='chromium'):
86     if bisect_utils.IsLinuxHost() and target_platform == 'android':
87       self._platform = 'android'
88     elif bisect_utils.IsLinuxHost():
89       self._platform = 'linux'
90     elif bisect_utils.IsMacHost():
91       self._platform = 'mac'
92     elif bisect_utils.Is64BitWindows() and target_arch == 'x64':
93       self._platform = 'win64'
94     elif bisect_utils.IsWindowsHost():
95       self._platform = 'win'
96     else:
97       raise NotImplementedError('Unknown platform "%s".' % sys.platform)
98
99   def BucketName(self):
100     raise NotImplementedError()
101
102   def FilePath(self, revision, deps_patch_sha=None):
103     """Returns the remote file path to download a build from.
104
105     Args:
106       revision: A Chromium revision; this could be a git commit hash or
107           commit position or SVN revision number.
108       deps_patch_sha: The SHA1 hash of a patch to the DEPS file, which
109           uniquely identifies a change to use a particular revision of
110           a dependency.
111
112     Returns:
113       A file path, which not does not include a bucket name.
114     """
115     raise NotImplementedError()
116
117   def _ZipFileName(self, revision, deps_patch_sha=None):
118     """Gets the file name of a zip archive for a particular revision.
119
120     This returns a file name of the form full-build-<platform>_<revision>.zip,
121     which is a format used by multiple types of builders that store archives.
122
123     Args:
124       revision: A git commit hash or other revision string.
125       deps_patch_sha: SHA1 hash of a DEPS file patch.
126
127     Returns:
128       The archive file name.
129     """
130     base_name = 'full-build-%s' % self._PlatformName()
131     if deps_patch_sha:
132       revision = '%s_%s' % (revision, deps_patch_sha)
133     return '%s_%s.zip' % (base_name, revision)
134
135   def _PlatformName(self):
136     """Return a string to be used in paths for the platform."""
137     if self._platform in ('win', 'win64'):
138       # Build archive for win64 is still stored with "win32" in the name.
139       return 'win32'
140     if self._platform in ('linux', 'android'):
141       # Android builds are also stored with "linux" in the name.
142       return 'linux'
143     if self._platform == 'mac':
144       return 'mac'
145     raise NotImplementedError('Unknown platform "%s".' % sys.platform)
146
147
148 class PerfBuildArchive(BuildArchive):
149
150   def BucketName(self):
151     return 'chrome-perf'
152
153   def FilePath(self, revision, deps_patch_sha=None):
154     return '%s/%s' % (self._ArchiveDirectory(),
155                       self._ZipFileName(revision, deps_patch_sha))
156
157   def _ArchiveDirectory(self):
158     """Returns the directory name to download builds from."""
159     platform_to_directory = {
160         'android': 'android_perf_rel',
161         'linux': 'Linux Builder',
162         'mac': 'Mac Builder',
163         'win64': 'Win x64 Builder',
164         'win': 'Win Builder',
165     }
166     assert self._platform in platform_to_directory
167     return platform_to_directory.get(self._platform)
168
169
170 class FullBuildArchive(BuildArchive):
171
172   def BucketName(self):
173     platform_to_bucket = {
174         'android': 'chromium-android',
175         'linux': 'chromium-linux-archive',
176         'mac': 'chromium-mac-archive',
177         'win64': 'chromium-win-archive',
178         'win': 'chromium-win-archive',
179     }
180     assert self._platform in platform_to_bucket
181     return platform_to_bucket.get(self._platform)
182
183   def FilePath(self, revision, deps_patch_sha=None):
184     return '%s/%s' % (self._ArchiveDirectory(),
185                       self._ZipFileName(revision, deps_patch_sha))
186
187   def _ArchiveDirectory(self):
188     """Returns the remote directory to download builds from."""
189     platform_to_directory = {
190         'android': 'android_main_rel',
191         'linux': 'chromium.linux/Linux Builder',
192         'mac': 'chromium.mac/Mac Builder',
193         'win64': 'chromium.win/Win x64 Builder',
194         'win': 'chromium.win/Win Builder',
195     }
196     assert self._platform in platform_to_directory
197     return platform_to_directory.get(self._platform)
198
199
200 def FetchFromCloudStorage(bucket_name, source_path, destination_dir):
201   """Fetches file(s) from the Google Cloud Storage.
202
203   As a side-effect, this prints messages to stdout about what's happening.
204
205   Args:
206     bucket_name: Google Storage bucket name.
207     source_path: Source file path.
208     destination_dir: Destination file path.
209
210   Returns:
211     Local file path of downloaded file if it was downloaded. If the file does
212     not exist in the given bucket, or if there was an error while downloading,
213     None is returned.
214   """
215   target_file = os.path.join(destination_dir, os.path.basename(source_path))
216   gs_url = 'gs://%s/%s' % (bucket_name, source_path)
217   try:
218     if cloud_storage.Exists(bucket_name, source_path):
219       print 'Fetching file from %s...' % gs_url
220       cloud_storage.Get(bucket_name, source_path, target_file)
221       if os.path.exists(target_file):
222         return target_file
223     else:
224       print 'File %s not found in cloud storage.' % gs_url
225   except Exception as e:
226     print 'Exception while fetching from cloud storage: %s' % e
227     if os.path.exists(target_file):
228       os.remove(target_file)
229   return None
230
231
232 def Unzip(filename, output_dir, verbose=True):
233   """Extracts a zip archive's contents into the given output directory.
234
235   This was based on ExtractZip from build/scripts/common/chromium_utils.py.
236
237   Args:
238     filename: Name of the zip file to extract.
239     output_dir: Path to the destination directory.
240     verbose: Whether to print out what is being extracted.
241
242   Raises:
243     IOError: The unzip command had a non-zero exit code.
244     RuntimeError: Failed to create the output directory.
245   """
246   _MakeDirectory(output_dir)
247
248   # On Linux and Mac, we use the unzip command because it handles links and
249   # file permissions bits, so achieving this behavior is easier than with
250   # ZipInfo options.
251   #
252   # The Mac Version of unzip unfortunately does not support Zip64, whereas
253   # the python module does, so we have to fall back to the python zip module
254   # on Mac if the file size is greater than 4GB.
255   mac_zip_size_limit = 2 ** 32  # 4GB
256   if (bisect_utils.IsLinuxHost() or
257       (bisect_utils.IsMacHost()
258        and os.path.getsize(filename) < mac_zip_size_limit)):
259     unzip_command = ['unzip', '-o']
260     _UnzipUsingCommand(unzip_command, filename, output_dir)
261     return
262
263   # On Windows, try to use 7z if it is installed, otherwise fall back to the
264   # Python zipfile module. If 7z is not installed, then this may fail if the
265   # zip file is larger than 512MB.
266   sevenzip_path = r'C:\Program Files\7-Zip\7z.exe'
267   if bisect_utils.IsWindowsHost() and os.path.exists(sevenzip_path):
268     unzip_command = [sevenzip_path, 'x', '-y']
269     _UnzipUsingCommand(unzip_command, filename, output_dir)
270     return
271
272   _UnzipUsingZipFile(filename, output_dir, verbose)
273
274
275 def _UnzipUsingCommand(unzip_command, filename, output_dir):
276   """Extracts a zip file using an external command.
277
278   Args:
279     unzip_command: An unzipping command, as a string list, without the filename.
280     filename: Path to the zip file.
281     output_dir: The directory which the contents should be extracted to.
282
283   Raises:
284     IOError: The command had a non-zero exit code.
285   """
286   absolute_filepath = os.path.abspath(filename)
287   command = unzip_command + [absolute_filepath]
288   return_code = _RunCommandInDirectory(output_dir, command)
289   if return_code:
290     _RemoveDirectoryTree(output_dir)
291     raise IOError('Unzip failed: %s => %s' % (str(command), return_code))
292
293
294 def _RunCommandInDirectory(directory, command):
295   """Changes to a directory, runs a command, then changes back."""
296   saved_dir = os.getcwd()
297   os.chdir(directory)
298   return_code = bisect_utils.RunProcess(command)
299   os.chdir(saved_dir)
300   return return_code
301
302
303 def _UnzipUsingZipFile(filename, output_dir, verbose=True):
304   """Extracts a zip file using the Python zipfile module."""
305   assert bisect_utils.IsWindowsHost() or bisect_utils.IsMacHost()
306   zf = zipfile.ZipFile(filename)
307   for name in zf.namelist():
308     if verbose:
309       print 'Extracting %s' % name
310     zf.extract(name, output_dir)
311     if bisect_utils.IsMacHost():
312       # Restore file permission bits.
313       mode = zf.getinfo(name).external_attr >> 16
314       os.chmod(os.path.join(output_dir, name), mode)
315
316
317 def _MakeDirectory(path):
318   try:
319     os.makedirs(path)
320   except OSError as e:
321     if e.errno != errno.EEXIST:
322       raise
323
324
325 def _RemoveDirectoryTree(path):
326   try:
327     if os.path.exists(path):
328       shutil.rmtree(path)
329   except OSError, e:
330     if e.errno != errno.ENOENT:
331       raise
332
333
334 def Main(argv):
335   """Downloads and extracts a build based on the command line arguments."""
336   parser = argparse.ArgumentParser()
337   parser.add_argument('builder_type')
338   parser.add_argument('revision')
339   parser.add_argument('output_dir')
340   parser.add_argument('--target-arch', default='ia32')
341   parser.add_argument('--target-platform', default='chromium')
342   parser.add_argument('--deps-patch-sha')
343   args = parser.parse_args(argv[1:])
344
345   FetchBuild(
346       args.builder_type, args.revision, args.output_dir,
347       target_arch=args.target_arch, target_platform=args.target_platform,
348       deps_patch_sha=args.deps_patch_sha)
349
350   print 'Build has been downloaded to and extracted in %s.' % args.output_dir
351
352   return 0
353
354
355 if __name__ == '__main__':
356   sys.exit(Main(sys.argv))
357