1# Copyright (C) 2018 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""A commandline tool to check and update packages in external/ 15 16Example usage: 17updater.sh checkall 18updater.sh update kotlinc 19""" 20 21import argparse 22import json 23import os 24import sys 25import subprocess 26import time 27 28from google.protobuf import text_format # pylint: disable=import-error 29 30from git_updater import GitUpdater 31from github_archive_updater import GithubArchiveUpdater 32import fileutils 33import git_utils 34import updater_utils 35 36 37UPDATERS = [GithubArchiveUpdater, GitUpdater] 38 39USE_COLOR = sys.stdout.isatty() 40 41def color_string(string, color): 42 """Changes the color of a string when print to terminal.""" 43 if not USE_COLOR: 44 return string 45 colors = { 46 'FRESH': '\x1b[32m', 47 'STALE': '\x1b[31;1m', 48 'ERROR': '\x1b[31m', 49 } 50 end_color = '\033[0m' 51 return colors[color] + string + end_color 52 53 54def build_updater(proj_path): 55 """Build updater for a project specified by proj_path. 56 57 Reads and parses METADATA file. And builds updater based on the information. 58 59 Args: 60 proj_path: Absolute or relative path to the project. 61 62 Returns: 63 The updater object built. None if there's any error. 64 """ 65 66 proj_path = fileutils.get_absolute_project_path(proj_path) 67 try: 68 metadata = fileutils.read_metadata(proj_path) 69 except text_format.ParseError as err: 70 print('{} {}.'.format(color_string('Invalid metadata file:', 'ERROR'), 71 err)) 72 return None 73 74 try: 75 updater = updater_utils.create_updater(metadata, proj_path, UPDATERS) 76 except ValueError: 77 print(color_string('No supported URL.', 'ERROR')) 78 return None 79 return updater 80 81 82def has_new_version(updater): 83 """Whether an updater found a new version.""" 84 return updater.get_current_version() != updater.get_latest_version() 85 86 87def _message_for_calledprocesserror(error): 88 return '\n'.join([error.stdout.decode('utf-8'), 89 error.stderr.decode('utf-8')]) 90 91 92def check_update(proj_path): 93 """Checks updates for a project. Prints result on console. 94 95 Args: 96 proj_path: Absolute or relative path to the project. 97 """ 98 99 print( 100 'Checking {}. '.format(fileutils.get_relative_project_path(proj_path)), 101 end='') 102 updater = build_updater(proj_path) 103 if updater is None: 104 return (None, 'Failed to create updater') 105 try: 106 updater.check() 107 if has_new_version(updater): 108 print(color_string(' Out of date!', 'STALE')) 109 else: 110 print(color_string(' Up to date.', 'FRESH')) 111 return (updater, None) 112 except (IOError, ValueError) as err: 113 print('{} {}.'.format(color_string('Failed.', 'ERROR'), 114 err)) 115 return (updater, str(err)) 116 except subprocess.CalledProcessError as err: 117 msg = _message_for_calledprocesserror(err) 118 print('{}\n{}'.format(msg, color_string('Failed.', 'ERROR'))) 119 return (updater, msg) 120 121 122def _process_update_result(path): 123 res = {} 124 updater, err = check_update(path) 125 if err is not None: 126 res['error'] = str(err) 127 else: 128 res['current'] = updater.get_current_version() 129 res['latest'] = updater.get_latest_version() 130 return res 131 132 133def _check_some(paths, delay): 134 results = {} 135 for path in paths: 136 relative_path = fileutils.get_relative_project_path(path) 137 results[relative_path] = _process_update_result(path) 138 time.sleep(delay) 139 return results 140 141 142def _check_all(delay): 143 results = {} 144 for path, dirs, files in os.walk(fileutils.EXTERNAL_PATH): 145 dirs.sort(key=lambda d: d.lower()) 146 if fileutils.METADATA_FILENAME in files: 147 # Skip sub directories. 148 dirs[:] = [] 149 relative_path = fileutils.get_relative_project_path(path) 150 results[relative_path] = _process_update_result(path) 151 time.sleep(delay) 152 return results 153 154 155def check(args): 156 """Handler for check command.""" 157 if args.all: 158 results = _check_all(args.delay) 159 else: 160 results = _check_some(args.paths, args.delay) 161 162 if args.json_output is not None: 163 with open(args.json_output, 'w') as f: 164 json.dump(results, f, sort_keys=True, indent=4) 165 166 167def update(args): 168 """Handler for update command.""" 169 try: 170 _do_update(args) 171 except subprocess.CalledProcessError as err: 172 msg = _message_for_calledprocesserror(err) 173 print( 174 '{}\n{}'.format( 175 msg, 176 color_string( 177 'Failed to upgrade.', 178 'ERROR'))) 179 180 181TMP_BRANCH_NAME = 'tmp_auto_upgrade' 182 183 184def _do_update(args): 185 updater, _ = check_update(args.path) 186 if updater is None: 187 return 188 if not has_new_version(updater) and not args.force: 189 return 190 191 full_path = fileutils.get_absolute_project_path(args.path) 192 if args.branch_and_commit: 193 git_utils.checkout(full_path, args.remote_name + '/master') 194 try: 195 git_utils.delete_branch(full_path, TMP_BRANCH_NAME) 196 except subprocess.CalledProcessError: 197 # Still continue if the branch doesn't exist. 198 pass 199 git_utils.start_branch(full_path, TMP_BRANCH_NAME) 200 201 updater.update() 202 203 if args.branch_and_commit: 204 msg = 'Upgrade {} to {}\n\nTest: None'.format( 205 args.path, updater.get_latest_version()) 206 git_utils.add_file(full_path, '*') 207 git_utils.commit(full_path, msg) 208 209 if args.push_change: 210 git_utils.push(full_path, args.remote_name) 211 212 if args.branch_and_commit: 213 git_utils.checkout(full_path, args.remote_name + '/master') 214 215 216def parse_args(): 217 """Parses commandline arguments.""" 218 219 parser = argparse.ArgumentParser( 220 description='Check updates for third party projects in external/.') 221 subparsers = parser.add_subparsers(dest='cmd') 222 subparsers.required = True 223 224 # Creates parser for check command. 225 check_parser = subparsers.add_parser( 226 'check', help='Check update for one project.') 227 check_parser.add_argument( 228 'paths', nargs='*', 229 help='Paths of the project. ' 230 'Relative paths will be resolved from external/.') 231 check_parser.add_argument( 232 '--json_output', 233 help='Path of a json file to write result to.') 234 check_parser.add_argument( 235 '--all', action='store_true', 236 help='If set, check updates for all supported projects.') 237 check_parser.add_argument( 238 '--delay', default=0, type=int, 239 help='Time in seconds to wait between checking two projects.') 240 check_parser.set_defaults(func=check) 241 242 # Creates parser for update command. 243 update_parser = subparsers.add_parser('update', help='Update one project.') 244 update_parser.add_argument( 245 'path', 246 help='Path of the project. ' 247 'Relative paths will be resolved from external/.') 248 update_parser.add_argument( 249 '--force', 250 help='Run update even if there\'s no new version.', 251 action='store_true') 252 update_parser.add_argument( 253 '--branch_and_commit', action='store_true', 254 help='Starts a new branch and commit changes.') 255 update_parser.add_argument( 256 '--push_change', action='store_true', 257 help='Pushes change to Gerrit.') 258 update_parser.add_argument( 259 '--remote_name', default='aosp', required=False, 260 help='Upstream remote name.') 261 update_parser.set_defaults(func=update) 262 263 return parser.parse_args() 264 265 266def main(): 267 """The main entry.""" 268 269 args = parse_args() 270 args.func(args) 271 272 273if __name__ == '__main__': 274 main() 275