1#!/usr/bin/env python 2 3# Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 4# 5# Use of this source code is governed by a BSD-style license 6# that can be found in the LICENSE file in the root of the source 7# tree. An additional intellectual property rights grant can be found 8# in the file PATENTS. All contributing project authors may 9# be found in the AUTHORS file in the root of the source tree. 10 11"""Script for publishing WebRTC AAR on Bintray. 12 13Set BINTRAY_USER and BINTRAY_API_KEY environment variables before running 14this script for authentication. 15""" 16 17import argparse 18import json 19import logging 20import os 21import re 22import shutil 23import subprocess 24import sys 25import tempfile 26import time 27 28 29SCRIPT_DIR = os.path.dirname(os.path.realpath(sys.argv[0])) 30CHECKOUT_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, os.pardir, os.pardir)) 31 32sys.path.append(os.path.join(CHECKOUT_ROOT, 'third_party')) 33import requests 34import jinja2 35 36sys.path.append(os.path.join(CHECKOUT_ROOT, 'tools_webrtc')) 37from android.build_aar import BuildAar 38 39 40ARCHS = ['armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'] 41MAVEN_REPOSITORY = 'https://google.bintray.com/webrtc' 42API = 'https://api.bintray.com' 43PACKAGE_PATH = 'google/webrtc/google-webrtc' 44CONTENT_API = API + '/content/' + PACKAGE_PATH 45PACKAGES_API = API + '/packages/' + PACKAGE_PATH 46GROUP_ID = 'org/webrtc' 47ARTIFACT_ID = 'google-webrtc' 48COMMIT_POSITION_REGEX = r'^Cr-Commit-Position: refs/heads/master@{#(\d+)}$' 49API_TIMEOUT_SECONDS = 10.0 50UPLOAD_TRIES = 3 51# The sleep time is increased exponentially. 52UPLOAD_RETRY_BASE_SLEEP_SECONDS = 2.0 53GRADLEW_BIN = os.path.join(CHECKOUT_ROOT, 54 'examples/androidtests/third_party/gradle/gradlew') 55ADB_BIN = os.path.join(CHECKOUT_ROOT, 56 'third_party/android_sdk/public/platform-tools/adb') 57AAR_PROJECT_DIR = os.path.join(CHECKOUT_ROOT, 'examples/aarproject') 58AAR_PROJECT_GRADLE = os.path.join(AAR_PROJECT_DIR, 'build.gradle') 59AAR_PROJECT_APP_GRADLE = os.path.join(AAR_PROJECT_DIR, 'app', 'build.gradle') 60AAR_PROJECT_DEPENDENCY = "implementation 'org.webrtc:google-webrtc:1.0.+'" 61AAR_PROJECT_VERSION_DEPENDENCY = "implementation 'org.webrtc:google-webrtc:%s'" 62 63 64def _ParseArgs(): 65 parser = argparse.ArgumentParser(description='Releases WebRTC on Bintray.') 66 parser.add_argument('--use-goma', action='store_true', default=False, 67 help='Use goma.') 68 parser.add_argument('--skip-tests', action='store_true', default=False, 69 help='Skips running the tests.') 70 parser.add_argument('--publish', action='store_true', default=False, 71 help='Automatically publishes the library if the tests pass.') 72 parser.add_argument('--build-dir', default=None, 73 help='Temporary directory to store the build files. If not specified, ' 74 'a new directory will be created.') 75 parser.add_argument('--verbose', action='store_true', default=False, 76 help='Debug logging.') 77 return parser.parse_args() 78 79 80def _GetCommitHash(): 81 commit_hash = subprocess.check_output( 82 ['git', 'rev-parse', 'HEAD'], cwd=CHECKOUT_ROOT).strip() 83 return commit_hash 84 85 86def _GetCommitPos(): 87 commit_message = subprocess.check_output( 88 ['git', 'rev-list', '--format=%B', '--max-count=1', 'HEAD'], 89 cwd=CHECKOUT_ROOT) 90 commit_pos_match = re.search( 91 COMMIT_POSITION_REGEX, commit_message, re.MULTILINE) 92 if not commit_pos_match: 93 raise Exception('Commit position not found in the commit message: %s' 94 % commit_message) 95 return commit_pos_match.group(1) 96 97 98def _UploadFile(user, password, filename, version, target_file): 99# URL is of format: 100 # <repository_api>/<version>/<group_id>/<artifact_id>/<version>/<target_file> 101 # Example: 102 # https://api.bintray.com/content/google/webrtc/google-webrtc/1.0.19742/org/webrtc/google-webrtc/1.0.19742/google-webrtc-1.0.19742.aar 103 104 target_dir = version + '/' + GROUP_ID + '/' + ARTIFACT_ID + '/' + version 105 target_path = target_dir + '/' + target_file 106 url = CONTENT_API + '/' + target_path 107 108 logging.info('Uploading %s to %s', filename, url) 109 with open(filename) as fh: 110 file_data = fh.read() 111 112 for attempt in xrange(UPLOAD_TRIES): 113 try: 114 response = requests.put(url, data=file_data, auth=(user, password), 115 timeout=API_TIMEOUT_SECONDS) 116 break 117 except requests.exceptions.Timeout as e: 118 logging.warning('Timeout while uploading: %s', e) 119 time.sleep(UPLOAD_RETRY_BASE_SLEEP_SECONDS ** attempt) 120 else: 121 raise Exception('Failed to upload %s' % filename) 122 123 if not response.ok: 124 raise Exception('Failed to upload %s. Response: %s' % (filename, response)) 125 logging.info('Uploaded %s: %s', filename, response) 126 127 128def _GeneratePom(target_file, version, commit): 129 env = jinja2.Environment( 130 loader=jinja2.PackageLoader('release_aar'), 131 ) 132 template = env.get_template('pom.jinja') 133 pom = template.render(version=version, commit=commit) 134 with open(target_file, 'w') as fh: 135 fh.write(pom) 136 137 138def _TestAAR(tmp_dir, username, password, version): 139 """Runs AppRTCMobile tests using the AAR. Returns true if the tests pass.""" 140 logging.info('Testing library.') 141 env = jinja2.Environment( 142 loader=jinja2.PackageLoader('release_aar'), 143 ) 144 145 gradle_backup = os.path.join(tmp_dir, 'build.gradle.backup') 146 app_gradle_backup = os.path.join(tmp_dir, 'app-build.gradle.backup') 147 148 # Make backup copies of the project files before modifying them. 149 shutil.copy2(AAR_PROJECT_GRADLE, gradle_backup) 150 shutil.copy2(AAR_PROJECT_APP_GRADLE, app_gradle_backup) 151 152 try: 153 maven_repository_template = env.get_template('maven-repository.jinja') 154 maven_repository = maven_repository_template.render( 155 url=MAVEN_REPOSITORY, username=username, password=password) 156 157 # Append Maven repository to build file to download unpublished files. 158 with open(AAR_PROJECT_GRADLE, 'a') as gradle_file: 159 gradle_file.write(maven_repository) 160 161 # Read app build file. 162 with open(AAR_PROJECT_APP_GRADLE, 'r') as gradle_app_file: 163 gradle_app = gradle_app_file.read() 164 165 if AAR_PROJECT_DEPENDENCY not in gradle_app: 166 raise Exception( 167 '%s not found in the build file.' % AAR_PROJECT_DEPENDENCY) 168 # Set version to the version to be tested. 169 target_dependency = AAR_PROJECT_VERSION_DEPENDENCY % version 170 gradle_app = gradle_app.replace(AAR_PROJECT_DEPENDENCY, target_dependency) 171 172 # Write back. 173 with open(AAR_PROJECT_APP_GRADLE, 'w') as gradle_app_file: 174 gradle_app_file.write(gradle_app) 175 176 # Uninstall any existing version of AppRTCMobile. 177 logging.info('Uninstalling previous AppRTCMobile versions. It is okay for ' 178 'these commands to fail if AppRTCMobile is not installed.') 179 subprocess.call([ADB_BIN, 'uninstall', 'org.appspot.apprtc']) 180 subprocess.call([ADB_BIN, 'uninstall', 'org.appspot.apprtc.test']) 181 182 # Run tests. 183 try: 184 # First clean the project. 185 subprocess.check_call([GRADLEW_BIN, 'clean'], cwd=AAR_PROJECT_DIR) 186 # Then run the tests. 187 subprocess.check_call([GRADLEW_BIN, 'connectedDebugAndroidTest'], 188 cwd=AAR_PROJECT_DIR) 189 except subprocess.CalledProcessError: 190 logging.exception('Test failure.') 191 return False # Clean or tests failed 192 193 return True # Tests pass 194 finally: 195 # Restore backups. 196 shutil.copy2(gradle_backup, AAR_PROJECT_GRADLE) 197 shutil.copy2(app_gradle_backup, AAR_PROJECT_APP_GRADLE) 198 199 200def _PublishAAR(user, password, version, additional_args): 201 args = { 202 'publish_wait_for_secs': 0 # Publish asynchronously. 203 } 204 args.update(additional_args) 205 206 url = CONTENT_API + '/' + version + '/publish' 207 response = requests.post(url, data=json.dumps(args), auth=(user, password), 208 timeout=API_TIMEOUT_SECONDS) 209 210 if not response.ok: 211 raise Exception('Failed to publish. Response: %s' % response) 212 213 214def _DeleteUnpublishedVersion(user, password, version): 215 url = PACKAGES_API + '/versions/' + version 216 response = requests.get(url, auth=(user, password), 217 timeout=API_TIMEOUT_SECONDS) 218 if not response.ok: 219 raise Exception('Failed to get version info. Response: %s' % response) 220 221 version_info = json.loads(response.content) 222 if version_info['published']: 223 logging.info('Version has already been published, not deleting.') 224 return 225 226 logging.info('Deleting unpublished version.') 227 response = requests.delete(url, auth=(user, password), 228 timeout=API_TIMEOUT_SECONDS) 229 if not response.ok: 230 raise Exception('Failed to delete version. Response: %s' % response) 231 232 233def ReleaseAar(use_goma, skip_tests, publish, build_dir): 234 version = '1.0.' + _GetCommitPos() 235 commit = _GetCommitHash() 236 logging.info('Releasing AAR version %s with hash %s', version, commit) 237 238 user = os.environ.get('BINTRAY_USER', None) 239 api_key = os.environ.get('BINTRAY_API_KEY', None) 240 if not user or not api_key: 241 raise Exception('Environment variables BINTRAY_USER and BINTRAY_API_KEY ' 242 'must be defined.') 243 244 # If build directory is not specified, create a temporary directory. 245 use_tmp_dir = not build_dir 246 if use_tmp_dir: 247 build_dir = tempfile.mkdtemp() 248 249 try: 250 base_name = ARTIFACT_ID + '-' + version 251 aar_file = os.path.join(build_dir, base_name + '.aar') 252 third_party_licenses_file = os.path.join(build_dir, 'LICENSE.md') 253 pom_file = os.path.join(build_dir, base_name + '.pom') 254 255 logging.info('Building at %s', build_dir) 256 BuildAar(ARCHS, aar_file, 257 use_goma=use_goma, 258 ext_build_dir=os.path.join(build_dir, 'aar-build')) 259 _GeneratePom(pom_file, version, commit) 260 261 _UploadFile(user, api_key, aar_file, version, base_name + '.aar') 262 _UploadFile(user, api_key, third_party_licenses_file, version, 263 'THIRD_PARTY_LICENSES.md') 264 _UploadFile(user, api_key, pom_file, version, base_name + '.pom') 265 266 tests_pass = skip_tests or _TestAAR(build_dir, user, api_key, version) 267 if not tests_pass: 268 logging.info('Discarding library.') 269 _PublishAAR(user, api_key, version, {'discard': True}) 270 _DeleteUnpublishedVersion(user, api_key, version) 271 raise Exception('Test failure. Discarded library.') 272 273 if publish: 274 logging.info('Publishing library.') 275 _PublishAAR(user, api_key, version, {}) 276 else: 277 logging.info('Note: The library has not not been published automatically.' 278 ' Please do so manually if desired.') 279 finally: 280 if use_tmp_dir: 281 shutil.rmtree(build_dir, True) 282 283 284def main(): 285 args = _ParseArgs() 286 logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO) 287 ReleaseAar(args.use_goma, args.skip_tests, args.publish, args.build_dir) 288 289 290if __name__ == '__main__': 291 sys.exit(main()) 292