• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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