#!/usr/bin/env python3 # # Copyright (C) 2020 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. # import sys import os import argparse from datetime import date import glob import pathlib import re import shutil import subprocess import toml # Import the JetpadClient from the parent directory sys.path.append("..") from JetpadClient import * from update_tracing_perfetto import update_tracing_perfetto # cd into directory of script os.chdir(os.path.dirname(os.path.abspath(__file__))) FRAMEWORKS_SUPPORT_FP = os.path.abspath(os.path.join(os.getcwd(), '..', '..')) LIBRARY_VERSIONS_REL = './libraryversions.toml' LIBRARY_VERSIONS_FP = os.path.join(FRAMEWORKS_SUPPORT_FP, LIBRARY_VERSIONS_REL) COMPOSE_VERSION_REL = './compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt' COMPOSE_VERSION_FP = os.path.join(FRAMEWORKS_SUPPORT_FP, COMPOSE_VERSION_REL) VERSION_CHECKER_REL = './compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt' VERSION_CHECKER_FP = os.path.join(FRAMEWORKS_SUPPORT_FP, VERSION_CHECKER_REL) VERSION_UPDATER_REL = './development/auto-version-updater' VERSION_UPDATER_FP = os.path.join(FRAMEWORKS_SUPPORT_FP, VERSION_UPDATER_REL) PREBUILTS_ANDROIDX_INTERNAL_REL = '../../prebuilts/androidx/internal' PREBUILTS_ANDROIDX_INTERNAL_FP = os.path.join(FRAMEWORKS_SUPPORT_FP, PREBUILTS_ANDROIDX_INTERNAL_REL) # Set up input arguments parser = argparse.ArgumentParser( description=("""Updates androidx library versions for a given release date. This script takes in a the release date as millisecond since the epoch, which is the unique id for the release in Jetpad. It queries the Jetpad db, then creates an output json file with the release information. Finally, updates LibraryVersions.kt, runs updateApi, and runs ignoreApiChanges.""")) parser.add_argument( 'date', help='Milliseconds since epoch') parser.add_argument( '--no-commit', action="store_true", help='If specified, this script will not commit the changes') def print_e(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) def ask_yes_or_no(question): """Primpts a yes or no question to the user. Args: question: the question to asked. Returns: boolean representing yes or no. """ while(True): reply = str(input(question+' (y/n): ')).lower().strip() if reply: if reply[0] == 'y': return True if reply[0] == 'n': return False print("Please respond with y/n") def run_update_api(): """Runs updateApi ignoreApiChanges from the frameworks/support root. """ gradle_cmd = "cd " + FRAMEWORKS_SUPPORT_FP + " && ./gradlew updateApi ignoreApiChanges" try: subprocess.check_output(gradle_cmd, stderr=subprocess.STDOUT, shell=True) except subprocess.CalledProcessError: print_e('FAIL: Unable run `updateApi ignoreApiChanges` with command: %s' % gradle_cmd) return None return True def convert_prerelease_type_to_num(prerelease_type): """" Convert a prerelease suffix type to its numeric equivalent. Args: prerelease_type: the androidx SemVer version prerelease suffix. Returns: An int representing that suffix. """ if prerelease_type == 'alpha': return 0 if prerelease_type == 'beta': return 1 if prerelease_type == 'rc': return 2 # Stable defaults to 9 return 9 def parse_version(version): """Converts a SemVer androidx version string into a list of ints. Accepts a SemVer androidx version string, such as "1.2.0-alpha02" and returns a list of integers representing the version in the following format: [,,,,] For example 1.2.0-alpha02" returns [1,2,0,0,2] Args: version: the androidx version string. Returns: a list of integers representing the version. """ version_elements = version.split('-')[0].split('.') version_list = [] for element in version_elements: version_list.append(int(element)) # Check if version contains prerelease suffix version_prerelease_suffix = version.split('-')[-1] # Account for suffixes with only 1 suffix number, i.e. "1.1.0-alphaX" version_prerelease_suffix_rev = version_prerelease_suffix[-2:] version_prerelease_suffix_type = version_prerelease_suffix[:-2] if not version_prerelease_suffix_rev.isnumeric(): version_prerelease_suffix_rev = version_prerelease_suffix[-1:] version_prerelease_suffix_type = version_prerelease_suffix[:-1] version_list.append(convert_prerelease_type_to_num(version_prerelease_suffix_type)) if version.find("-") == -1: # Version contains no prerelease suffix version_list.append(99) else: version_list.append(int(version_prerelease_suffix_rev)) return version_list def get_higher_version(version_a, version_b): """Given two androidx SemVer versions, returns the greater one. Args: version_a: first version to be compared. version_b: second version to be compared. Returns: The greater of version_a and version_b. """ version_a_list = parse_version(version_a) version_b_list = parse_version(version_b) for i in range(len(version_a_list)): if version_a_list[i] > version_b_list[i]: return version_a if version_a_list[i] < version_b_list[i]: return version_b return version_a def should_update_group_version_in_library_versions_toml(old_version, new_version, group_id): """Whether or not this specific group ID and version should be updated. Returns true if the new_version is greater than the version in line and the group ID is not the set of group_ids_to_not_update. Args: old_version: the old version from libraryversions.toml file. new_version: the version to check again. group_id: group id of the version being considered Returns: True if should update version, false otherwise. """ # If we hit a group ID we should not update, just return. group_ids_to_not_update = ["androidx.car", "androidx.compose.compiler"] if group_id in group_ids_to_not_update: return False return new_version == get_higher_version(old_version, new_version) def should_update_artifact_version_in_library_versions_toml(old_version, new_version, artifact_id): """Whether or not this specific artifact ID and version should be updated. Returns true if the new_version is greater than the version in line and the artifact ID is not the set of artifact_ids_to_not_update. Args: old_version: the old version from libraryversions.toml file. new_version: the version to check again. artifact_id: artifact id of the version being considered Returns: True if should update version, false otherwise. """ # If we hit a artifact ID we should not update, just return. artifact_ids_to_not_update = [] # empty list as of now if artifact_id in artifact_ids_to_not_update: return False return new_version == get_higher_version(old_version, new_version) def increment_version(version): """Increments an androidx SemVer version. If the version is alpha or beta, the suffix is simply incremented. Otherwise, it chooses the next minor version. Args: version: the version to be incremented. Returns: The incremented version. """ if "alpha" in version or "beta" in version: version_prerelease_suffix = version[-2:] new_version_prerelease_suffix = int(version_prerelease_suffix) + 1 new_version = version[:-2] + "%02d" % (new_version_prerelease_suffix,) else: version_minor = version.split(".")[1] new_version_minor = str(int(version_minor) + 1) new_version = version.split(".")[0] + "." + new_version_minor + ".0-alpha01" return new_version def increment_version_within_minor_version(version): """Increments an androidx SemVer version without bumping the minor version. Args: version: the version to be incremented. Returns: The incremented version. """ if "alpha" in version or "beta" in version or "rc0" in version: version_prerelease_suffix = version[-2:] new_version_prerelease_suffix = int(version_prerelease_suffix) + 1 new_version = version[:-2] + "%02d" % (new_version_prerelease_suffix,) else: bugfix_version = version.split(".")[2] new_bugfix_version = str(int(bugfix_version) + 1) new_version = ".".join(version.split(".")[0:2]) + "." + new_bugfix_version return new_version def get_library_constants_in_library_versions_toml(group_id, artifact_id): """Gets the constants for a library in libraryversions.toml. Args: group_id: group_id of the existing library artifact_id: artifact_id of the existing library Returns: A touple of the group_id constant and the artifact_id constant """ group_id_variable_name = group_id.replace("androidx.","").replace(".","_").upper() artifact_id_variable_name = artifact_id.replace("androidx.","").replace("-","_").upper() # Special case Compose because it uses the same version variable. if (group_id_variable_name.startswith("COMPOSE") and group_id_variable_name != "COMPOSE_MATERIAL3"): group_id_variable_name = "COMPOSE" # Special case Compose runtime tracing if group_id == "androidx.compose.runtime" and artifact_id == "runtime-tracing": group_id_variable_name = "COMPOSE_RUNTIME_TRACING" artifact_id_variable_name = "COMPOSE_RUNTIME_TRACING" return (group_id_variable_name, artifact_id_variable_name) def update_versions_in_library_versions_toml(group_id, artifact_id, old_version): """Updates the versions in the libraryversions.toml file. This will take the old_version and increment it to find the appropriate new version. Args: group_id: group_id of the existing library artifact_id: artifact_id of the existing library old_version: old version of the existing library Returns: True if the version was updated, false otherwise. """ (group_id_variable_name, artifact_id_variable_name ) = get_library_constants_in_library_versions_toml(group_id, artifact_id) new_version = increment_version(old_version) # Open toml file library_versions = toml.load(LIBRARY_VERSIONS_FP) updated_version = False # First check any artifact ids with unique versions. if artifact_id_variable_name in library_versions["versions"]: old_version = library_versions["versions"][artifact_id_variable_name] if should_update_artifact_version_in_library_versions_toml(old_version, new_version, artifact_id): library_versions["versions"][artifact_id_variable_name] = new_version updated_version = True if not updated_version: # Then check any group ids. if group_id_variable_name in library_versions["versions"]: old_version = library_versions["versions"][group_id_variable_name] if should_update_group_version_in_library_versions_toml(old_version, new_version, group_id): library_versions["versions"][group_id_variable_name] = new_version updated_version = True # sort the entries library_versions["versions"] = dict(sorted(library_versions["versions"].items())) # Open file for writing and write toml back with open(LIBRARY_VERSIONS_FP, 'w') as f: versions_toml_file_string = toml.dumps(library_versions, encoder=toml.TomlPreserveInlineDictEncoder()) versions_toml_file_string_new = re.sub(",]", " ]", versions_toml_file_string) f.write(versions_toml_file_string_new) return updated_version def parse_version_checker_line(line): runtime_version_str = line.split(' to ')[0].strip() if runtime_version_str.isnumeric(): runtime_version = int(runtime_version_str) else: print_e("Could not parse Compose runtime version in %s. " "Skipping the update of the Compose runtime version." % VERSION_CHECKER_FP) return None, None compose_version = line.split(' to ')[1].split('"')[1].strip() return (runtime_version, compose_version) def get_compose_to_runtime_version_map(compose_to_runtime_version_map): """Generates the compose to runtime version map from VersionChecker.kt Args: lines: lines from VERSION_CHECKER_FP (VersionChecker.kt) compose_to_runtime_version_map: map to populate Returns: highest_index: the highest line index reached in the file """ # Highest line index in the map, used for new alpha/beta versions. highest_index = -1 with open(VERSION_CHECKER_FP, 'r') as f: version_checker_lines = f.readlines() num_lines = len(version_checker_lines) for i in range(num_lines): cur_line = version_checker_lines[i] # Skip any line that doesn't declare a version map. if ' to "' not in cur_line: continue (runtime_version, compose_version) = parse_version_checker_line(cur_line) # If it returned none, we couldn't parse properly, so return. if not runtime_version: return compose_to_runtime_version_map[compose_version] = { "runtime_version": runtime_version, "file_index": i } if i > highest_index: highest_index = i return highest_index def update_compose_runtime_version(group_id, artifact_id, old_version): """Updates the compose runtime version in ComposeVersion.kt / VersionChecker.kt This will take the old_version and increment it to find the appropriate new version. Internal version = current_version + 100 if alpha/beta + 1 if rc/stable Args: group_id: group_id of the existing library artifact_id: artifact_id of the existing library old_version: old version of the existing library Returns: Nothing """ # New runtime version that will be used new_compose_runtime_version = 0 # New compose version that will be used updated_compose_version = increment_version_within_minor_version(old_version) # Highest index in the file, used for new alpha/beta versions. highest_index = -1 # Map of the compose version to it's runtime version. compose_to_runtime_version_map = {} highest_index = get_compose_to_runtime_version_map(compose_to_runtime_version_map) # If the value has already been added, we're done! We can return! if updated_compose_version in compose_to_runtime_version_map.keys(): return # If the old value isn't in the map, we can't be sure how to update, # so we skip. if old_version not in compose_to_runtime_version_map.keys(): print_e("Could not parse Compose runtime version in %s. " "Skipping the update of the Compose runtime version." % VERSION_CHECKER_FP) return # Open file for reading and get all lines, so we can update the current compose version. with open(VERSION_CHECKER_FP, 'r') as f: version_checker_lines = f.readlines() num_lines = len(version_checker_lines) old_runtime_version = compose_to_runtime_version_map[old_version]["runtime_version"] if "alpha" in updated_compose_version or "beta" in updated_compose_version: new_compose_runtime_version = old_runtime_version + 100 else: new_compose_runtime_version = old_runtime_version + 1 new_version_line = ' %d to "%s",\n' % (new_compose_runtime_version, updated_compose_version) insert_line = compose_to_runtime_version_map[old_version]["file_index"] + 1 version_checker_lines.insert(insert_line, new_version_line) # Open file for reading and get all lines with open(COMPOSE_VERSION_FP, 'r') as f: compose_version_lines = f.readlines() num_lines = len(compose_version_lines) for i in range(num_lines): cur_line = compose_version_lines[i] # Skip any line that doesn't declare the version if 'const val version: Int = ' not in cur_line: continue version_str = cur_line.split('const val version: Int = ')[1].strip() if version_str.isnumeric(): current_runtime_version = int(version_str) else: print_e("Could not parse Compose runtime version in %s." "Skipping the update of the Compose runtime version." % COMPOSE_VERSION_FP) return # Only update if we have a new higher version. if current_runtime_version < new_compose_runtime_version: new_version_line = ' const val version: Int = %d\n' % new_compose_runtime_version compose_version_lines[i] = new_version_line break # Open file for writing and update all lines with open(COMPOSE_VERSION_FP, 'w') as f: f.writelines(compose_version_lines) # Open file for writing and update all lines with open(VERSION_CHECKER_FP, 'w') as f: f.writelines(version_checker_lines) return def commit_updates(release_date): for dir in [FRAMEWORKS_SUPPORT_FP, PREBUILTS_ANDROIDX_INTERNAL_FP]: subprocess.check_call(["git", "add", "."], cwd=dir, stderr=subprocess.STDOUT) # ensure that we've actually made a change: staged_changes = subprocess.check_output(["git", "diff", "--cached"], cwd=dir, stderr=subprocess.STDOUT) if not staged_changes: continue msg = "Update versions for release id %s\n\nThis commit was generated from the command:\n%s\n\n%s" % ( release_date, " ".join(sys.argv), "Test: ./gradlew checkApi") subprocess.check_call(["git", "commit", "-m", msg], cwd=dir, stderr=subprocess.STDOUT) subprocess.check_call(["repo", "upload", ".", "--cbr", "-t", "-y", "-o", "banned-words~skip", "--label", "Presubmit-Ready+1"], cwd=dir, stderr=subprocess.STDOUT) def main(args): # Parse arguments and check for existence of build ID or file args = parser.parse_args() if not args.date: parser.error("You must specify a release date in Milliseconds since epoch") sys.exit(1) release_json_object = getJetpadRelease(args.date, False) non_updated_libraries = [] tracing_perfetto_updated = False for group_id in release_json_object["modules"]: for artifact in release_json_object["modules"][group_id]: updated = False if artifact["branch"].startswith("aosp-androidx-"): # Only update versions for artifacts released from the AOSP # androidx-main branch or from androidx release branches, but # not from any other development branch. updated = update_versions_in_library_versions_toml(group_id, artifact["artifactId"], artifact["version"]) if (group_id == "androidx.compose.runtime" and artifact["artifactId"] == "runtime"): update_compose_runtime_version(group_id, artifact["artifactId"], artifact["version"]) if (group_id == "androidx.tracing" and artifact["artifactId"].startswith("tracing-perfetto")): if tracing_perfetto_updated: updated = True else: current_version = artifact["version"] target_version = increment_version(current_version) update_tracing_perfetto(current_version, target_version, FRAMEWORKS_SUPPORT_FP) tracing_perfetto_updated = True if not updated: non_updated_libraries.append("%s:%s:%s" % (group_id, artifact["artifactId"], artifact["version"])) if non_updated_libraries: print("The following libraries were not updated:") for library in non_updated_libraries: print("\t", library) print("Updated library versions. \nRunning `updateApi ignoreApiChanges` " "for the new versions, this may take a minute...", end='') if run_update_api(): print("done.") else: print_e("failed. Please investigate manually.") if not args.no_commit: commit_updates(args.date) if __name__ == '__main__': main(sys.argv)