1#!/usr/bin/env python3 2 3# 4# Copyright 2023, The Android Open Source Project 5# 6# Licensed under the Apache License, Version 2.0 (the "License"); 7# you may not use this file except in compliance with the License. 8# You may obtain a copy of the License at 9# 10# http://www.apache.org/licenses/LICENSE-2.0 11# 12# Unless required by applicable law or agreed to in writing, software 13# distributed under the License is distributed on an "AS IS" BASIS, 14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15# See the License for the specific language governing permissions and 16# limitations under the License. 17# 18"""Script to prepare an update to a new version of ktfmt.""" 19 20import subprocess 21import os 22import sys 23import re 24import shutil 25import argparse 26import textwrap 27 28tmp_dir = "/tmp/ktfmt" 29zip_path = os.path.join(tmp_dir, "common.zip") 30jar_path = os.path.join(tmp_dir, "framework/ktfmt.jar") 31copy_path = os.path.join(tmp_dir, "copy.jar") 32 33 34def main(): 35 parser = argparse.ArgumentParser( 36 description="Prepare a repository for the upgrade of ktfmt to a new version." 37 ) 38 parser.add_argument( 39 "--build_id", 40 required=True, 41 help="The build ID of aosp-build-tools-release with the new version of ktfmt" 42 ) 43 parser.add_argument( 44 "--bug_id", 45 required=True, 46 help="The bug ID associated to each CL generated by this tool") 47 parser.add_argument( 48 "--repo", 49 required=True, 50 help="The relative path of the repository to upgrade, e.g. 'frameworks/base/'" 51 ) 52 args = parser.parse_args() 53 54 build_id = args.build_id 55 bug_id = args.bug_id 56 repo_relative_path = args.repo 57 58 build_top = os.environ["ANDROID_BUILD_TOP"] 59 repo_absolute_path = os.path.join(build_top, repo_relative_path) 60 61 print("Preparing upgrade of ktfmt from build", build_id) 62 os.chdir(repo_absolute_path) 63 check_workspace_clean() 64 check_branches() 65 66 print("Downloading ktfmt.jar from aosp-build-tools-release") 67 download_jar(build_id) 68 69 print(f"Creating local branch ktfmt_update1") 70 run_cmd(["repo", "start", "ktfmt_update1"]) 71 72 includes_file = find_includes_file(repo_relative_path) 73 if includes_file: 74 update_includes_file(build_top, includes_file, bug_id) 75 else: 76 print("No includes file found, skipping first CL") 77 78 print(f"Creating local branch ktfmt_update2") 79 run_cmd(["repo", "start", "--head", "ktfmt_update2"]) 80 format_files(build_top, includes_file, repo_absolute_path, bug_id) 81 82 print("Done. You can now submit the generated CL(s), if any.") 83 84 85def run_cmd(cmd): 86 result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 87 if result.returncode != 0: 88 print("Error running command: {}".format(" ".join(cmd))) 89 print("Output: {}".format(result.stderr.decode())) 90 sys.exit(1) 91 return result.stdout.decode("utf-8") 92 93 94def is_workspace_clean(): 95 return run_cmd(["git", "status", "--porcelain"]) == "" 96 97 98def check_workspace_clean(): 99 if not is_workspace_clean(): 100 print( 101 "The current repository contains uncommitted changes, please run this script in a clean workspace" 102 ) 103 sys.exit(1) 104 105 106def check_branches(): 107 result = run_cmd(["git", "branch"]) 108 if "ktfmt_update1" in result or "ktfmt_update2" in result: 109 print( 110 "Branches ktfmt_update1 or ktfmt_update2 already exist, you should delete them before running this script" 111 ) 112 sys.exit(1) 113 114 115def download_jar(build_id): 116 cmd = [ 117 "/google/data/ro/projects/android/fetch_artifact", "--branch", 118 "aosp-build-tools-release", "--bid", build_id, "--target", "linux", 119 "build-common-prebuilts.zip", zip_path 120 ] 121 run_cmd(cmd) 122 cmd = ["unzip", "-q", "-o", "-d", tmp_dir, zip_path] 123 run_cmd(cmd) 124 125 if not os.path.isfile(jar_path): 126 print("Error: {} is not readable".format(jar_path)) 127 sys.exit(1) 128 129 130def find_includes_file(repo_relative_path): 131 with open("PREUPLOAD.cfg") as f: 132 includes_line = [line for line in f if "ktfmt.py" in line][0].split(" ") 133 if "-i" not in includes_line: 134 return None 135 136 index = includes_line.index("-i") + 1 137 includes_file = includes_line[index][len("${REPO_ROOT}/") + 138 len(repo_relative_path):] 139 if not os.path.isfile(includes_file): 140 print("Error: {} does not exist or is not a file".format(includes_file)) 141 sys.exit(1) 142 return includes_file 143 144 145def get_included_folders(includes_file): 146 with open(includes_file) as f: 147 return [line[1:] for line in f.read().splitlines() if line.startswith("+")] 148 149 150def update_includes_file(build_top, includes_file, bug_id): 151 included_folders = get_included_folders(includes_file) 152 cmd = [ 153 f"{build_top}/external/ktfmt/generate_includes_file.py", 154 f"--output={includes_file}" 155 ] + included_folders 156 print(f"Updating {includes_file} with the command: {cmd}") 157 run_cmd(cmd) 158 159 if is_workspace_clean(): 160 print(f"No change were made to {includes_file}, skipping first CL") 161 else: 162 print(f"Creating first CL with update of {includes_file}") 163 create_first_cl(bug_id) 164 165 166def create_first_cl(bug_id): 167 sha1sum = get_sha1sum(jar_path) 168 change_id = f"I{sha1sum}" 169 command = " ".join(sys.argv) 170 cl_message = textwrap.dedent(f""" 171 Regenerate include file for ktfmt upgrade 172 173 This CL was generated automatically from the following command: 174 175 $ {command} 176 177 This CL regenerates the inclusion file with the current version of ktfmt 178 so that it is up-to-date with files currently formatted or ignored by 179 ktfmt. 180 181 Bug: {bug_id} 182 Test: Presubmits 183 Change-Id: {change_id} 184 Merged-In: {change_id} 185 """) 186 187 run_cmd(["git", "add", "--all"]) 188 run_cmd(["git", "commit", "-m", cl_message]) 189 190 191def get_sha1sum(file): 192 output = run_cmd(["sha1sum", file]) 193 regex = re.compile(r"[a-f0-9]{40}") 194 match = regex.search(output) 195 if not match: 196 print(f"sha1sum not found in output: {output}") 197 sys.exit(1) 198 return match.group() 199 200 201def format_files(build_top, includes_file, repo_absolute_path, bug_id): 202 if (includes_file): 203 included_folders = get_included_folders(includes_file) 204 cmd = [ 205 f"{build_top}/external/ktfmt/ktfmt.py", "-i", includes_file, "--jar", 206 jar_path 207 ] + included_folders 208 else: 209 cmd = [ 210 f"{build_top}/external/ktfmt/ktfmt.py", "--jar", jar_path, 211 repo_absolute_path 212 ] 213 214 print( 215 f"Formatting the files that are already formatted with the command: {cmd}" 216 ) 217 run_cmd(cmd) 218 219 if is_workspace_clean(): 220 print("All files were already properly formatted, skipping second CL") 221 else: 222 print("Creating second CL that formats all files") 223 create_second_cl(bug_id) 224 225 226def create_second_cl(bug_id): 227 # Append 'ktfmt_update' at the end of a copy of the jar file to get 228 # a different sha1sum. 229 shutil.copyfile(jar_path, copy_path) 230 with open(copy_path, "a") as file_object: 231 file_object.write("ktfmt_update") 232 233 sha1sum = get_sha1sum(copy_path) 234 change_id = f"I{sha1sum}" 235 command = " ".join(sys.argv) 236 cl_message = textwrap.dedent(f""" 237 Format files with the upcoming version of ktfmt 238 239 This CL was generated automatically from the following command: 240 241 $ {command} 242 243 This CL formats all files already correctly formatted with the upcoming 244 version of ktfmt. 245 246 Bug: {bug_id} 247 Test: Presubmits 248 Change-Id: {change_id} 249 Merged-In: {change_id} 250 """) 251 252 run_cmd(["git", "add", "--all"]) 253 run_cmd(["git", "commit", "-m", cl_message]) 254 255 256if __name__ == "__main__": 257 main() 258