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