1#!/usr/bin/env python3 2# 3# Copyright (C) 2020 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# 17import sys 18import os 19import argparse 20from datetime import date 21import glob 22import pathlib 23import re 24import shutil 25import subprocess 26import toml 27 28# Import the JetpadClient from the parent directory 29sys.path.append("..") 30from JetpadClient import * 31from update_tracing_perfetto import update_tracing_perfetto 32 33# cd into directory of script 34os.chdir(os.path.dirname(os.path.abspath(__file__))) 35 36FRAMEWORKS_SUPPORT_FP = os.path.abspath(os.path.join(os.getcwd(), '..', '..')) 37LIBRARY_VERSIONS_REL = './libraryversions.toml' 38LIBRARY_VERSIONS_FP = os.path.join(FRAMEWORKS_SUPPORT_FP, LIBRARY_VERSIONS_REL) 39COMPOSE_VERSION_REL = './compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt' 40COMPOSE_VERSION_FP = os.path.join(FRAMEWORKS_SUPPORT_FP, COMPOSE_VERSION_REL) 41VERSION_CHECKER_REL = './compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt' 42VERSION_CHECKER_FP = os.path.join(FRAMEWORKS_SUPPORT_FP, VERSION_CHECKER_REL) 43VERSION_UPDATER_REL = './development/auto-version-updater' 44VERSION_UPDATER_FP = os.path.join(FRAMEWORKS_SUPPORT_FP, VERSION_UPDATER_REL) 45PREBUILTS_ANDROIDX_INTERNAL_REL = '../../prebuilts/androidx/internal' 46PREBUILTS_ANDROIDX_INTERNAL_FP = os.path.join(FRAMEWORKS_SUPPORT_FP, PREBUILTS_ANDROIDX_INTERNAL_REL) 47 48# Set up input arguments 49parser = argparse.ArgumentParser( 50 description=("""Updates androidx library versions for a given release date. 51 This script takes in a the release date as millisecond since the epoch, 52 which is the unique id for the release in Jetpad. It queries the 53 Jetpad db, then creates an output json file with the release information. 54 Finally, updates LibraryVersions.kt, runs updateApi, and runs 55 ignoreApiChanges.""")) 56parser.add_argument( 57 'date', 58 help='Milliseconds since epoch') 59parser.add_argument( 60 '--no-commit', action="store_true", 61 help='If specified, this script will not commit the changes') 62 63def print_e(*args, **kwargs): 64 print(*args, file=sys.stderr, **kwargs) 65 66 67def ask_yes_or_no(question): 68 """Primpts a yes or no question to the user. 69 70 Args: 71 question: the question to asked. 72 73 Returns: 74 boolean representing yes or no. 75 """ 76 while(True): 77 reply = str(input(question+' (y/n): ')).lower().strip() 78 if reply: 79 if reply[0] == 'y': return True 80 if reply[0] == 'n': return False 81 print("Please respond with y/n") 82 83 84def run_update_api(): 85 """Runs updateApi ignoreApiChanges from the frameworks/support root. 86 """ 87 gradle_cmd = "cd " + FRAMEWORKS_SUPPORT_FP + " && ./gradlew updateApi ignoreApiChanges" 88 try: 89 subprocess.check_output(gradle_cmd, stderr=subprocess.STDOUT, shell=True) 90 except subprocess.CalledProcessError: 91 print_e('FAIL: Unable run `updateApi ignoreApiChanges` with command: %s' % gradle_cmd) 92 return None 93 return True 94 95 96def convert_prerelease_type_to_num(prerelease_type): 97 """" Convert a prerelease suffix type to its numeric equivalent. 98 99 Args: 100 prerelease_type: the androidx SemVer version prerelease suffix. 101 102 Returns: 103 An int representing that suffix. 104 """ 105 if prerelease_type == 'alpha': 106 return 0 107 if prerelease_type == 'beta': 108 return 1 109 if prerelease_type == 'rc': 110 return 2 111 # Stable defaults to 9 112 return 9 113 114 115def parse_version(version): 116 """Converts a SemVer androidx version string into a list of ints. 117 118 Accepts a SemVer androidx version string, such as "1.2.0-alpha02" and 119 returns a list of integers representing the version in the following format: 120 [<major>,<minor>,<bugfix>,<prerelease-suffix>,<prerelease-suffix-revision>] 121 For example 1.2.0-alpha02" returns [1,2,0,0,2] 122 123 Args: 124 version: the androidx version string. 125 126 Returns: 127 a list of integers representing the version. 128 """ 129 version_elements = version.split('-')[0].split('.') 130 version_list = [] 131 for element in version_elements: 132 version_list.append(int(element)) 133 # Check if version contains prerelease suffix 134 version_prerelease_suffix = version.split('-')[-1] 135 # Account for suffixes with only 1 suffix number, i.e. "1.1.0-alphaX" 136 version_prerelease_suffix_rev = version_prerelease_suffix[-2:] 137 version_prerelease_suffix_type = version_prerelease_suffix[:-2] 138 if not version_prerelease_suffix_rev.isnumeric(): 139 version_prerelease_suffix_rev = version_prerelease_suffix[-1:] 140 version_prerelease_suffix_type = version_prerelease_suffix[:-1] 141 version_list.append(convert_prerelease_type_to_num(version_prerelease_suffix_type)) 142 if version.find("-") == -1: 143 # Version contains no prerelease suffix 144 version_list.append(99) 145 else: 146 version_list.append(int(version_prerelease_suffix_rev)) 147 return version_list 148 149 150def get_higher_version(version_a, version_b): 151 """Given two androidx SemVer versions, returns the greater one. 152 153 Args: 154 version_a: first version to be compared. 155 version_b: second version to be compared. 156 157 Returns: 158 The greater of version_a and version_b. 159 """ 160 version_a_list = parse_version(version_a) 161 version_b_list = parse_version(version_b) 162 for i in range(len(version_a_list)): 163 if version_a_list[i] > version_b_list[i]: 164 return version_a 165 if version_a_list[i] < version_b_list[i]: 166 return version_b 167 return version_a 168 169 170def should_update_group_version_in_library_versions_toml(old_version, new_version, group_id): 171 """Whether or not this specific group ID and version should be updated. 172 173 Returns true if the new_version is greater than the version in line 174 and the group ID is not the set of group_ids_to_not_update. 175 176 Args: 177 old_version: the old version from libraryversions.toml file. 178 new_version: the version to check again. 179 group_id: group id of the version being considered 180 181 Returns: 182 True if should update version, false otherwise. 183 """ 184 # If we hit a group ID we should not update, just return. 185 group_ids_to_not_update = ["androidx.car", "androidx.compose.compiler"] 186 if group_id in group_ids_to_not_update: return False 187 return new_version == get_higher_version(old_version, new_version) 188 189 190def should_update_artifact_version_in_library_versions_toml(old_version, new_version, artifact_id): 191 """Whether or not this specific artifact ID and version should be updated. 192 193 Returns true if the new_version is greater than the version in line 194 and the artifact ID is not the set of artifact_ids_to_not_update. 195 196 Args: 197 old_version: the old version from libraryversions.toml file. 198 new_version: the version to check again. 199 artifact_id: artifact id of the version being considered 200 201 Returns: 202 True if should update version, false otherwise. 203 """ 204 # If we hit a artifact ID we should not update, just return. 205 artifact_ids_to_not_update = [] # empty list as of now 206 if artifact_id in artifact_ids_to_not_update: return False 207 return new_version == get_higher_version(old_version, new_version) 208 209 210def increment_version(version): 211 """Increments an androidx SemVer version. 212 213 If the version is alpha or beta, the suffix is simply incremented. 214 Otherwise, it chooses the next minor version. 215 216 Args: 217 version: the version to be incremented. 218 219 Returns: 220 The incremented version. 221 """ 222 if "alpha" in version or "beta" in version: 223 version_prerelease_suffix = version[-2:] 224 new_version_prerelease_suffix = int(version_prerelease_suffix) + 1 225 new_version = version[:-2] + "%02d" % (new_version_prerelease_suffix,) 226 else: 227 version_minor = version.split(".")[1] 228 new_version_minor = str(int(version_minor) + 1) 229 new_version = version.split(".")[0] + "." + new_version_minor + ".0-alpha01" 230 return new_version 231 232 233def increment_version_within_minor_version(version): 234 """Increments an androidx SemVer version without bumping the minor version. 235 236 Args: 237 version: the version to be incremented. 238 239 Returns: 240 The incremented version. 241 """ 242 if "alpha" in version or "beta" in version or "rc0" in version: 243 version_prerelease_suffix = version[-2:] 244 new_version_prerelease_suffix = int(version_prerelease_suffix) + 1 245 new_version = version[:-2] + "%02d" % (new_version_prerelease_suffix,) 246 else: 247 bugfix_version = version.split(".")[2] 248 new_bugfix_version = str(int(bugfix_version) + 1) 249 new_version = ".".join(version.split(".")[0:2]) + "." + new_bugfix_version 250 return new_version 251 252 253def get_library_constants_in_library_versions_toml(group_id, artifact_id): 254 """Gets the constants for a library in libraryversions.toml. 255 256 Args: 257 group_id: group_id of the existing library 258 artifact_id: artifact_id of the existing library 259 260 Returns: 261 A touple of the group_id constant and the artifact_id constant 262 """ 263 group_id_variable_name = group_id.replace("androidx.","").replace(".","_").upper() 264 artifact_id_variable_name = artifact_id.replace("androidx.","").replace("-","_").upper() 265 # Special case Compose because it uses the same version variable. 266 if (group_id_variable_name.startswith("COMPOSE") and 267 group_id_variable_name != "COMPOSE_MATERIAL3"): 268 group_id_variable_name = "COMPOSE" 269 # Special case Compose runtime tracing 270 if group_id == "androidx.compose.runtime" and artifact_id == "runtime-tracing": 271 group_id_variable_name = "COMPOSE_RUNTIME_TRACING" 272 artifact_id_variable_name = "COMPOSE_RUNTIME_TRACING" 273 return (group_id_variable_name, artifact_id_variable_name) 274 275 276def update_versions_in_library_versions_toml(group_id, artifact_id, old_version): 277 """Updates the versions in the libraryversions.toml file. 278 279 This will take the old_version and increment it to find the appropriate 280 new version. 281 282 Args: 283 group_id: group_id of the existing library 284 artifact_id: artifact_id of the existing library 285 old_version: old version of the existing library 286 287 Returns: 288 True if the version was updated, false otherwise. 289 """ 290 (group_id_variable_name, artifact_id_variable_name 291 ) = get_library_constants_in_library_versions_toml(group_id, artifact_id) 292 new_version = increment_version(old_version) 293 294 # Open toml file 295 library_versions = toml.load(LIBRARY_VERSIONS_FP) 296 updated_version = False 297 298 # First check any artifact ids with unique versions. 299 if artifact_id_variable_name in library_versions["versions"]: 300 old_version = library_versions["versions"][artifact_id_variable_name] 301 if should_update_artifact_version_in_library_versions_toml(old_version, new_version, artifact_id): 302 library_versions["versions"][artifact_id_variable_name] = new_version 303 updated_version = True 304 305 if not updated_version: 306 # Then check any group ids. 307 if group_id_variable_name in library_versions["versions"]: 308 old_version = library_versions["versions"][group_id_variable_name] 309 if should_update_group_version_in_library_versions_toml(old_version, new_version, group_id): 310 library_versions["versions"][group_id_variable_name] = new_version 311 updated_version = True 312 313 # sort the entries 314 library_versions["versions"] = dict(sorted(library_versions["versions"].items())) 315 316 # Open file for writing and write toml back 317 with open(LIBRARY_VERSIONS_FP, 'w') as f: 318 versions_toml_file_string = toml.dumps(library_versions, encoder=toml.TomlPreserveInlineDictEncoder()) 319 versions_toml_file_string_new = re.sub(",]", " ]", versions_toml_file_string) 320 f.write(versions_toml_file_string_new) 321 322 return updated_version 323 324 325 326def parse_version_checker_line(line): 327 runtime_version_str = line.split(' to ')[0].strip() 328 if runtime_version_str.isnumeric(): 329 runtime_version = int(runtime_version_str) 330 else: 331 print_e("Could not parse Compose runtime version in %s. " 332 "Skipping the update of the Compose runtime version." 333 % VERSION_CHECKER_FP) 334 return None, None 335 compose_version = line.split(' to ')[1].split('"')[1].strip() 336 return (runtime_version, compose_version) 337 338 339 340def get_compose_to_runtime_version_map(compose_to_runtime_version_map): 341 """Generates the compose to runtime version map from VersionChecker.kt 342 343 Args: 344 lines: lines from VERSION_CHECKER_FP (VersionChecker.kt) 345 compose_to_runtime_version_map: map to populate 346 Returns: 347 highest_index: the highest line index reached in the file 348 """ 349 # Highest line index in the map, used for new alpha/beta versions. 350 highest_index = -1 351 with open(VERSION_CHECKER_FP, 'r') as f: 352 version_checker_lines = f.readlines() 353 num_lines = len(version_checker_lines) 354 for i in range(num_lines): 355 cur_line = version_checker_lines[i] 356 # Skip any line that doesn't declare a version map. 357 if ' to "' not in cur_line: continue 358 (runtime_version, compose_version) = parse_version_checker_line(cur_line) 359 # If it returned none, we couldn't parse properly, so return. 360 if not runtime_version: return 361 compose_to_runtime_version_map[compose_version] = { 362 "runtime_version": runtime_version, 363 "file_index": i 364 } 365 if i > highest_index: highest_index = i 366 return highest_index 367 368def update_compose_runtime_version(group_id, artifact_id, old_version): 369 """Updates the compose runtime version in ComposeVersion.kt / VersionChecker.kt 370 371 This will take the old_version and increment it to find the appropriate 372 new version. 373 374 Internal version = current_version + 100 if alpha/beta + 1 if rc/stable 375 376 Args: 377 group_id: group_id of the existing library 378 artifact_id: artifact_id of the existing library 379 old_version: old version of the existing library 380 381 Returns: 382 Nothing 383 """ 384 # New runtime version that will be used 385 new_compose_runtime_version = 0 386 # New compose version that will be used 387 updated_compose_version = increment_version_within_minor_version(old_version) 388 # Highest index in the file, used for new alpha/beta versions. 389 highest_index = -1 390 # Map of the compose version to it's runtime version. 391 compose_to_runtime_version_map = {} 392 393 highest_index = get_compose_to_runtime_version_map(compose_to_runtime_version_map) 394 395 # If the value has already been added, we're done! We can return! 396 if updated_compose_version in compose_to_runtime_version_map.keys(): 397 return 398 # If the old value isn't in the map, we can't be sure how to update, 399 # so we skip. 400 if old_version not in compose_to_runtime_version_map.keys(): 401 print_e("Could not parse Compose runtime version in %s. " 402 "Skipping the update of the Compose runtime version." 403 % VERSION_CHECKER_FP) 404 return 405 406 # Open file for reading and get all lines, so we can update the current compose version. 407 with open(VERSION_CHECKER_FP, 'r') as f: 408 version_checker_lines = f.readlines() 409 num_lines = len(version_checker_lines) 410 411 old_runtime_version = compose_to_runtime_version_map[old_version]["runtime_version"] 412 if "alpha" in updated_compose_version or "beta" in updated_compose_version: 413 new_compose_runtime_version = old_runtime_version + 100 414 else: 415 new_compose_runtime_version = old_runtime_version + 1 416 new_version_line = ' %d to "%s",\n' % (new_compose_runtime_version, updated_compose_version) 417 insert_line = compose_to_runtime_version_map[old_version]["file_index"] + 1 418 version_checker_lines.insert(insert_line, new_version_line) 419 420 # Open file for reading and get all lines 421 with open(COMPOSE_VERSION_FP, 'r') as f: 422 compose_version_lines = f.readlines() 423 num_lines = len(compose_version_lines) 424 425 for i in range(num_lines): 426 cur_line = compose_version_lines[i] 427 # Skip any line that doesn't declare the version 428 if 'const val version: Int = ' not in cur_line: continue 429 version_str = cur_line.split('const val version: Int = ')[1].strip() 430 431 if version_str.isnumeric(): 432 current_runtime_version = int(version_str) 433 else: 434 print_e("Could not parse Compose runtime version in %s." 435 "Skipping the update of the Compose runtime version." 436 % COMPOSE_VERSION_FP) 437 return 438 # Only update if we have a new higher version. 439 if current_runtime_version < new_compose_runtime_version: 440 new_version_line = ' const val version: Int = %d\n' % new_compose_runtime_version 441 compose_version_lines[i] = new_version_line 442 break 443 444 445 # Open file for writing and update all lines 446 with open(COMPOSE_VERSION_FP, 'w') as f: 447 f.writelines(compose_version_lines) 448 449 # Open file for writing and update all lines 450 with open(VERSION_CHECKER_FP, 'w') as f: 451 f.writelines(version_checker_lines) 452 453 return 454 455 456def commit_updates(release_date): 457 for dir in [FRAMEWORKS_SUPPORT_FP, PREBUILTS_ANDROIDX_INTERNAL_FP]: 458 subprocess.check_call(["git", "add", "."], cwd=dir, stderr=subprocess.STDOUT) 459 # ensure that we've actually made a change: 460 staged_changes = subprocess.check_output(["git", "diff", "--cached"], cwd=dir, stderr=subprocess.STDOUT) 461 if not staged_changes: 462 continue 463 msg = "Update versions for release id %s\n\nThis commit was generated from the command:\n%s\n\n%s" % ( 464 release_date, " ".join(sys.argv), "Test: ./gradlew checkApi") 465 subprocess.check_call(["git", "commit", "-m", msg], cwd=dir, stderr=subprocess.STDOUT) 466 subprocess.check_call(["repo", "upload", ".", "--cbr", "-t", "-y", "-o", "banned-words~skip", "--label", "Presubmit-Ready+1"], cwd=dir, 467 stderr=subprocess.STDOUT) 468 469def main(args): 470 # Parse arguments and check for existence of build ID or file 471 args = parser.parse_args() 472 if not args.date: 473 parser.error("You must specify a release date in Milliseconds since epoch") 474 sys.exit(1) 475 release_json_object = getJetpadRelease(args.date, False) 476 non_updated_libraries = [] 477 tracing_perfetto_updated = False 478 for group_id in release_json_object["modules"]: 479 for artifact in release_json_object["modules"][group_id]: 480 updated = False 481 if artifact["branch"].startswith("aosp-androidx-"): 482 # Only update versions for artifacts released from the AOSP 483 # androidx-main branch or from androidx release branches, but 484 # not from any other development branch. 485 updated = update_versions_in_library_versions_toml(group_id, 486 artifact["artifactId"], artifact["version"]) 487 if (group_id == "androidx.compose.runtime" and 488 artifact["artifactId"] == "runtime"): 489 update_compose_runtime_version(group_id, 490 artifact["artifactId"], 491 artifact["version"]) 492 if (group_id == "androidx.tracing" and 493 artifact["artifactId"].startswith("tracing-perfetto")): 494 if tracing_perfetto_updated: 495 updated = True 496 else: 497 current_version = artifact["version"] 498 target_version = increment_version(current_version) 499 update_tracing_perfetto(current_version, target_version, FRAMEWORKS_SUPPORT_FP) 500 tracing_perfetto_updated = True 501 502 if not updated: 503 non_updated_libraries.append("%s:%s:%s" % (group_id, 504 artifact["artifactId"], 505 artifact["version"])) 506 if non_updated_libraries: 507 print("The following libraries were not updated:") 508 for library in non_updated_libraries: 509 print("\t", library) 510 print("Updated library versions. \nRunning `updateApi ignoreApiChanges` " 511 "for the new versions, this may take a minute...", end='') 512 if run_update_api(): 513 print("done.") 514 else: 515 print_e("failed. Please investigate manually.") 516 if not args.no_commit: 517 commit_updates(args.date) 518 519 520if __name__ == '__main__': 521 main(sys.argv) 522 523