| # Copyright (C) 2018 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """A commandline tool to check and update packages in external/ |
| |
| Example usage: |
| updater.sh checkall |
| updater.sh update kotlinc |
| """ |
| |
| import argparse |
| import json |
| import os |
| import subprocess |
| import time |
| |
| from google.protobuf import text_format # pylint: disable=import-error |
| |
| from git_updater import GitUpdater |
| from github_archive_updater import GithubArchiveUpdater |
| import fileutils |
| import git_utils |
| import updater_utils |
| |
| |
| UPDATERS = [GithubArchiveUpdater, GitUpdater] |
| |
| |
| def color_string(string, color): |
| """Changes the color of a string when print to terminal.""" |
| colors = { |
| 'FRESH': '\x1b[32m', |
| 'STALE': '\x1b[31;1m', |
| 'ERROR': '\x1b[31m', |
| } |
| end_color = '\033[0m' |
| return colors[color] + string + end_color |
| |
| |
| def build_updater(proj_path): |
| """Build updater for a project specified by proj_path. |
| |
| Reads and parses METADATA file. And builds updater based on the information. |
| |
| Args: |
| proj_path: Absolute or relative path to the project. |
| |
| Returns: |
| The updater object built. None if there's any error. |
| """ |
| |
| proj_path = fileutils.get_absolute_project_path(proj_path) |
| try: |
| metadata = fileutils.read_metadata(proj_path) |
| except text_format.ParseError as err: |
| print('{} {}.'.format(color_string('Invalid metadata file:', 'ERROR'), |
| err)) |
| return None |
| |
| try: |
| updater = updater_utils.create_updater(metadata, proj_path, UPDATERS) |
| except ValueError: |
| print(color_string('No supported URL.', 'ERROR')) |
| return None |
| return updater |
| |
| |
| def has_new_version(updater): |
| """Whether an updater found a new version.""" |
| return updater.get_current_version() != updater.get_latest_version() |
| |
| |
| def _message_for_calledprocesserror(error): |
| return '\n'.join([error.stdout.decode('utf-8'), |
| error.stderr.decode('utf-8')]) |
| |
| |
| def check_update(proj_path): |
| """Checks updates for a project. Prints result on console. |
| |
| Args: |
| proj_path: Absolute or relative path to the project. |
| """ |
| |
| print( |
| 'Checking {}. '.format(fileutils.get_relative_project_path(proj_path)), |
| end='') |
| updater = build_updater(proj_path) |
| if updater is None: |
| return (None, 'Failed to create updater') |
| try: |
| updater.check() |
| if has_new_version(updater): |
| print(color_string(' Out of date!', 'STALE')) |
| else: |
| print(color_string(' Up to date.', 'FRESH')) |
| return (updater, None) |
| except (IOError, ValueError) as err: |
| print('{} {}.'.format(color_string('Failed.', 'ERROR'), |
| err)) |
| return (updater, str(err)) |
| except subprocess.CalledProcessError as err: |
| msg = _message_for_calledprocesserror(err) |
| print('{}\n{}'.format(msg, color_string('Failed.', 'ERROR'))) |
| return (updater, msg) |
| |
| |
| def _process_update_result(path): |
| res = {} |
| updater, err = check_update(path) |
| if err is not None: |
| res['error'] = str(err) |
| else: |
| res['current'] = updater.get_current_version() |
| res['latest'] = updater.get_latest_version() |
| return res |
| |
| |
| def _check_some(paths, delay): |
| results = {} |
| for path in paths: |
| relative_path = fileutils.get_relative_project_path(path) |
| results[relative_path] = _process_update_result(path) |
| time.sleep(delay) |
| return results |
| |
| |
| def _check_all(delay): |
| results = {} |
| for path, dirs, files in os.walk(fileutils.EXTERNAL_PATH): |
| dirs.sort(key=lambda d: d.lower()) |
| if fileutils.METADATA_FILENAME in files: |
| # Skip sub directories. |
| dirs[:] = [] |
| relative_path = fileutils.get_relative_project_path(path) |
| results[relative_path] = _process_update_result(path) |
| time.sleep(delay) |
| return results |
| |
| |
| def check(args): |
| """Handler for check command.""" |
| if args.all: |
| results = _check_all(args.delay) |
| else: |
| results = _check_some(args.paths, args.delay) |
| |
| if args.json_output is not None: |
| with open(args.json_output, 'w') as f: |
| json.dump(results, f, sort_keys=True, indent=4) |
| |
| |
| def update(args): |
| """Handler for update command.""" |
| try: |
| _do_update(args) |
| except subprocess.CalledProcessError as err: |
| msg = _message_for_calledprocesserror(err) |
| print( |
| '{}\n{}'.format( |
| msg, |
| color_string( |
| 'Failed to upgrade.', |
| 'ERROR'))) |
| |
| |
| TMP_BRANCH_NAME = 'tmp_auto_upgrade' |
| |
| |
| def _do_update(args): |
| updater, err = check_update(args.path) |
| if updater is None: |
| return |
| if not has_new_version(updater) and not args.force: |
| return |
| |
| full_path = fileutils.get_absolute_project_path(args.path) |
| if args.branch_and_commit: |
| git_utils.checkout(full_path, args.remote_name + '/master') |
| try: |
| git_utils.delete_branch(full_path, TMP_BRANCH_NAME) |
| except subprocess.CalledProcessError as err: |
| # Still continue if the branch doesn't exist. |
| pass |
| git_utils.start_branch(full_path, TMP_BRANCH_NAME) |
| |
| updater.update() |
| |
| if args.branch_and_commit: |
| msg = 'Upgrade {} to {}\n\nTest: None'.format( |
| args.path, updater.get_latest_version()) |
| git_utils.add_file(full_path, '*') |
| git_utils.commit(full_path, msg) |
| |
| if args.push_change: |
| git_utils.push(full_path, args.remote_name) |
| |
| if args.branch_and_commit: |
| git_utils.checkout(full_path, args.remote_name + '/master') |
| |
| |
| def parse_args(): |
| """Parses commandline arguments.""" |
| |
| parser = argparse.ArgumentParser( |
| description='Check updates for third party projects in external/.') |
| subparsers = parser.add_subparsers(dest='cmd') |
| subparsers.required = True |
| |
| # Creates parser for check command. |
| check_parser = subparsers.add_parser( |
| 'check', help='Check update for one project.') |
| check_parser.add_argument( |
| 'paths', nargs='*', |
| help='Paths of the project. ' |
| 'Relative paths will be resolved from external/.') |
| check_parser.add_argument( |
| '--json_output', |
| help='Path of a json file to write result to.') |
| check_parser.add_argument( |
| '--all', action='store_true', |
| help='If set, check updates for all supported projects.') |
| check_parser.add_argument( |
| '--delay', default=0, type=int, |
| help='Time in seconds to wait between checking two projects.') |
| check_parser.set_defaults(func=check) |
| |
| # Creates parser for update command. |
| update_parser = subparsers.add_parser('update', help='Update one project.') |
| update_parser.add_argument( |
| 'path', |
| help='Path of the project. ' |
| 'Relative paths will be resolved from external/.') |
| update_parser.add_argument( |
| '--force', |
| help='Run update even if there\'s no new version.', |
| action='store_true') |
| update_parser.add_argument( |
| '--branch_and_commit', action='store_true', |
| help='Starts a new branch and commit changes.') |
| update_parser.add_argument( |
| '--push_change', action='store_true', |
| help='Pushes change to Gerrit.') |
| update_parser.add_argument( |
| '--remote_name', default='aosp', required=False, |
| help='Upstream remote name.') |
| update_parser.set_defaults(func=update) |
| |
| return parser.parse_args() |
| |
| |
| def main(): |
| """The main entry.""" |
| |
| args = parse_args() |
| args.func(args) |
| |
| |
| if __name__ == '__main__': |
| main() |