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 19from pathlib import Path 20import textwrap 21 22# pylint: disable=import-error 23from google.protobuf import text_format # type: ignore 24 25# pylint: disable=import-error 26import metadata_pb2 # type: ignore 27 28 29METADATA_FILENAME = 'METADATA' 30 31 32@enum.unique 33class IdentifierType(enum.Enum): 34 """A subset of different Identifier types""" 35 GIT = 'Git' 36 SVN = 'SVN' 37 HG = 'Hg' 38 DARCS = 'Darcs' 39 ARCHIVE = 'Archive' 40 OTHER = 'Other' 41 42 43def find_tree_containing(project: Path) -> Path: 44 """Returns the path to the repo tree parent of the given project. 45 46 The parent tree is found by searching up the directory tree until a 47 directory is found that contains a .repo directory. Other methods of 48 finding this directory won't necessarily work: 49 50 * Using ANDROID_BUILD_TOP might find the wrong tree (if external_updater 51 is used to manage a project that is not in AOSP, as it does for CMake, 52 rr, and a few others), since ANDROID_BUILD_TOP will be the one that built 53 external_updater rather than the given project. 54 * Paths relative to __file__ are no good because we'll run from a "built" 55 PAR somewhere in the soong out directory, or possibly somewhere more 56 arbitrary when run from CI. 57 * Paths relative to the CWD require external_updater to be run from a 58 predictable location. Doing so prevents the user from using relative 59 paths (and tab complete) from directories other than the expected location. 60 61 The result for one project should not be reused for other projects, 62 as it's possible that the user has provided project paths from multiple 63 trees. 64 """ 65 if (project / ".repo").exists(): 66 return project 67 if project.parent == project: 68 raise FileNotFoundError( 69 f"Could not find a .repo directory in any parent of {project}" 70 ) 71 return find_tree_containing(project.parent) 72 73 74def external_path() -> Path: 75 """Returns the path to //external. 76 77 We cannot use the relative path from this file to find the top of the 78 tree because this will often be run in a "compiled" form from an 79 arbitrary location in the out directory. We can't fully rely on 80 ANDROID_BUILD_TOP because not all contexts will have run envsetup/lunch 81 either. We use ANDROID_BUILD_TOP whenever it is set, but if it is not set 82 we instead rely on the convention that the CWD is the root of the tree ( 83 updater.sh will cd there before executing). 84 85 There is one other context where this function cannot succeed: CI. Tests 86 run in CI do not have a source tree to find, so calling this function in 87 that context will fail. 88 """ 89 android_top = Path(os.environ.get("ANDROID_BUILD_TOP", os.getcwd())) 90 top = android_top / 'external' 91 92 if not top.exists(): 93 raise RuntimeError( 94 f"{top} does not exist. This program must be run from the " 95 f"root of an Android tree (CWD is {os.getcwd()})." 96 ) 97 return top 98 99 100def get_absolute_project_path(proj_path: Path) -> Path: 101 """Gets absolute path of a project. 102 103 Path resolution starts from external/. 104 """ 105 if proj_path.is_absolute(): 106 return proj_path 107 return external_path() / proj_path 108 109 110def resolve_command_line_paths(paths: list[str]) -> list[Path]: 111 """Resolves project paths provided by the command line. 112 113 Both relative and absolute paths are resolved to fully qualified paths 114 and returned. If any path does not exist relative to the CWD, a message 115 will be printed and that path will be pruned from the list. 116 """ 117 resolved: list[Path] = [] 118 for path_str in paths: 119 path = Path(path_str) 120 if not path.exists(): 121 print(f"Provided path {path} ({path.resolve()}) does not exist. Skipping.") 122 else: 123 resolved.append(path.resolve()) 124 return resolved 125 126 127def get_metadata_path(proj_path: Path) -> Path: 128 """Gets the absolute path of METADATA for a project.""" 129 return get_absolute_project_path(proj_path) / METADATA_FILENAME 130 131 132def get_relative_project_path(proj_path: Path) -> Path: 133 """Gets the relative path of a project starting from external/.""" 134 return get_absolute_project_path(proj_path).relative_to(external_path()) 135 136 137def canonicalize_project_path(proj_path: Path) -> Path: 138 """Returns the canonical representation of the project path. 139 140 For paths that are in the same tree as external_updater (the common 141 case), the canonical path is the path of the project relative to //external. 142 143 For paths that are in a different tree (an uncommon case used for 144 updating projects in other builds such as the NDK), the canonical path is 145 the absolute path. 146 """ 147 try: 148 return get_relative_project_path(proj_path) 149 except ValueError as ex: 150 # A less common use case, but the path might be to a non-local tree, 151 # in which case the path will not be relative to our tree. This 152 # happens when using external_updater in another project like the NDK 153 # or rr. 154 if proj_path.is_absolute(): 155 return proj_path 156 157 # Not relative to //external, and not an absolute path. This case 158 # hasn't existed before, so it has no canonical form. 159 raise ValueError( 160 f"{proj_path} must be either an absolute path or relative to {external_path()}" 161 ) from ex 162 163 164def read_metadata(proj_path: Path) -> metadata_pb2.MetaData: 165 """Reads and parses METADATA file for a project. 166 167 Args: 168 proj_path: Path to the project. 169 170 Returns: 171 Parsed MetaData proto. 172 173 Raises: 174 text_format.ParseError: Occurred when the METADATA file is invalid. 175 FileNotFoundError: Occurred when METADATA file is not found. 176 """ 177 178 with get_metadata_path(proj_path).open('r') as metadata_file: 179 metadata = metadata_file.read() 180 return text_format.Parse(metadata, metadata_pb2.MetaData()) 181 182def convert_url_to_identifier(metadata: metadata_pb2.MetaData) -> metadata_pb2.MetaData: 183 """Converts the old style METADATA to the new style""" 184 for url in metadata.third_party.url: 185 if url.type == metadata_pb2.URL.HOMEPAGE: 186 metadata.third_party.homepage = url.value 187 else: 188 identifier = metadata_pb2.Identifier() 189 identifier.type = IdentifierType[metadata_pb2.URL.Type.Name(url.type)].value 190 identifier.value = url.value 191 identifier.version = metadata.third_party.version 192 metadata.third_party.ClearField("version") 193 metadata.third_party.identifier.append(identifier) 194 metadata.third_party.ClearField("url") 195 return metadata 196 197 198def write_metadata(proj_path: Path, metadata: metadata_pb2.MetaData, keep_date: bool) -> None: 199 """Writes updated METADATA file for a project. 200 201 This function updates last_upgrade_date in metadata and write to the project 202 directory. 203 204 Args: 205 proj_path: Path to the project. 206 metadata: The MetaData proto to write. 207 keep_date: Do not change date. 208 """ 209 210 if not keep_date: 211 date = metadata.third_party.last_upgrade_date 212 now = datetime.datetime.now() 213 date.year = now.year 214 date.month = now.month 215 date.day = now.day 216 try: 217 rel_proj_path = str(get_relative_project_path(proj_path)) 218 except ValueError: 219 # Absolute paths to other trees will not be relative to our tree. 220 # There are no portable instructions for upgrading that project, 221 # since the path will differ between machines (or checkouts). 222 rel_proj_path = "<absolute path to project>" 223 usage_hint = textwrap.dedent(f"""\ 224 # This project was upgraded with external_updater. 225 # Usage: tools/external_updater/updater.sh update external/{rel_proj_path} 226 # For more info, check https://cs.android.com/android/platform/superproject/+/main:tools/external_updater/README.md 227 228 """) 229 text_metadata = usage_hint + text_format.MessageToString(metadata) 230 with get_metadata_path(proj_path).open('w') as metadata_file: 231 if metadata.third_party.license_type == metadata_pb2.LicenseType.BY_EXCEPTION_ONLY: 232 metadata_file.write(textwrap.dedent("""\ 233 # THIS PACKAGE HAS SPECIAL LICENSING CONDITIONS. PLEASE 234 # CONSULT THE OWNERS AND opensource-licensing@google.com BEFORE 235 # DEPENDING ON IT IN YOUR PROJECT. 236 237 """)) 238 metadata_file.write(text_metadata) 239