#!/usr/bin/env python3 # # Copyright 2023, 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. # """Script to prepare an update to a new version of ktfmt.""" import subprocess import os import sys import re import shutil import argparse import textwrap tmp_dir = "/tmp/ktfmt" zip_path = os.path.join(tmp_dir, "common.zip") jar_path = os.path.join(tmp_dir, "framework/ktfmt.jar") copy_path = os.path.join(tmp_dir, "copy.jar") def main(): parser = argparse.ArgumentParser( description="Prepare a repository for the upgrade of ktfmt to a new version." ) parser.add_argument( "--build_id", required=True, help="The build ID of aosp-build-tools-release with the new version of ktfmt" ) parser.add_argument( "--bug_id", required=True, help="The bug ID associated to each CL generated by this tool") parser.add_argument( "--repo", required=True, help="The relative path of the repository to upgrade, e.g. 'frameworks/base/'" ) args = parser.parse_args() build_id = args.build_id bug_id = args.bug_id repo_relative_path = args.repo build_top = os.environ["ANDROID_BUILD_TOP"] repo_absolute_path = os.path.join(build_top, repo_relative_path) print("Preparing upgrade of ktfmt from build", build_id) os.chdir(repo_absolute_path) check_workspace_clean() check_branches() print("Downloading ktfmt.jar from aosp-build-tools-release") download_jar(build_id) print(f"Creating local branch ktfmt_update1") run_cmd(["repo", "start", "ktfmt_update1"]) includes_file = find_includes_file(repo_relative_path) if includes_file: update_includes_file(build_top, includes_file, bug_id) else: print("No includes file found, skipping first CL") print(f"Creating local branch ktfmt_update2") run_cmd(["repo", "start", "--head", "ktfmt_update2"]) format_files(build_top, includes_file, repo_absolute_path, bug_id) print("Done. You can now submit the generated CL(s), if any.") def run_cmd(cmd): result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if result.returncode != 0: print("Error running command: {}".format(" ".join(cmd))) print("Output: {}".format(result.stderr.decode())) sys.exit(1) return result.stdout.decode("utf-8") def is_workspace_clean(): return run_cmd(["git", "status", "--porcelain"]) == "" def check_workspace_clean(): if not is_workspace_clean(): print( "The current repository contains uncommitted changes, please run this script in a clean workspace" ) sys.exit(1) def check_branches(): result = run_cmd(["git", "branch"]) if "ktfmt_update1" in result or "ktfmt_update2" in result: print( "Branches ktfmt_update1 or ktfmt_update2 already exist, you should delete them before running this script" ) sys.exit(1) def download_jar(build_id): cmd = [ "/google/data/ro/projects/android/fetch_artifact", "--branch", "aosp-build-tools-release", "--bid", build_id, "--target", "linux", "build-common-prebuilts.zip", zip_path ] run_cmd(cmd) cmd = ["unzip", "-q", "-o", "-d", tmp_dir, zip_path] run_cmd(cmd) if not os.path.isfile(jar_path): print("Error: {} is not readable".format(jar_path)) sys.exit(1) def find_includes_file(repo_relative_path): with open("PREUPLOAD.cfg") as f: includes_line = [line for line in f if "ktfmt.py" in line][0].split(" ") if "-i" not in includes_line: return None index = includes_line.index("-i") + 1 includes_file = includes_line[index][len("${REPO_ROOT}/") + len(repo_relative_path):] if not os.path.isfile(includes_file): print("Error: {} does not exist or is not a file".format(includes_file)) sys.exit(1) return includes_file def get_included_folders(includes_file): with open(includes_file) as f: return [line[1:] for line in f.read().splitlines() if line.startswith("+")] def update_includes_file(build_top, includes_file, bug_id): included_folders = get_included_folders(includes_file) cmd = [ f"{build_top}/external/ktfmt/generate_includes_file.py", f"--output={includes_file}" ] + included_folders print(f"Updating {includes_file} with the command: {cmd}") run_cmd(cmd) if is_workspace_clean(): print(f"No change were made to {includes_file}, skipping first CL") else: print(f"Creating first CL with update of {includes_file}") create_first_cl(bug_id) def create_first_cl(bug_id): sha1sum = get_sha1sum(jar_path) change_id = f"I{sha1sum}" command = " ".join(sys.argv) cl_message = textwrap.dedent(f""" Regenerate include file for ktfmt upgrade This CL was generated automatically from the following command: $ {command} This CL regenerates the inclusion file with the current version of ktfmt so that it is up-to-date with files currently formatted or ignored by ktfmt. Bug: {bug_id} Test: Presubmits Change-Id: {change_id} Merged-In: {change_id} """) run_cmd(["git", "add", "--all"]) run_cmd(["git", "commit", "-m", cl_message]) def get_sha1sum(file): output = run_cmd(["sha1sum", file]) regex = re.compile(r"[a-f0-9]{40}") match = regex.search(output) if not match: print(f"sha1sum not found in output: {output}") sys.exit(1) return match.group() def format_files(build_top, includes_file, repo_absolute_path, bug_id): if (includes_file): included_folders = get_included_folders(includes_file) cmd = [ f"{build_top}/external/ktfmt/ktfmt.py", "-i", includes_file, "--jar", jar_path ] + included_folders else: cmd = [ f"{build_top}/external/ktfmt/ktfmt.py", "--jar", jar_path, repo_absolute_path ] print( f"Formatting the files that are already formatted with the command: {cmd}" ) run_cmd(cmd) if is_workspace_clean(): print("All files were already properly formatted, skipping second CL") else: print("Creating second CL that formats all files") create_second_cl(bug_id) def create_second_cl(bug_id): # Append 'ktfmt_update' at the end of a copy of the jar file to get # a different sha1sum. shutil.copyfile(jar_path, copy_path) with open(copy_path, "a") as file_object: file_object.write("ktfmt_update") sha1sum = get_sha1sum(copy_path) change_id = f"I{sha1sum}" command = " ".join(sys.argv) cl_message = textwrap.dedent(f""" Format files with the upcoming version of ktfmt This CL was generated automatically from the following command: $ {command} This CL formats all files already correctly formatted with the upcoming version of ktfmt. Bug: {bug_id} Test: Presubmits Change-Id: {change_id} Merged-In: {change_id} """) run_cmd(["git", "add", "--all"]) run_cmd(["git", "commit", "-m", cl_message]) if __name__ == "__main__": main()