1#!/usr/bin/env python 2# Copyright 2015 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6''' 7Script to help uploading and downloading the Google Play services library to 8and from a Google Cloud storage. 9''' 10 11import argparse 12import logging 13import os 14import re 15import shutil 16import sys 17import tempfile 18import zipfile 19 20sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir)) 21import devil_chromium 22from devil.utils import cmd_helper 23from play_services import utils 24from pylib import constants 25from pylib.constants import host_paths 26from pylib.utils import logging_utils 27 28sys.path.append(os.path.join(host_paths.DIR_SOURCE_ROOT, 'build')) 29import find_depot_tools # pylint: disable=import-error,unused-import 30import breakpad 31import download_from_google_storage 32import upload_to_google_storage 33 34 35# Directory where the SHA1 files for the zip and the license are stored 36# It should be managed by git to provided information about new versions. 37SHA1_DIRECTORY = os.path.join(host_paths.DIR_SOURCE_ROOT, 'build', 'android', 38 'play_services') 39 40# Default bucket used for storing the files. 41GMS_CLOUD_STORAGE = 'chromium-android-tools/play-services' 42 43# Path to the default configuration file. It exposes the currently installed 44# version of the library in a human readable way. 45CONFIG_DEFAULT_PATH = os.path.join(host_paths.DIR_SOURCE_ROOT, 'build', 46 'android', 'play_services', 'config.json') 47 48LICENSE_FILE_NAME = 'LICENSE' 49ZIP_FILE_NAME = 'google_play_services_library.zip' 50GMS_PACKAGE_ID = 'extra-google-google_play_services' # used by sdk manager 51 52LICENSE_PATTERN = re.compile(r'^Pkg\.License=(?P<text>.*)$', re.MULTILINE) 53 54 55def main(raw_args): 56 parser = argparse.ArgumentParser( 57 description=__doc__ + 'Please see the subcommand help for more details.', 58 formatter_class=utils.DefaultsRawHelpFormatter) 59 subparsers = parser.add_subparsers(title='commands') 60 61 # Download arguments 62 parser_download = subparsers.add_parser( 63 'download', 64 help='download the library from the cloud storage', 65 description=Download.__doc__, 66 formatter_class=utils.DefaultsRawHelpFormatter) 67 parser_download.set_defaults(func=Download) 68 AddBasicArguments(parser_download) 69 AddBucketArguments(parser_download) 70 71 # SDK Update arguments 72 parser_sdk = subparsers.add_parser( 73 'sdk', 74 help='get the latest Google Play services SDK using Android SDK Manager', 75 description=UpdateSdk.__doc__, 76 formatter_class=utils.DefaultsRawHelpFormatter) 77 parser_sdk.set_defaults(func=UpdateSdk) 78 AddBasicArguments(parser_sdk) 79 80 # Upload arguments 81 parser_upload = subparsers.add_parser( 82 'upload', 83 help='upload the library to the cloud storage', 84 description=Upload.__doc__, 85 formatter_class=utils.DefaultsRawHelpFormatter) 86 87 parser_upload.add_argument('--skip-git', 88 action='store_true', 89 help="don't commit the changes at the end") 90 parser_upload.set_defaults(func=Upload) 91 AddBasicArguments(parser_upload) 92 AddBucketArguments(parser_upload) 93 94 args = parser.parse_args(raw_args) 95 if args.verbose: 96 logging.basicConfig(level=logging.DEBUG) 97 logging_utils.ColorStreamHandler.MakeDefault(not _IsBotEnvironment()) 98 devil_chromium.Initialize() 99 return args.func(args) 100 101 102def AddBasicArguments(parser): 103 ''' 104 Defines the common arguments on subparser rather than the main one. This 105 allows to put arguments after the command: `foo.py upload --debug --force` 106 instead of `foo.py --debug upload --force` 107 ''' 108 109 parser.add_argument('--sdk-root', 110 help='base path to the Android SDK tools root', 111 default=constants.ANDROID_SDK_ROOT) 112 113 parser.add_argument('-v', '--verbose', 114 action='store_true', 115 help='print debug information') 116 117 118def AddBucketArguments(parser): 119 parser.add_argument('--bucket', 120 help='name of the bucket where the files are stored', 121 default=GMS_CLOUD_STORAGE) 122 123 parser.add_argument('--config', 124 help='JSON Configuration file', 125 default=CONFIG_DEFAULT_PATH) 126 127 parser.add_argument('--dry-run', 128 action='store_true', 129 help=('run the script in dry run mode. Files will be ' 130 'copied to a local directory instead of the ' 131 'cloud storage. The bucket name will be as path ' 132 'to that directory relative to the repository ' 133 'root.')) 134 135 parser.add_argument('-f', '--force', 136 action='store_true', 137 help='run even if the library is already up to date') 138 139 140def Download(args): 141 ''' 142 Downloads the Google Play services library from a Google Cloud Storage bucket 143 and installs it to 144 //third_party/android_tools/sdk/extras/google/google_play_services. 145 146 A license check will be made, and the user might have to accept the license 147 if that has not been done before. 148 ''' 149 150 if not os.path.isdir(args.sdk_root): 151 logging.debug('Did not find the Android SDK root directory at "%s".', 152 args.sdk_root) 153 if not args.force: 154 logging.info('Skipping, not on an android checkout.') 155 return 0 156 157 config = utils.ConfigParser(args.config) 158 paths = PlayServicesPaths(args.sdk_root, config.version_xml_path) 159 160 if os.path.isdir(paths.package) and not os.access(paths.package, os.W_OK): 161 logging.error('Failed updating the Google Play Services library. ' 162 'The location is not writable. Please remove the ' 163 'directory (%s) and try again.', paths.package) 164 return -2 165 166 new_lib_zip_sha1 = os.path.join(SHA1_DIRECTORY, ZIP_FILE_NAME + '.sha1') 167 168 logging.debug('Comparing zip hashes: %s and %s', new_lib_zip_sha1, 169 paths.lib_zip_sha1) 170 if utils.FileEquals(new_lib_zip_sha1, paths.lib_zip_sha1) and not args.force: 171 logging.info('Skipping, the Google Play services library is up to date.') 172 return 0 173 174 bucket_path = _VerifyBucketPathFormat(args.bucket, 175 config.version_number, 176 args.dry_run) 177 178 tmp_root = tempfile.mkdtemp() 179 try: 180 # setup the destination directory 181 if not os.path.isdir(paths.package): 182 os.makedirs(paths.package) 183 184 # download license file from bucket/{version_number}/license.sha1 185 new_license = os.path.join(tmp_root, LICENSE_FILE_NAME) 186 187 license_sha1 = os.path.join(SHA1_DIRECTORY, LICENSE_FILE_NAME + '.sha1') 188 _DownloadFromBucket(bucket_path, license_sha1, new_license, 189 args.verbose, args.dry_run) 190 191 if (not _IsBotEnvironment() and 192 not _CheckLicenseAgreement(new_license, paths.license, 193 config.version_number)): 194 logging.warning('Your version of the Google Play services library is ' 195 'not up to date. You might run into issues building ' 196 'or running the app. Please run `%s download` to ' 197 'retry downloading it.', __file__) 198 return 0 199 200 new_lib_zip = os.path.join(tmp_root, ZIP_FILE_NAME) 201 _DownloadFromBucket(bucket_path, new_lib_zip_sha1, new_lib_zip, 202 args.verbose, args.dry_run) 203 204 try: 205 # We remove the current version of the Google Play services SDK. 206 if os.path.exists(paths.package): 207 shutil.rmtree(paths.package) 208 os.makedirs(paths.package) 209 210 logging.debug('Extracting the library to %s', paths.lib) 211 with zipfile.ZipFile(new_lib_zip, "r") as new_lib_zip_file: 212 new_lib_zip_file.extractall(paths.lib) 213 214 logging.debug('Copying %s to %s', new_license, paths.license) 215 shutil.copy(new_license, paths.license) 216 217 logging.debug('Copying %s to %s', new_lib_zip_sha1, paths.lib_zip_sha1) 218 shutil.copy(new_lib_zip_sha1, paths.lib_zip_sha1) 219 220 logging.info('Update complete.') 221 222 except Exception as e: # pylint: disable=broad-except 223 logging.error('Failed updating the Google Play Services library. ' 224 'An error occurred while installing the new version in ' 225 'the SDK directory: %s ', e) 226 return -3 227 finally: 228 shutil.rmtree(tmp_root) 229 230 return 0 231 232 233def UpdateSdk(args): 234 ''' 235 Uses the Android SDK Manager to download the latest Google Play services SDK 236 locally. Its usual installation path is 237 //third_party/android_tools/sdk/extras/google/google_play_services 238 ''' 239 240 # This should function should not run on bots and could fail for many user 241 # and setup related reasons. Also, exceptions here are not caught, so we 242 # disable breakpad to avoid spamming the logs. 243 breakpad.IS_ENABLED = False 244 245 sdk_manager = os.path.join(args.sdk_root, 'tools', 'android') 246 cmd = [sdk_manager, 'update', 'sdk', '--no-ui', '--filter', GMS_PACKAGE_ID] 247 cmd_helper.Call(cmd) 248 # If no update is needed, it still returns successfully so we just do nothing 249 250 return 0 251 252 253def Upload(args): 254 ''' 255 Uploads the library from the local Google Play services SDK to a Google Cloud 256 storage bucket. 257 258 By default, a local commit will be made at the end of the operation. 259 ''' 260 261 # This should function should not run on bots and could fail for many user 262 # and setup related reasons. Also, exceptions here are not caught, so we 263 # disable breakpad to avoid spamming the logs. 264 breakpad.IS_ENABLED = False 265 266 config = utils.ConfigParser(args.config) 267 paths = PlayServicesPaths(args.sdk_root, config.version_xml_path) 268 269 if not args.skip_git and utils.IsRepoDirty(host_paths.DIR_SOURCE_ROOT): 270 logging.error('The repo is dirty. Please commit or stash your changes.') 271 return -1 272 273 new_version_number = utils.GetVersionNumberFromLibraryResources( 274 paths.version_xml) 275 logging.debug('comparing versions: new=%d, old=%s', 276 new_version_number, config.version_number) 277 if new_version_number <= config.version_number and not args.force: 278 logging.info('The checked in version of the library is already the latest ' 279 'one. No update is needed. Please rerun with --force to skip ' 280 'this check.') 281 return 0 282 283 tmp_root = tempfile.mkdtemp() 284 try: 285 new_lib_zip = os.path.join(tmp_root, ZIP_FILE_NAME) 286 new_license = os.path.join(tmp_root, LICENSE_FILE_NAME) 287 288 # need to strip '.zip' from the file name here 289 shutil.make_archive(new_lib_zip[:-4], 'zip', paths.lib) 290 _ExtractLicenseFile(new_license, paths.source_prop) 291 292 bucket_path = _VerifyBucketPathFormat(args.bucket, new_version_number, 293 args.dry_run) 294 files_to_upload = [new_lib_zip, new_license] 295 logging.debug('Uploading %s to %s', files_to_upload, bucket_path) 296 _UploadToBucket(bucket_path, files_to_upload, args.dry_run) 297 298 new_lib_zip_sha1 = os.path.join(SHA1_DIRECTORY, 299 ZIP_FILE_NAME + '.sha1') 300 new_license_sha1 = os.path.join(SHA1_DIRECTORY, 301 LICENSE_FILE_NAME + '.sha1') 302 shutil.copy(new_lib_zip + '.sha1', new_lib_zip_sha1) 303 shutil.copy(new_license + '.sha1', new_license_sha1) 304 finally: 305 shutil.rmtree(tmp_root) 306 307 config.UpdateVersionNumber(new_version_number) 308 309 if not args.skip_git: 310 commit_message = ('Update the Google Play services dependency to %s\n' 311 '\n') % new_version_number 312 utils.MakeLocalCommit(host_paths.DIR_SOURCE_ROOT, 313 [new_lib_zip_sha1, new_license_sha1, config.path], 314 commit_message) 315 316 return 0 317 318 319def _DownloadFromBucket(bucket_path, sha1_file, destination, verbose, 320 is_dry_run): 321 '''Downloads the file designated by the provided sha1 from a cloud bucket.''' 322 323 download_from_google_storage.download_from_google_storage( 324 input_filename=sha1_file, 325 base_url=bucket_path, 326 gsutil=_InitGsutil(is_dry_run), 327 num_threads=1, 328 directory=None, 329 recursive=False, 330 force=False, 331 output=destination, 332 ignore_errors=False, 333 sha1_file=sha1_file, 334 verbose=verbose, 335 auto_platform=True, 336 extract=False) 337 338 339def _UploadToBucket(bucket_path, files_to_upload, is_dry_run): 340 '''Uploads the files designated by the provided paths to a cloud bucket. ''' 341 342 upload_to_google_storage.upload_to_google_storage( 343 input_filenames=files_to_upload, 344 base_url=bucket_path, 345 gsutil=_InitGsutil(is_dry_run), 346 force=False, 347 use_md5=False, 348 num_threads=1, 349 skip_hashing=False, 350 gzip=None) 351 352 353def _InitGsutil(is_dry_run): 354 '''Initialize the Gsutil object as regular or dummy version for dry runs. ''' 355 356 if is_dry_run: 357 return DummyGsutil() 358 else: 359 return download_from_google_storage.Gsutil( 360 download_from_google_storage.GSUTIL_DEFAULT_PATH) 361 362 363def _ExtractLicenseFile(license_path, prop_file_path): 364 with open(prop_file_path, 'r') as prop_file: 365 prop_file_content = prop_file.read() 366 367 match = LICENSE_PATTERN.search(prop_file_content) 368 if not match: 369 raise AttributeError('The license was not found in ' + 370 os.path.abspath(prop_file_path)) 371 372 with open(license_path, 'w') as license_file: 373 license_file.write(match.group('text')) 374 375 376def _CheckLicenseAgreement(expected_license_path, actual_license_path, 377 version_number): 378 ''' 379 Checks that the new license is the one already accepted by the user. If it 380 isn't, it prompts the user to accept it. Returns whether the expected license 381 has been accepted. 382 ''' 383 384 if utils.FileEquals(expected_license_path, actual_license_path): 385 return True 386 387 with open(expected_license_path) as license_file: 388 # Uses plain print rather than logging to make sure this is not formatted 389 # by the logger. 390 print ('Updating the Google Play services SDK to ' 391 'version %d.' % version_number) 392 393 # The output is buffered when running as part of gclient hooks. We split 394 # the text here and flush is explicitly to avoid having part of it dropped 395 # out. 396 # Note: text contains *escaped* new lines, so we split by '\\n', not '\n'. 397 for license_part in license_file.read().split('\\n'): 398 print license_part 399 sys.stdout.flush() 400 401 # Need to put the prompt on a separate line otherwise the gclient hook buffer 402 # only prints it after we received an input. 403 print 'Do you accept the license? [y/n]: ' 404 sys.stdout.flush() 405 return raw_input('> ') in ('Y', 'y') 406 407 408def _IsBotEnvironment(): 409 return bool(os.environ.get('CHROME_HEADLESS')) 410 411 412def _VerifyBucketPathFormat(bucket_name, version_number, is_dry_run): 413 ''' 414 Formats and checks the download/upload path depending on whether we are 415 running in dry run mode or not. Returns a supposedly safe path to use with 416 Gsutil. 417 ''' 418 419 if is_dry_run: 420 bucket_path = os.path.abspath(os.path.join(bucket_name, 421 str(version_number))) 422 if not os.path.isdir(bucket_path): 423 os.makedirs(bucket_path) 424 else: 425 if bucket_name.startswith('gs://'): 426 # We enforce the syntax without gs:// for consistency with the standalone 427 # download/upload scripts and to make dry run transition easier. 428 raise AttributeError('Please provide the bucket name without the gs:// ' 429 'prefix (e.g. %s)' % GMS_CLOUD_STORAGE) 430 bucket_path = 'gs://%s/%d' % (bucket_name, version_number) 431 432 return bucket_path 433 434 435class PlayServicesPaths(object): 436 ''' 437 Describes the different paths to be used in the update process. 438 439 Filesystem hierarchy | Exposed property / notes 440 ---------------------------------------------------|------------------------- 441 [sdk_root] | sdk_root / (1) 442 +- extras | 443 +- google | 444 +- google_play_services | package / (2) 445 +- source.properties | source_prop / (3) 446 +- LICENSE | license / (4) 447 +- google_play_services_library.zip.sha1 | lib_zip_sha1 / (5) 448 +- libproject | 449 +- google-play-services_lib | lib / (6) 450 +- res | 451 +- values | 452 +- version.xml | version_xml (7) 453 454 Notes: 455 456 1. sdk_root: Path provided as a parameter to the script (--sdk_root) 457 2. package: This directory contains the Google Play services SDK itself. 458 When downloaded via the Android SDK manager, it will contain, 459 documentation, samples and other files in addition to the library. When 460 the update script downloads the library from our cloud storage, it is 461 cleared. 462 3. source_prop: File created by the Android SDK manager that contains 463 the package information, such as the version info and the license. 464 4. license: File created by the update script. Contains the license accepted 465 by the user. 466 5. lib_zip_sha1: sha1 of the library zip that has been installed by the 467 update script. It is compared with the one required by the config file to 468 check if an update is necessary. 469 6. lib: Contains the library itself: jar and resources. This is what is 470 downloaded from the cloud storage. 471 7. version_xml: File that contains the exact Google Play services library 472 version, the one that we track. The version looks like 811500, is used in 473 the code and the on-device APK, as opposed to the SDK package version 474 which looks like 27.0.0 and is used only by the Android SDK manager. 475 476 ''' 477 478 def __init__(self, sdk_root, version_xml_path): 479 relative_package = os.path.join('extras', 'google', 'google_play_services') 480 relative_lib = os.path.join(relative_package, 'libproject', 481 'google-play-services_lib') 482 self.sdk_root = sdk_root 483 484 self.package = os.path.join(sdk_root, relative_package) 485 self.lib_zip_sha1 = os.path.join(self.package, ZIP_FILE_NAME + '.sha1') 486 self.license = os.path.join(self.package, LICENSE_FILE_NAME) 487 self.source_prop = os.path.join(self.package, 'source.properties') 488 489 self.lib = os.path.join(sdk_root, relative_lib) 490 self.version_xml = os.path.join(self.lib, version_xml_path) 491 492 493class DummyGsutil(download_from_google_storage.Gsutil): 494 ''' 495 Class that replaces Gsutil to use a local directory instead of an online 496 bucket. It relies on the fact that Gsutil commands are very similar to shell 497 ones, so for the ones used here (ls, cp), it works to just use them with a 498 local directory. 499 ''' 500 501 def __init__(self): 502 super(DummyGsutil, self).__init__( 503 download_from_google_storage.GSUTIL_DEFAULT_PATH) 504 505 def call(self, *args): 506 logging.debug('Calling command "%s"', str(args)) 507 return cmd_helper.GetCmdStatusOutputAndError(args) 508 509 def check_call(self, *args): 510 logging.debug('Calling command "%s"', str(args)) 511 return cmd_helper.GetCmdStatusOutputAndError(args) 512 513 514if __name__ == '__main__': 515 sys.exit(main(sys.argv[1:])) 516