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 catapult_base
import cloud_storage
30 # Possible builder types.
33 ANDROID_CHROME_PERF_BUILDER
= 'android-chrome-perf'
35 # Maximum time in seconds to wait after posting build request to the try server.
36 MAX_MAC_BUILD_TIME
= 14400
37 MAX_WIN_BUILD_TIME
= 14400
38 MAX_LINUX_BUILD_TIME
= 14400
40 # Try server status page URLs, used to get build status.
41 PERF_TRY_SERVER_URL
= 'http://build.chromium.org/p/tryserver.chromium.perf'
42 LINUX_TRY_SERVER_URL
= 'http://build.chromium.org/p/tryserver.chromium.linux'
45 def GetBucketAndRemotePath(revision
, builder_type
=PERF_BUILDER
,
46 target_arch
='ia32', target_platform
='chromium',
47 deps_patch_sha
=None, extra_src
=None):
48 """Returns the location where a build archive is expected to be.
51 revision: Revision string, e.g. a git commit hash or SVN revision.
52 builder_type: Type of build archive.
53 target_arch: Architecture, e.g. "ia32".
54 target_platform: Platform name, e.g. "chromium" or "android".
55 deps_patch_sha: SHA1 hash which identifies a particular combination of
56 custom revisions for dependency repositories.
57 extra_src: Path to a script which can be used to modify the bisect script's
61 A pair of strings (bucket, path), where the archive is expected to be.
63 logging
.info('Getting GS URL for archive of builder "%s", "%s", "%s".',
64 builder_type
, target_arch
, target_platform
)
65 build_archive
= BuildArchive
.Create(
66 builder_type
, target_arch
=target_arch
, target_platform
=target_platform
,
68 bucket
= build_archive
.BucketName()
69 remote_path
= build_archive
.FilePath(revision
, deps_patch_sha
=deps_patch_sha
)
70 return bucket
, remote_path
73 def GetBuilderNameAndBuildTime(builder_type
=PERF_BUILDER
, target_arch
='ia32',
74 target_platform
='chromium', extra_src
=None):
75 """Gets builder bot name and build time in seconds based on platform."""
76 logging
.info('Getting builder name for builder "%s", "%s", "%s".',
77 builder_type
, target_arch
, target_platform
)
78 build_archive
= BuildArchive
.Create(
79 builder_type
, target_arch
=target_arch
, target_platform
=target_platform
,
81 return build_archive
.GetBuilderName(), build_archive
.GetBuilderBuildTime()
84 def GetBuildBotUrl(builder_type
=PERF_BUILDER
, target_arch
='ia32',
85 target_platform
='chromium', extra_src
=None):
86 """Gets buildbot URL for a given builder type."""
87 logging
.info('Getting buildbot URL for "%s", "%s", "%s".',
88 builder_type
, target_arch
, target_platform
)
89 build_archive
= BuildArchive
.Create(
90 builder_type
, target_arch
=target_arch
, target_platform
=target_platform
,
92 return build_archive
.GetBuildBotUrl()
95 class BuildArchive(object):
96 """Represents a place where builds of some type are stored.
98 There are two pieces of information required to locate a file in Google
99 Cloud Storage, bucket name and file path. Subclasses of this class contain
100 specific logic about which bucket names and paths should be used to fetch
105 def Create(builder_type
, target_arch
='ia32', target_platform
='chromium',
107 if builder_type
== PERF_BUILDER
:
108 return PerfBuildArchive(target_arch
, target_platform
)
109 if builder_type
== FULL_BUILDER
:
110 return FullBuildArchive(target_arch
, target_platform
)
111 if builder_type
== ANDROID_CHROME_PERF_BUILDER
:
113 # Load and initialize a module in extra source file and
114 # return its module object to access android-chrome specific data.
115 loaded_extra_src
= bisect_utils
.LoadExtraSrc(extra_src
)
116 return AndroidChromeBuildArchive(
117 target_arch
, target_platform
, loaded_extra_src
)
118 except (IOError, TypeError, ImportError):
119 raise RuntimeError('Invalid or missing --extra_src. [%s]' % extra_src
)
120 raise NotImplementedError('Builder type "%s" not supported.' % builder_type
)
122 def __init__(self
, target_arch
='ia32', target_platform
='chromium',
124 self
._extra
_src
= extra_src
125 if bisect_utils
.IsLinuxHost() and target_platform
== 'android':
126 if target_arch
== 'arm64':
127 self
._platform
= 'android_arm64'
129 self
._platform
= 'android'
130 elif bisect_utils
.IsLinuxHost() and target_platform
== 'android-chrome':
131 self
._platform
= 'android-chrome'
132 elif bisect_utils
.IsLinuxHost():
133 self
._platform
= 'linux'
134 elif bisect_utils
.IsMacHost():
135 self
._platform
= 'mac'
136 elif bisect_utils
.Is64BitWindows() and target_arch
== 'x64':
137 self
._platform
= 'win64'
138 elif bisect_utils
.IsWindowsHost():
139 self
._platform
= 'win'
141 raise NotImplementedError('Unknown platform "%s".' % sys
.platform
)
143 def BucketName(self
):
144 raise NotImplementedError()
146 def FilePath(self
, revision
, deps_patch_sha
=None):
147 """Returns the remote file path to download a build from.
150 revision: A Chromium revision; this could be a git commit hash or
151 commit position or SVN revision number.
152 deps_patch_sha: The SHA1 hash of a patch to the DEPS file, which
153 uniquely identifies a change to use a particular revision of
157 A file path, which not does not include a bucket name.
159 raise NotImplementedError()
161 def _ZipFileName(self
, revision
, deps_patch_sha
=None):
162 """Gets the file name of a zip archive for a particular revision.
164 This returns a file name of the form full-build-<platform>_<revision>.zip,
165 which is a format used by multiple types of builders that store archives.
168 revision: A git commit hash or other revision string.
169 deps_patch_sha: SHA1 hash of a DEPS file patch.
172 The archive file name.
174 base_name
= 'full-build-%s' % self
._PlatformName
()
176 revision
= '%s_%s' % (revision
, deps_patch_sha
)
177 return '%s_%s.zip' % (base_name
, revision
)
179 def _PlatformName(self
):
180 """Return a string to be used in paths for the platform."""
181 if self
._platform
in ('win', 'win64'):
182 # Build archive for win64 is still stored with "win32" in the name.
184 if self
._platform
in ('linux', 'android', 'android_arm64'):
185 # Android builds are also stored with "linux" in the name.
187 if self
._platform
== 'mac':
189 raise NotImplementedError('Unknown platform "%s".' % sys
.platform
)
191 def GetBuilderName(self
):
192 raise NotImplementedError()
194 def GetBuilderBuildTime(self
):
195 """Returns the time to wait for a build after requesting one."""
196 if self
._platform
in ('win', 'win64'):
197 return MAX_WIN_BUILD_TIME
198 if self
._platform
in ('linux', 'android',
199 'android_arm64', 'android-chrome'):
200 return MAX_LINUX_BUILD_TIME
201 if self
._platform
== 'mac':
202 return MAX_MAC_BUILD_TIME
203 raise NotImplementedError('Unsupported Platform "%s".' % sys
.platform
)
205 def GetBuildBotUrl(self
):
206 raise NotImplementedError()
209 class PerfBuildArchive(BuildArchive
):
211 def BucketName(self
):
214 def FilePath(self
, revision
, deps_patch_sha
=None):
215 return '%s/%s' % (self
._ArchiveDirectory
(),
216 self
._ZipFileName
(revision
, deps_patch_sha
))
218 def _ArchiveDirectory(self
):
219 """Returns the directory name to download builds from."""
220 platform_to_directory
= {
221 'android': 'android_perf_rel',
222 'android_arm64': 'android_perf_rel_arm64',
223 'linux': 'Linux Builder',
224 'mac': 'Mac Builder',
225 'win64': 'Win x64 Builder',
226 'win': 'Win Builder',
228 assert self
._platform
in platform_to_directory
229 return platform_to_directory
.get(self
._platform
)
231 def GetBuilderName(self
):
232 """Gets builder bot name based on platform."""
233 if self
._platform
== 'win64':
234 return 'win_x64_perf_bisect_builder'
235 elif self
._platform
== 'win':
236 return 'win_perf_bisect_builder'
237 elif self
._platform
== 'linux':
238 return 'linux_perf_bisect_builder'
239 elif self
._platform
== 'android':
240 return 'android_perf_bisect_builder'
241 elif self
._platform
== 'android_arm64':
242 return 'android_arm64_perf_bisect_builder'
243 elif self
._platform
== 'mac':
244 return 'mac_perf_bisect_builder'
245 raise NotImplementedError('Unsupported platform "%s".' % sys
.platform
)
247 def GetBuildBotUrl(self
):
248 """Returns buildbot URL for fetching build info."""
249 return PERF_TRY_SERVER_URL
252 class FullBuildArchive(BuildArchive
):
254 def BucketName(self
):
255 platform_to_bucket
= {
256 'android': 'chromium-android',
257 'linux': 'chromium-linux-archive',
258 'mac': 'chromium-mac-archive',
259 'win64': 'chromium-win-archive',
260 'win': 'chromium-win-archive',
262 assert self
._platform
in platform_to_bucket
263 return platform_to_bucket
.get(self
._platform
)
265 def FilePath(self
, revision
, deps_patch_sha
=None):
266 return '%s/%s' % (self
._ArchiveDirectory
(),
267 self
._ZipFileName
(revision
, deps_patch_sha
))
269 def _ArchiveDirectory(self
):
270 """Returns the remote directory to download builds from."""
271 platform_to_directory
= {
272 'android': 'android_main_rel',
273 'linux': 'chromium.linux/Linux Builder',
274 'mac': 'chromium.mac/Mac Builder',
275 'win64': 'chromium.win/Win x64 Builder',
276 'win': 'chromium.win/Win Builder',
278 assert self
._platform
in platform_to_directory
279 return platform_to_directory
.get(self
._platform
)
281 def GetBuilderName(self
):
282 """Gets builder bot name based on platform."""
283 if self
._platform
== 'linux':
284 return 'linux_full_bisect_builder'
285 raise NotImplementedError('Unsupported platform "%s".' % sys
.platform
)
287 def GetBuildBotUrl(self
):
288 """Returns buildbot URL for fetching build info."""
289 return LINUX_TRY_SERVER_URL
292 class AndroidChromeBuildArchive(BuildArchive
):
293 """Represents a place where builds of android-chrome type are stored.
295 If AndroidChromeBuildArchive is used, it is assumed that the --extra_src
296 is a valid Python module which contains the module-level functions
297 GetBucketName and GetArchiveDirectory.
300 def BucketName(self
):
301 return self
._extra
_src
.GetBucketName()
303 def _ZipFileName(self
, revision
, deps_patch_sha
=None):
304 """Gets the file name of a zip archive on android-chrome.
306 This returns a file name of the form build_product_<revision>.zip,
307 which is a format used by android-chrome.
310 revision: A git commit hash or other revision string.
311 deps_patch_sha: SHA1 hash of a DEPS file patch.
314 The archive file name.
317 revision
= '%s_%s' % (revision
, deps_patch_sha
)
318 return 'build_product_%s.zip' % revision
320 def FilePath(self
, revision
, deps_patch_sha
=None):
321 return '%s/%s' % (self
._ArchiveDirectory
(),
322 self
._ZipFileName
(revision
, deps_patch_sha
))
324 def _ArchiveDirectory(self
):
325 """Returns the directory name to download builds from."""
326 return self
._extra
_src
.GetArchiveDirectory()
328 def GetBuilderName(self
):
329 """Returns the builder name extra source."""
330 return self
._extra
_src
.GetBuilderName()
332 def GetBuildBotUrl(self
):
333 """Returns buildbot URL for fetching build info."""
334 return self
._extra
_src
.GetBuildBotUrl()
337 def BuildIsAvailable(bucket_name
, remote_path
):
338 """Checks whether a build is currently archived at some place."""
339 logging
.info('Checking existence: gs://%s/%s' % (bucket_name
, remote_path
))
341 exists
= cloud_storage
.Exists(bucket_name
, remote_path
)
342 logging
.info('Exists? %s' % exists
)
344 except cloud_storage
.CloudStorageError
:
348 def FetchFromCloudStorage(bucket_name
, source_path
, destination_dir
):
349 """Fetches file(s) from the Google Cloud Storage.
351 As a side-effect, this prints messages to stdout about what's happening.
354 bucket_name: Google Storage bucket name.
355 source_path: Source file path.
356 destination_dir: Destination file path.
359 Local file path of downloaded file if it was downloaded. If the file does
360 not exist in the given bucket, or if there was an error while downloading,
363 target_file
= os
.path
.join(destination_dir
, os
.path
.basename(source_path
))
364 gs_url
= 'gs://%s/%s' % (bucket_name
, source_path
)
366 if cloud_storage
.Exists(bucket_name
, source_path
):
367 logging
.info('Fetching file from %s...', gs_url
)
368 cloud_storage
.Get(bucket_name
, source_path
, target_file
)
369 if os
.path
.exists(target_file
):
372 logging
.info('File %s not found in cloud storage.', gs_url
)
374 except Exception as e
:
375 logging
.warn('Exception while fetching from cloud storage: %s', e
)
376 if os
.path
.exists(target_file
):
377 os
.remove(target_file
)
381 def Unzip(file_path
, output_dir
, verbose
=True):
382 """Extracts a zip archive's contents into the given output directory.
384 This was based on ExtractZip from build/scripts/common/chromium_utils.py.
387 file_path: Path of the zip file to extract.
388 output_dir: Path to the destination directory.
389 verbose: Whether to print out what is being extracted.
392 IOError: The unzip command had a non-zero exit code.
393 RuntimeError: Failed to create the output directory.
395 _MakeDirectory(output_dir
)
397 # On Linux and Mac, we use the unzip command because it handles links and
398 # file permissions bits, so achieving this behavior is easier than with
401 # The Mac Version of unzip unfortunately does not support Zip64, whereas
402 # the python module does, so we have to fall back to the python zip module
403 # on Mac if the file size is greater than 4GB.
404 mac_zip_size_limit
= 2 ** 32 # 4GB
405 if (bisect_utils
.IsLinuxHost() or
406 (bisect_utils
.IsMacHost()
407 and os
.path
.getsize(file_path
) < mac_zip_size_limit
)):
408 unzip_command
= ['unzip', '-o']
409 _UnzipUsingCommand(unzip_command
, file_path
, output_dir
)
412 # On Windows, try to use 7z if it is installed, otherwise fall back to the
413 # Python zipfile module. If 7z is not installed, then this may fail if the
414 # zip file is larger than 512MB.
415 sevenzip_path
= r
'C:\Program Files\7-Zip\7z.exe'
416 if bisect_utils
.IsWindowsHost() and os
.path
.exists(sevenzip_path
):
417 unzip_command
= [sevenzip_path
, 'x', '-y']
418 _UnzipUsingCommand(unzip_command
, file_path
, output_dir
)
421 _UnzipUsingZipFile(file_path
, output_dir
, verbose
)
424 def _UnzipUsingCommand(unzip_command
, file_path
, output_dir
):
425 """Extracts a zip file using an external command.
428 unzip_command: An unzipping command, as a string list, without the filename.
429 file_path: Path to the zip file.
430 output_dir: The directory which the contents should be extracted to.
433 IOError: The command had a non-zero exit code.
435 absolute_filepath
= os
.path
.abspath(file_path
)
436 command
= unzip_command
+ [absolute_filepath
]
437 return_code
= _RunCommandInDirectory(output_dir
, command
)
439 _RemoveDirectoryTree(output_dir
)
440 raise IOError('Unzip failed: %s => %s' % (str(command
), return_code
))
443 def _RunCommandInDirectory(directory
, command
):
444 """Changes to a directory, runs a command, then changes back."""
445 saved_dir
= os
.getcwd()
447 return_code
= bisect_utils
.RunProcess(command
)
452 def _UnzipUsingZipFile(file_path
, output_dir
, verbose
=True):
453 """Extracts a zip file using the Python zipfile module."""
454 assert bisect_utils
.IsWindowsHost() or bisect_utils
.IsMacHost()
455 zf
= zipfile
.ZipFile(file_path
)
456 for name
in zf
.namelist():
458 print 'Extracting %s' % name
459 zf
.extract(name
, output_dir
)
460 if bisect_utils
.IsMacHost():
461 # Restore file permission bits.
462 mode
= zf
.getinfo(name
).external_attr
>> 16
463 os
.chmod(os
.path
.join(output_dir
, name
), mode
)
466 def _MakeDirectory(path
):
470 if e
.errno
!= errno
.EEXIST
:
474 def _RemoveDirectoryTree(path
):
476 if os
.path
.exists(path
):
479 if e
.errno
!= errno
.ENOENT
:
484 """Downloads and extracts a build based on the command line arguments."""
485 parser
= argparse
.ArgumentParser()
486 parser
.add_argument('builder_type')
487 parser
.add_argument('revision')
488 parser
.add_argument('output_dir')
489 parser
.add_argument('--target-arch', default
='ia32')
490 parser
.add_argument('--target-platform', default
='chromium')
491 parser
.add_argument('--deps-patch-sha')
492 args
= parser
.parse_args(argv
[1:])
494 bucket_name
, remote_path
= GetBucketAndRemotePath(
495 args
.revision
, args
.builder_type
, target_arch
=args
.target_arch
,
496 target_platform
=args
.target_platform
,
497 deps_patch_sha
=args
.deps_patch_sha
)
498 print 'Bucket name: %s, remote path: %s' % (bucket_name
, remote_path
)
500 if not BuildIsAvailable(bucket_name
, remote_path
):
501 print 'Build is not available.'
504 FetchFromCloudStorage(bucket_name
, remote_path
, args
.output_dir
)
505 print 'Build has been downloaded to and extracted in %s.' % args
.output_dir
509 if __name__
== '__main__':
510 sys
.exit(Main(sys
.argv
))