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]
25 # Telemetry (src/tools/telemetry) is expected to be in the PYTHONPATH.
26 from telemetry
.util
import cloud_storage
30 # Possible builder types.
33 ANDROID_CHROME_PERF_BUILDER
= 'android-chrome-perf'
36 def GetBucketAndRemotePath(revision
, builder_type
=PERF_BUILDER
,
37 target_arch
='ia32', target_platform
='chromium',
38 deps_patch_sha
=None, extra_src
=None):
39 """Returns the location where a build archive is expected to be.
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 which identifies a particular combination of
47 custom revisions for dependency repositories.
48 extra_src: Path to a script which can be used to modify the bisect script's
52 A pair of strings (bucket, path), where the archive is expected to be.
54 logging
.info('Creating BuildArchive, type "%s", arch "%s", platform "%s".',
55 builder_type
, target_arch
, target_platform
)
56 build_archive
= BuildArchive
.Create(
57 builder_type
, target_arch
=target_arch
, target_platform
=target_platform
,
59 bucket
= build_archive
.BucketName()
60 remote_path
= build_archive
.FilePath(revision
, deps_patch_sha
=deps_patch_sha
)
61 return bucket
, remote_path
64 class BuildArchive(object):
65 """Represents a place where builds of some type are stored.
67 There are two pieces of information required to locate a file in Google
68 Cloud Storage, bucket name and file path. Subclasses of this class contain
69 specific logic about which bucket names and paths should be used to fetch
74 def Create(builder_type
, target_arch
='ia32', target_platform
='chromium',
76 logging
.info('Creating BuildArchive, type "%s", arch "%s", platform "%s".',
77 builder_type
, target_arch
, target_platform
)
78 if builder_type
== PERF_BUILDER
:
79 return PerfBuildArchive(target_arch
, target_platform
)
80 if builder_type
== FULL_BUILDER
:
81 return FullBuildArchive(target_arch
, target_platform
)
82 if builder_type
== ANDROID_CHROME_PERF_BUILDER
:
84 # Load and initialize a module in extra source file and
85 # return its module object to access android-chrome specific data.
86 loaded_extra_src
= bisect_utils
.LoadExtraSrc(extra_src
)
87 return AndroidChromeBuildArchive(
88 target_arch
, target_platform
, loaded_extra_src
)
89 except (IOError, TypeError, ImportError):
90 raise RuntimeError('Invalid or missing --extra_src. [%s]' % extra_src
)
91 raise NotImplementedError('Builder type "%s" not supported.' % builder_type
)
93 def __init__(self
, target_arch
='ia32', target_platform
='chromium',
95 self
._extra
_src
= extra_src
96 if bisect_utils
.IsLinuxHost() and target_platform
== 'android':
97 self
._platform
= 'android'
98 elif bisect_utils
.IsLinuxHost():
99 self
._platform
= 'linux'
100 elif bisect_utils
.IsMacHost():
101 self
._platform
= 'mac'
102 elif bisect_utils
.Is64BitWindows() and target_arch
== 'x64':
103 self
._platform
= 'win64'
104 elif bisect_utils
.IsWindowsHost():
105 self
._platform
= 'win'
107 raise NotImplementedError('Unknown platform "%s".' % sys
.platform
)
109 def BucketName(self
):
110 raise NotImplementedError()
112 def FilePath(self
, revision
, deps_patch_sha
=None):
113 """Returns the remote file path to download a build from.
116 revision: A Chromium revision; this could be a git commit hash or
117 commit position or SVN revision number.
118 deps_patch_sha: The SHA1 hash of a patch to the DEPS file, which
119 uniquely identifies a change to use a particular revision of
123 A file path, which not does not include a bucket name.
125 raise NotImplementedError()
127 def _ZipFileName(self
, revision
, deps_patch_sha
=None):
128 """Gets the file name of a zip archive for a particular revision.
130 This returns a file name of the form full-build-<platform>_<revision>.zip,
131 which is a format used by multiple types of builders that store archives.
134 revision: A git commit hash or other revision string.
135 deps_patch_sha: SHA1 hash of a DEPS file patch.
138 The archive file name.
140 base_name
= 'full-build-%s' % self
._PlatformName
()
142 revision
= '%s_%s' % (revision
, deps_patch_sha
)
143 return '%s_%s.zip' % (base_name
, revision
)
145 def _PlatformName(self
):
146 """Return a string to be used in paths for the platform."""
147 if self
._platform
in ('win', 'win64'):
148 # Build archive for win64 is still stored with "win32" in the name.
150 if self
._platform
in ('linux', 'android'):
151 # Android builds are also stored with "linux" in the name.
153 if self
._platform
== 'mac':
155 raise NotImplementedError('Unknown platform "%s".' % sys
.platform
)
158 class PerfBuildArchive(BuildArchive
):
160 def BucketName(self
):
163 def FilePath(self
, revision
, deps_patch_sha
=None):
164 return '%s/%s' % (self
._ArchiveDirectory
(),
165 self
._ZipFileName
(revision
, deps_patch_sha
))
167 def _ArchiveDirectory(self
):
168 """Returns the directory name to download builds from."""
169 platform_to_directory
= {
170 'android': 'android_perf_rel',
171 'linux': 'Linux Builder',
172 'mac': 'Mac Builder',
173 'win64': 'Win x64 Builder',
174 'win': 'Win Builder',
176 assert self
._platform
in platform_to_directory
177 return platform_to_directory
.get(self
._platform
)
180 class FullBuildArchive(BuildArchive
):
182 def BucketName(self
):
183 platform_to_bucket
= {
184 'android': 'chromium-android',
185 'linux': 'chromium-linux-archive',
186 'mac': 'chromium-mac-archive',
187 'win64': 'chromium-win-archive',
188 'win': 'chromium-win-archive',
190 assert self
._platform
in platform_to_bucket
191 return platform_to_bucket
.get(self
._platform
)
193 def FilePath(self
, revision
, deps_patch_sha
=None):
194 return '%s/%s' % (self
._ArchiveDirectory
(),
195 self
._ZipFileName
(revision
, deps_patch_sha
))
197 def _ArchiveDirectory(self
):
198 """Returns the remote directory to download builds from."""
199 platform_to_directory
= {
200 'android': 'android_main_rel',
201 'linux': 'chromium.linux/Linux Builder',
202 'mac': 'chromium.mac/Mac Builder',
203 'win64': 'chromium.win/Win x64 Builder',
204 'win': 'chromium.win/Win Builder',
206 assert self
._platform
in platform_to_directory
207 return platform_to_directory
.get(self
._platform
)
210 class AndroidChromeBuildArchive(BuildArchive
):
211 """Represents a place where builds of android-chrome type are stored.
213 If AndroidChromeBuildArchive is used, it is assumed that the --extra_src
214 is a valid Python module which contains the module-level functions
215 GetBucketName and GetArchiveDirectory.
218 def BucketName(self
):
219 return self
._extra
_src
.GetBucketName()
221 def _ZipFileName(self
, revision
, deps_patch_sha
=None):
222 """Gets the file name of a zip archive on android-chrome.
224 This returns a file name of the form build_product_<revision>.zip,
225 which is a format used by android-chrome.
228 revision: A git commit hash or other revision string.
229 deps_patch_sha: SHA1 hash of a DEPS file patch.
232 The archive file name.
235 revision
= '%s_%s' % (revision
, deps_patch_sha
)
236 return 'build_product_%s.zip' % revision
238 def FilePath(self
, revision
, deps_patch_sha
=None):
239 return '%s/%s' % (self
._ArchiveDirectory
(),
240 self
._ZipFileName
(revision
, deps_patch_sha
))
242 def _ArchiveDirectory(self
):
243 """Returns the directory name to download builds from."""
244 return self
._extra
_src
.GetArchiveDirectory()
247 def BuildIsAvailable(bucket_name
, remote_path
):
248 """Checks whether a build is currently archived at some place."""
249 logging
.info('Checking existance: gs://%s/%s' % (bucket_name
, remote_path
))
251 exists
= cloud_storage
.Exists(bucket_name
, remote_path
)
252 logging
.info('Exists? %s' % exists
)
254 except cloud_storage
.CloudStorageError
:
258 def FetchFromCloudStorage(bucket_name
, source_path
, destination_dir
):
259 """Fetches file(s) from the Google Cloud Storage.
261 As a side-effect, this prints messages to stdout about what's happening.
264 bucket_name: Google Storage bucket name.
265 source_path: Source file path.
266 destination_dir: Destination file path.
269 Local file path of downloaded file if it was downloaded. If the file does
270 not exist in the given bucket, or if there was an error while downloading,
273 target_file
= os
.path
.join(destination_dir
, os
.path
.basename(source_path
))
274 gs_url
= 'gs://%s/%s' % (bucket_name
, source_path
)
276 if cloud_storage
.Exists(bucket_name
, source_path
):
277 logging
.info('Fetching file from %s...', gs_url
)
278 cloud_storage
.Get(bucket_name
, source_path
, target_file
)
279 if os
.path
.exists(target_file
):
282 logging
.info('File %s not found in cloud storage.', gs_url
)
284 except Exception as e
:
285 logging
.warn('Exception while fetching from cloud storage: %s', e
)
286 if os
.path
.exists(target_file
):
287 os
.remove(target_file
)
291 def Unzip(file_path
, output_dir
, verbose
=True):
292 """Extracts a zip archive's contents into the given output directory.
294 This was based on ExtractZip from build/scripts/common/chromium_utils.py.
297 file_path: Path of the zip file to extract.
298 output_dir: Path to the destination directory.
299 verbose: Whether to print out what is being extracted.
302 IOError: The unzip command had a non-zero exit code.
303 RuntimeError: Failed to create the output directory.
305 _MakeDirectory(output_dir
)
307 # On Linux and Mac, we use the unzip command because it handles links and
308 # file permissions bits, so achieving this behavior is easier than with
311 # The Mac Version of unzip unfortunately does not support Zip64, whereas
312 # the python module does, so we have to fall back to the python zip module
313 # on Mac if the file size is greater than 4GB.
314 mac_zip_size_limit
= 2 ** 32 # 4GB
315 if (bisect_utils
.IsLinuxHost() or
316 (bisect_utils
.IsMacHost()
317 and os
.path
.getsize(file_path
) < mac_zip_size_limit
)):
318 unzip_command
= ['unzip', '-o']
319 _UnzipUsingCommand(unzip_command
, file_path
, output_dir
)
322 # On Windows, try to use 7z if it is installed, otherwise fall back to the
323 # Python zipfile module. If 7z is not installed, then this may fail if the
324 # zip file is larger than 512MB.
325 sevenzip_path
= r
'C:\Program Files\7-Zip\7z.exe'
326 if bisect_utils
.IsWindowsHost() and os
.path
.exists(sevenzip_path
):
327 unzip_command
= [sevenzip_path
, 'x', '-y']
328 _UnzipUsingCommand(unzip_command
, file_path
, output_dir
)
331 _UnzipUsingZipFile(file_path
, output_dir
, verbose
)
334 def _UnzipUsingCommand(unzip_command
, file_path
, output_dir
):
335 """Extracts a zip file using an external command.
338 unzip_command: An unzipping command, as a string list, without the filename.
339 file_path: Path to the zip file.
340 output_dir: The directory which the contents should be extracted to.
343 IOError: The command had a non-zero exit code.
345 absolute_filepath
= os
.path
.abspath(file_path
)
346 command
= unzip_command
+ [absolute_filepath
]
347 return_code
= _RunCommandInDirectory(output_dir
, command
)
349 _RemoveDirectoryTree(output_dir
)
350 raise IOError('Unzip failed: %s => %s' % (str(command
), return_code
))
353 def _RunCommandInDirectory(directory
, command
):
354 """Changes to a directory, runs a command, then changes back."""
355 saved_dir
= os
.getcwd()
357 return_code
= bisect_utils
.RunProcess(command
)
362 def _UnzipUsingZipFile(file_path
, output_dir
, verbose
=True):
363 """Extracts a zip file using the Python zipfile module."""
364 assert bisect_utils
.IsWindowsHost() or bisect_utils
.IsMacHost()
365 zf
= zipfile
.ZipFile(file_path
)
366 for name
in zf
.namelist():
368 print 'Extracting %s' % name
369 zf
.extract(name
, output_dir
)
370 if bisect_utils
.IsMacHost():
371 # Restore file permission bits.
372 mode
= zf
.getinfo(name
).external_attr
>> 16
373 os
.chmod(os
.path
.join(output_dir
, name
), mode
)
376 def _MakeDirectory(path
):
380 if e
.errno
!= errno
.EEXIST
:
384 def _RemoveDirectoryTree(path
):
386 if os
.path
.exists(path
):
389 if e
.errno
!= errno
.ENOENT
:
394 """Downloads and extracts a build based on the command line arguments."""
395 parser
= argparse
.ArgumentParser()
396 parser
.add_argument('builder_type')
397 parser
.add_argument('revision')
398 parser
.add_argument('output_dir')
399 parser
.add_argument('--target-arch', default
='ia32')
400 parser
.add_argument('--target-platform', default
='chromium')
401 parser
.add_argument('--deps-patch-sha')
402 args
= parser
.parse_args(argv
[1:])
404 bucket_name
, remote_path
= GetBucketAndRemotePath(
405 args
.revision
, args
.builder_type
, target_arch
=args
.target_arch
,
406 target_platform
=args
.target_platform
,
407 deps_patch_sha
=args
.deps_patch_sha
)
408 print 'Bucket name: %s, remote path: %s' % (bucket_name
, remote_path
)
410 if not BuildIsAvailable(bucket_name
, remote_path
):
411 print 'Build is not available.'
414 FetchFromCloudStorage(bucket_name
, remote_path
, args
.output_dir
)
415 print 'Build has been downloaded to and extracted in %s.' % args
.output_dir
419 if __name__
== '__main__':
420 sys
.exit(Main(sys
.argv
))