1# Copyright (C) 2018 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""Tool functions to deal with files.""" 15 16import datetime 17import enum 18import os 19import shutil 20import subprocess 21from pathlib import Path 22import textwrap 23 24# pylint: disable=import-error 25from google.protobuf import text_format # type: ignore 26 27# pylint: disable=import-error 28import metadata_pb2 # type: ignore 29 30import git_utils 31 32METADATA_FILENAME = 'METADATA' 33ANDROID_BP_FILENAME = 'Android.bp' 34 35 36@enum.unique 37class IdentifierType(enum.Enum): 38 """A subset of different Identifier types""" 39 GIT = 'Git' 40 SVN = 'SVN' 41 HG = 'Hg' 42 DARCS = 'Darcs' 43 ARCHIVE = 'Archive' 44 OTHER = 'Other' 45 46 47def find_tree_containing(project: Path) -> Path: 48 """Returns the path to the repo tree parent of the given project. 49 50 The parent tree is found by searching up the directory tree until a 51 directory is found that contains a .repo directory. Other methods of 52 finding this directory won't necessarily work: 53 54 * Using ANDROID_BUILD_TOP might find the wrong tree (if external_updater 55 is used to manage a project that is not in AOSP, as it does for CMake, 56 rr, and a few others), since ANDROID_BUILD_TOP will be the one that built 57 external_updater rather than the given project. 58 * Paths relative to __file__ are no good because we'll run from a "built" 59 PAR somewhere in the soong out directory, or possibly somewhere more 60 arbitrary when run from CI. 61 * Paths relative to the CWD require external_updater to be run from a 62 predictable location. Doing so prevents the user from using relative 63 paths (and tab complete) from directories other than the expected location. 64 65 The result for one project should not be reused for other projects, 66 as it's possible that the user has provided project paths from multiple 67 trees. 68 """ 69 if (project / ".repo").exists(): 70 return project 71 if project.parent == project: 72 raise FileNotFoundError( 73 f"Could not find a .repo directory in any parent of {project}" 74 ) 75 return find_tree_containing(project.parent) 76 77 78def external_path() -> Path: 79 """Returns the path to //external. 80 81 We cannot use the relative path from this file to find the top of the 82 tree because this will often be run in a "compiled" form from an 83 arbitrary location in the out directory. We can't fully rely on 84 ANDROID_BUILD_TOP because not all contexts will have run envsetup/lunch 85 either. We use ANDROID_BUILD_TOP whenever it is set, but if it is not set 86 we instead rely on the convention that the CWD is the root of the tree ( 87 updater.sh will cd there before executing). 88 89 There is one other context where this function cannot succeed: CI. Tests 90 run in CI do not have a source tree to find, so calling this function in 91 that context will fail. 92 """ 93 android_top = Path(os.environ.get("ANDROID_BUILD_TOP", os.getcwd())) 94 top = android_top / 'external' 95 96 if not top.exists(): 97 raise RuntimeError( 98 f"{top} does not exist. This program must be run from the " 99 f"root of an Android tree (CWD is {os.getcwd()})." 100 ) 101 return top 102 103 104def get_absolute_project_path(proj_path: Path) -> Path: 105 """Gets absolute path of a project. 106 107 Path resolution starts from external/. 108 """ 109 if proj_path.is_absolute(): 110 return proj_path 111 return external_path() / proj_path 112 113 114def resolve_command_line_paths(paths: list[str]) -> list[Path]: 115 """Resolves project paths provided by the command line. 116 117 Both relative and absolute paths are resolved to fully qualified paths 118 and returned. If any path does not exist relative to the CWD, a message 119 will be printed and that path will be pruned from the list. 120 """ 121 resolved: list[Path] = [] 122 for path_str in paths: 123 path = Path(path_str) 124 if not path.exists(): 125 print(f"Provided path {path} ({path.resolve()}) does not exist. Skipping.") 126 else: 127 resolved.append(path.resolve()) 128 return resolved 129 130 131def get_metadata_path(proj_path: Path) -> Path: 132 """Gets the absolute path of METADATA for a project.""" 133 return get_absolute_project_path(proj_path) / METADATA_FILENAME 134 135 136def get_relative_project_path(proj_path: Path) -> Path: 137 """Gets the relative path of a project starting from external/.""" 138 return get_absolute_project_path(proj_path).relative_to(external_path()) 139 140 141def canonicalize_project_path(proj_path: Path) -> Path: 142 """Returns the canonical representation of the project path. 143 144 For paths that are in the same tree as external_updater (the common 145 case), the canonical path is the path of the project relative to //external. 146 147 For paths that are in a different tree (an uncommon case used for 148 updating projects in other builds such as the NDK), the canonical path is 149 the absolute path. 150 """ 151 try: 152 return get_relative_project_path(proj_path) 153 except ValueError as ex: 154 # A less common use case, but the path might be to a non-local tree, 155 # in which case the path will not be relative to our tree. This 156 # happens when using external_updater in another project like the NDK 157 # or rr. 158 if proj_path.is_absolute(): 159 return proj_path 160 161 # Not relative to //external, and not an absolute path. This case 162 # hasn't existed before, so it has no canonical form. 163 raise ValueError( 164 f"{proj_path} must be either an absolute path or relative to {external_path()}" 165 ) from ex 166 167 168def read_metadata(proj_path: Path) -> metadata_pb2.MetaData: 169 """Reads and parses METADATA file for a project. 170 171 Args: 172 proj_path: Path to the project. 173 174 Returns: 175 Parsed MetaData proto. 176 177 Raises: 178 text_format.ParseError: Occurred when the METADATA file is invalid. 179 FileNotFoundError: Occurred when METADATA file is not found. 180 """ 181 182 with get_metadata_path(proj_path).open('r') as metadata_file: 183 metadata = metadata_file.read() 184 return text_format.Parse(metadata, metadata_pb2.MetaData()) 185 186 187def convert_url_to_identifier(metadata: metadata_pb2.MetaData) -> metadata_pb2.MetaData: 188 """Converts the old style METADATA to the new style""" 189 for url in metadata.third_party.url: 190 if url.type == metadata_pb2.URL.HOMEPAGE: 191 metadata.third_party.homepage = url.value 192 else: 193 identifier = metadata_pb2.Identifier() 194 identifier.type = IdentifierType[metadata_pb2.URL.Type.Name(url.type)].value 195 identifier.value = url.value 196 identifier.version = metadata.third_party.version 197 metadata.third_party.ClearField("version") 198 metadata.third_party.identifier.append(identifier) 199 metadata.third_party.ClearField("url") 200 return metadata 201 202 203def write_metadata(proj_path: Path, metadata: metadata_pb2.MetaData, keep_date: bool) -> None: 204 """Writes updated METADATA file for a project. 205 206 This function updates last_upgrade_date in metadata and write to the project 207 directory. 208 209 Args: 210 proj_path: Path to the project. 211 metadata: The MetaData proto to write. 212 keep_date: Do not change date. 213 """ 214 215 if not keep_date: 216 date = metadata.third_party.last_upgrade_date 217 now = datetime.datetime.now() 218 date.year = now.year 219 date.month = now.month 220 date.day = now.day 221 try: 222 rel_proj_path = str(get_relative_project_path(proj_path)) 223 except ValueError: 224 # Absolute paths to other trees will not be relative to our tree. 225 # There are no portable instructions for upgrading that project, 226 # since the path will differ between machines (or checkouts). 227 rel_proj_path = "<absolute path to project>" 228 usage_hint = textwrap.dedent(f"""\ 229 # This project was upgraded with external_updater. 230 # Usage: tools/external_updater/updater.sh update external/{rel_proj_path} 231 # For more info, check https://cs.android.com/android/platform/superproject/main/+/main:tools/external_updater/README.md 232 233 """) 234 text_metadata = usage_hint + text_format.MessageToString(metadata) 235 with get_metadata_path(proj_path).open('w') as metadata_file: 236 if metadata.third_party.license_type == metadata_pb2.LicenseType.BY_EXCEPTION_ONLY: 237 metadata_file.write(textwrap.dedent("""\ 238 # THIS PACKAGE HAS SPECIAL LICENSING CONDITIONS. PLEASE 239 # CONSULT THE OWNERS AND opensource-licensing@google.com BEFORE 240 # DEPENDING ON IT IN YOUR PROJECT. 241 242 """)) 243 metadata_file.write(text_metadata) 244 245 246def find_local_bp_files(proj_path: Path, latest_version: str) -> list[str]: 247 """Finds the bp files that are in the local project but not upstream. 248 249 Args: 250 proj_path: Path to the project. 251 latest_version: To compare upstream's latest_version to current working dir 252 """ 253 added_files = git_utils.diff_name_only(proj_path, 'A', latest_version).splitlines() 254 bp_files = [file for file in added_files if ANDROID_BP_FILENAME in file] 255 return bp_files 256 257 258def bpfmt(proj_path: Path, bp_files: list[str]) -> bool: 259 """Runs bpfmt. 260 261 It only runs bpfmt on Android.bp files that are not in upstream to prevent 262 merge conflicts. 263 264 Args: 265 proj_path: Path to the project. 266 bp_files: List of bp files to run bpfmt on 267 """ 268 cmd = ['bpfmt', '-w'] 269 270 if shutil.which("bpfmt") is None: 271 print("bpfmt is not in your PATH. You may need to run lunch, or run 'm bpfmt' first.") 272 return False 273 274 if not bp_files: 275 print("Did not find any Android.bp files to format") 276 return False 277 278 try: 279 for file in bp_files: 280 subprocess.run(cmd + [file], capture_output=True, cwd=proj_path, check=True) 281 return True 282 except subprocess.CalledProcessError as ex: 283 print(f"bpfmt failed: {ex}") 284 return False 285