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