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