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