1#!/usr/bin/python3 -B 2 3# Copyright 2021 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16"""ojluni_modify_expectation is a command-line tool for modifying the EXPECTED_UPSTREAM file.""" 17 18import argparse 19import sys 20 21# pylint: disable=g-importing-member 22# pylint: disable=g-multiple-import 23from typing import ( 24 Sequence, 25 List, 26) 27 28from common_util import ( 29 ExpectedUpstreamEntry, 30 ExpectedUpstreamFile, 31 LIBCORE_DIR, 32 OpenjdkFinder, 33 OjluniFinder, 34) 35 36# Import git only after common_util because common_util will 37# produce informative error 38from git import (Commit, Repo) 39from gitdb.exc import BadName 40 41LIBCORE_REPO = Repo(LIBCORE_DIR.as_posix()) 42 43AUTOCOMPLETE_TAGS = [ 44 'jdk7u/jdk7u40-b60', 45 'jdk8u/jdk8u121-b13', 46 'jdk8u/jdk8u60-b31', 47 'jdk9/jdk-9+181', 48 'jdk11u/jdk-11.0.22-ga', 49 'jdk17u/jdk-17.0.10-ga', 50 'jdk21u/jdk-21.0.2-ga', 51] 52 53 54def error_and_exit(msg: str) -> None: 55 print(f'Error: {msg}', file=sys.stderr) 56 sys.exit(1) 57 58 59def get_commit_or_exit(git_ref: str) -> Commit: 60 try: 61 return LIBCORE_REPO.commit(git_ref) 62 except BadName as e: 63 error_and_exit(f'{e}') 64 65 66def autocomplete_tag_or_commit(str_tag_or_commit: str) -> List[str]: 67 """Returns a list of tags / commits matching the given partial string.""" 68 if str_tag_or_commit is None: 69 str_tag_or_commit = '' 70 return list( 71 filter(lambda tag: tag.startswith(str_tag_or_commit), AUTOCOMPLETE_TAGS)) 72 73 74COMMAND_ACTIONS = ['add', 'modify', 'remove', 'sort'] 75 76 77def autocomplete_action(partial_str: str) -> None: 78 result_list = list( 79 filter(lambda action: action.startswith(partial_str), COMMAND_ACTIONS)) 80 print('\n'.join(result_list)) 81 exit(0) 82 83 84def main(argv: Sequence[str]) -> None: 85 is_auto_complete = len(argv) >= 2 and argv[0] == '--autocomplete' 86 # argparse can't help autocomplete subcommand. We implement this without 87 # argparse here. 88 if is_auto_complete and argv[1] == '1': 89 action = argv[2] if len(argv) >= 3 else '' 90 autocomplete_action(action) 91 92 # If it's for autocompletion, then all arguments are optional. 93 parser_nargs = '?' if is_auto_complete else 1 94 95 main_parser = argparse.ArgumentParser( 96 description='A command line tool modifying the EXPECTED_UPSTREAM file.') 97 # --autocomplete <int> is an 'int' argument because the value represents 98 # the raw index of the argument to be autocompleted received in the Shell, 99 # and this number is not always the same as the number of arguments 100 # received here, i.e. len(argv), for examples of empty value in the 101 # argument or autocompleting the middle argument, not last argument. 102 main_parser.add_argument( 103 '--autocomplete', type=int, help='flag when tabbing in command line') 104 subparsers = main_parser.add_subparsers( 105 dest='command', help='sub-command help') 106 107 add_parser = subparsers.add_parser( 108 'add', help='Add a new entry into the EXPECTED_UPSTREAM ' 109 'file') 110 add_parser.add_argument( 111 'tag_or_commit', 112 nargs=parser_nargs, 113 help='A git tag or commit in the upstream-openjdkXXX branch') 114 add_parser.add_argument( 115 'class_or_source_file', 116 nargs=parser_nargs, 117 help='Fully qualified class name or upstream source path') 118 add_parser.add_argument( 119 'ojluni_path', nargs='?', help='Destination path in ojluni/') 120 121 modify_parser = subparsers.add_parser( 122 'modify', help='Modify an entry in the EXPECTED_UPSTREAM file') 123 modify_parser.add_argument( 124 'class_or_ojluni_path', nargs=parser_nargs, help='File path in ojluni/') 125 modify_parser.add_argument( 126 'tag_or_commit', 127 nargs=parser_nargs, 128 help='A git tag or commit in the upstream-openjdkXXX branch') 129 modify_parser.add_argument( 130 'source_file', nargs='?', help='A upstream source path') 131 132 modify_parser = subparsers.add_parser( 133 'remove', help='Remove an entry in the EXPECTED_UPSTREAM file') 134 modify_parser.add_argument( 135 'class_or_ojluni_path', nargs=parser_nargs, help='File path in ojluni/') 136 137 subparsers.add_parser( 138 'sort', help='Sort the entries in the EXPECTED_UPSTREAM file') 139 140 args = main_parser.parse_args(argv) 141 142 expected_upstream_file = ExpectedUpstreamFile() 143 expected_entries = expected_upstream_file.read_all_entries() 144 145 if is_auto_complete: 146 no_args = args.autocomplete 147 148 autocomp_result = [] 149 if args.command == 'modify' or args.command == 'remove': 150 if no_args == 2: 151 input_class_or_ojluni_path = args.class_or_ojluni_path 152 if input_class_or_ojluni_path is None: 153 input_class_or_ojluni_path = '' 154 155 existing_dst_paths = list( 156 map(lambda entry: entry.dst_path, expected_entries)) 157 ojluni_finder: OjluniFinder = OjluniFinder(existing_dst_paths) 158 # Case 1: Treat the input as file path 159 autocomp_result += ojluni_finder.match_path_prefix( 160 input_class_or_ojluni_path) 161 162 # Case 2: Treat the input as java package / class name 163 autocomp_result += ojluni_finder.match_classname_prefix( 164 input_class_or_ojluni_path) 165 elif no_args == 3 and args.command == 'modify': 166 autocomp_result += autocomplete_tag_or_commit(args.tag_or_commit) 167 elif args.command == 'add': 168 if no_args == 2: 169 autocomp_result += autocomplete_tag_or_commit(args.tag_or_commit) 170 elif no_args == 3: 171 commit = get_commit_or_exit(args.tag_or_commit) 172 class_or_src_path = args.class_or_source_file 173 if class_or_src_path is None: 174 class_or_src_path = '' 175 176 openjdk_finder: OpenjdkFinder = OpenjdkFinder(commit) 177 178 matches = openjdk_finder.match_path_prefix( 179 class_or_src_path) 180 181 matches += openjdk_finder.match_classname_prefix( 182 class_or_src_path) 183 184 existing_dst_paths = set(map(lambda e: e.dst_path, expected_entries)) 185 186 # Translate the class names or source paths to dst paths and exclude 187 # such matches from the auto-completion result 188 def source_not_exists(src_path_or_class: str) -> bool: 189 nonlocal existing_dst_paths, openjdk_finder 190 t_src_path = openjdk_finder.find_src_path_from_classname( 191 src_path_or_class) 192 if t_src_path is None: 193 # t_src_path is a java package. It must not in existing_dst_paths. 194 return True 195 t_dst_path = OpenjdkFinder.translate_src_path_to_ojluni_path( 196 t_src_path) 197 return t_dst_path not in existing_dst_paths 198 199 autocomp_result += list(filter(source_not_exists, matches)) 200 201 print('\n'.join(autocomp_result)) 202 exit(0) 203 204 if args.command == 'modify': 205 dst_class_or_file = args.class_or_ojluni_path[0] 206 dst_path = OjluniFinder.translate_from_class_name_to_ojluni_path( 207 dst_class_or_file) 208 matches = list(filter(lambda e: dst_path == e.dst_path, expected_entries)) 209 if not matches: 210 error_and_exit(f'{dst_path} is not found in the EXPECTED_UPSTREAM.') 211 entry: ExpectedUpstreamEntry = matches[0] 212 str_tag_or_commit = args.tag_or_commit[0] 213 is_src_given = args.source_file is not None 214 src_path = args.source_file if is_src_given else entry.src_path 215 commit = get_commit_or_exit(str_tag_or_commit) 216 openjdk_finder: OpenjdkFinder = OpenjdkFinder(commit) 217 if openjdk_finder.has_file(src_path): 218 pass 219 elif not is_src_given: 220 guessed_src_path = openjdk_finder.find_src_path_from_ojluni_path(dst_path) 221 if guessed_src_path is None: 222 error_and_exit('[source_file] argument is required.') 223 src_path = guessed_src_path 224 else: 225 error_and_exit(f'{src_path} is not found in the {str_tag_or_commit}') 226 entry.git_ref = str_tag_or_commit 227 entry.src_path = src_path 228 expected_upstream_file.write_all_entries(expected_entries) 229 print(f'Modified the entry {entry}') 230 elif args.command == 'remove': 231 dst_class_or_file = args.class_or_ojluni_path[0] 232 dst_path = OjluniFinder.translate_from_class_name_to_ojluni_path( 233 dst_class_or_file) 234 matches = list(filter(lambda e: dst_path == e.dst_path, expected_entries)) 235 if not matches: 236 error_and_exit(f'{dst_path} is not found in the EXPECTED_UPSTREAM.') 237 238 entry: ExpectedUpstreamEntry = matches[0] 239 expected_entries.remove(entry) 240 expected_upstream_file.write_all_entries(expected_entries) 241 print(f'Removed the entry {entry}') 242 elif args.command == 'add': 243 class_or_src_path = args.class_or_source_file[0] 244 str_tag_or_commit = args.tag_or_commit[0] 245 commit = get_commit_or_exit(str_tag_or_commit) 246 openjdk_finder = OpenjdkFinder(commit) 247 src_path = openjdk_finder.find_src_path_from_classname(class_or_src_path) 248 if src_path is None: 249 search_paths = openjdk_finder.get_search_paths() 250 error_and_exit(f'{class_or_src_path} is not found in {commit}. ' 251 f'The search paths are:\n{search_paths}') 252 ojluni_path = args.ojluni_path 253 # Guess the source path if it's not given in the argument 254 if ojluni_path is None: 255 ojluni_path = OpenjdkFinder.translate_src_path_to_ojluni_path(src_path) 256 if ojluni_path is None: 257 error_and_exit('The ojluni destination path is not given.') 258 259 matches = list( 260 filter(lambda e: ojluni_path == e.dst_path, expected_entries)) 261 if matches: 262 error_and_exit(f"Can't add the file {ojluni_path} because " 263 f'{class_or_src_path} exists in the EXPECTED_UPSTREAM') 264 265 new_entry = ExpectedUpstreamEntry(ojluni_path, str_tag_or_commit, src_path) 266 expected_upstream_file.write_new_entry(new_entry, expected_entries) 267 elif args.command == 'sort': 268 expected_upstream_file.sort_and_write_all_entries(expected_entries) 269 else: 270 error_and_exit(f'Unknown subcommand: {args.command}') 271 272 273if __name__ == '__main__': 274 main(sys.argv[1:]) 275