普通文本  |  271行  |  7.99 KB

# 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()