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.
5 """This module contains functions for fetching and extracting archived builds.
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.
11 This module can be either imported or run as a stand-alone script to download
14 Usage: fetch_build.py <type> <revision> <output_dir> [options]
24 # Telemetry (src/tools/telemetry) is expected to be in the PYTHONPATH.
25 from telemetry
.util
import cloud_storage
29 # Possible builder types.
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.
38 If the build is successfully downloaded and extracted to |output_dir|, the
39 downloaded archive file is also deleted.
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.
50 IOError: Unzipping failed.
51 OSError: Directory creation or deletion failed.
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
)
58 filename
= FetchFromCloudStorage(bucket
, remote_path
, output_dir
)
60 raise RuntimeError('Failed to fetch gs://%s/%s.' % (bucket
, remote_path
))
62 Unzip(filename
, output_dir
)
64 if os
.path
.exists(filename
):
68 class BuildArchive(object):
69 """Represents a place where builds of some type are stored.
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
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
)
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'
97 raise NotImplementedError('Unknown platform "%s".' % sys
.platform
)
100 raise NotImplementedError()
102 def FilePath(self
, revision
, deps_patch_sha
=None):
103 """Returns the remote file path to download a build from.
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
113 A file path, which not does not include a bucket name.
115 raise NotImplementedError()
117 def _ZipFileName(self
, revision
, deps_patch_sha
=None):
118 """Gets the file name of a zip archive for a particular revision.
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.
124 revision: A git commit hash or other revision string.
125 deps_patch_sha: SHA1 hash of a DEPS file patch.
128 The archive file name.
130 base_name
= 'full-build-%s' % self
._PlatformName
()
132 revision
= '%s_%s' % (revision
, deps_patch_sha
)
133 return '%s_%s.zip' % (base_name
, revision
)
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.
140 if self
._platform
in ('linux', 'android'):
141 # Android builds are also stored with "linux" in the name.
143 if self
._platform
== 'mac':
145 raise NotImplementedError('Unknown platform "%s".' % sys
.platform
)
148 class PerfBuildArchive(BuildArchive
):
150 def BucketName(self
):
153 def FilePath(self
, revision
, deps_patch_sha
=None):
154 return '%s/%s' % (self
._ArchiveDirectory
(),
155 self
._ZipFileName
(revision
, deps_patch_sha
))
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',
166 assert self
._platform
in platform_to_directory
167 return platform_to_directory
.get(self
._platform
)
170 class FullBuildArchive(BuildArchive
):
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',
180 assert self
._platform
in platform_to_bucket
181 return platform_to_bucket
.get(self
._platform
)
183 def FilePath(self
, revision
, deps_patch_sha
=None):
184 return '%s/%s' % (self
._ArchiveDirectory
(),
185 self
._ZipFileName
(revision
, deps_patch_sha
))
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',
196 assert self
._platform
in platform_to_directory
197 return platform_to_directory
.get(self
._platform
)
200 def FetchFromCloudStorage(bucket_name
, source_path
, destination_dir
):
201 """Fetches file(s) from the Google Cloud Storage.
203 As a side-effect, this prints messages to stdout about what's happening.
206 bucket_name: Google Storage bucket name.
207 source_path: Source file path.
208 destination_dir: Destination file path.
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,
215 target_file
= os
.path
.join(destination_dir
, os
.path
.basename(source_path
))
216 gs_url
= 'gs://%s/%s' % (bucket_name
, source_path
)
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
):
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
)
232 def Unzip(filename
, output_dir
, verbose
=True):
233 """Extracts a zip archive's contents into the given output directory.
235 This was based on ExtractZip from build/scripts/common/chromium_utils.py.
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.
243 IOError: The unzip command had a non-zero exit code.
244 RuntimeError: Failed to create the output directory.
246 _MakeDirectory(output_dir
)
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
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
)
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
)
272 _UnzipUsingZipFile(filename
, output_dir
, verbose
)
275 def _UnzipUsingCommand(unzip_command
, filename
, output_dir
):
276 """Extracts a zip file using an external command.
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.
284 IOError: The command had a non-zero exit code.
286 absolute_filepath
= os
.path
.abspath(filename
)
287 command
= unzip_command
+ [absolute_filepath
]
288 return_code
= _RunCommandInDirectory(output_dir
, command
)
290 _RemoveDirectoryTree(output_dir
)
291 raise IOError('Unzip failed: %s => %s' % (str(command
), return_code
))
294 def _RunCommandInDirectory(directory
, command
):
295 """Changes to a directory, runs a command, then changes back."""
296 saved_dir
= os
.getcwd()
298 return_code
= bisect_utils
.RunProcess(command
)
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():
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
)
317 def _MakeDirectory(path
):
321 if e
.errno
!= errno
.EEXIST
:
325 def _RemoveDirectoryTree(path
):
327 if os
.path
.exists(path
):
330 if e
.errno
!= errno
.ENOENT
:
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:])
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
)
350 print 'Build has been downloaded to and extracted in %s.' % args
.output_dir
355 if __name__
== '__main__':
356 sys
.exit(Main(sys
.argv
))