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 17from functools import cache 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@cache 33def external_path() -> Path: 34 """Returns the path to //external. 35 36 We cannot use the relative path from this file to find the top of the tree because 37 this will often be run in a "compiled" form from an arbitrary location in the out 38 directory. We can't fully rely on ANDROID_BUILD_TOP because not all contexts will 39 have run envsetup/lunch either. We use ANDROID_BUILD_TOP whenever it is set, but if 40 it is not set we instead rely on the convention that the CWD is the root of the tree 41 (updater.sh will cd there before executing). 42 43 There is one other context where this function cannot succeed: CI. Tests run in CI 44 do not have a source tree to find, so calling this function in that context will 45 fail. 46 """ 47 android_top = Path(os.environ.get("ANDROID_BUILD_TOP", os.getcwd())) 48 top = android_top / 'external' 49 50 if not top.exists(): 51 raise RuntimeError( 52 f"{top} does not exist. This program must be run from the " 53 f"root of an Android tree (CWD is {os.getcwd()})." 54 ) 55 return top 56 57 58def get_absolute_project_path(proj_path: Path) -> Path: 59 """Gets absolute path of a project. 60 61 Path resolution starts from external/. 62 """ 63 return external_path() / proj_path 64 65 66def get_metadata_path(proj_path: Path) -> Path: 67 """Gets the absolute path of METADATA for a project.""" 68 return get_absolute_project_path(proj_path) / METADATA_FILENAME 69 70 71def get_relative_project_path(proj_path: Path) -> Path: 72 """Gets the relative path of a project starting from external/.""" 73 return get_absolute_project_path(proj_path).relative_to(external_path()) 74 75 76def canonicalize_project_path(proj_path: Path) -> Path: 77 """Returns the canonical representation of the project path. 78 79 For paths that are in the same tree as external_updater (the common case), the 80 canonical path is the path of the project relative to //external. 81 82 For paths that are in a different tree (an uncommon case used for updating projects 83 in other builds such as the NDK), the canonical path is the absolute path. 84 """ 85 try: 86 return get_relative_project_path(proj_path) 87 except ValueError: 88 # A less common use case, but the path might be to a non-local tree, in which case 89 # the path will not be relative to our tree. This happens when using 90 # external_updater in another project like the NDK or rr. 91 if proj_path.is_absolute(): 92 return proj_path 93 94 # Not relative to //external, and not an absolute path. This case hasn't existed 95 # before, so it has no canonical form. 96 raise ValueError( 97 f"{proj_path} must be either an absolute path or relative to {external_path()}" 98 ) 99 100 101def read_metadata(proj_path: Path) -> metadata_pb2.MetaData: 102 """Reads and parses METADATA file for a project. 103 104 Args: 105 proj_path: Path to the project. 106 107 Returns: 108 Parsed MetaData proto. 109 110 Raises: 111 text_format.ParseError: Occurred when the METADATA file is invalid. 112 FileNotFoundError: Occurred when METADATA file is not found. 113 """ 114 115 with get_metadata_path(proj_path).open('r') as metadata_file: 116 metadata = metadata_file.read() 117 return text_format.Parse(metadata, metadata_pb2.MetaData()) 118 119 120def write_metadata(proj_path: Path, metadata: metadata_pb2.MetaData, keep_date: bool) -> None: 121 """Writes updated METADATA file for a project. 122 123 This function updates last_upgrade_date in metadata and write to the project 124 directory. 125 126 Args: 127 proj_path: Path to the project. 128 metadata: The MetaData proto to write. 129 keep_date: Do not change date. 130 """ 131 132 if not keep_date: 133 date = metadata.third_party.last_upgrade_date 134 now = datetime.datetime.now() 135 date.year = now.year 136 date.month = now.month 137 date.day = now.day 138 try: 139 rel_proj_path = str(get_relative_project_path(proj_path)) 140 except ValueError: 141 # Absolute paths to other trees will not be relative to our tree. There are 142 # not portable instructions for upgrading that project, since the path will 143 # differ between machines (or checkouts). 144 rel_proj_path = "<absolute path to project>" 145 usage_hint = textwrap.dedent(f"""\ 146 # This project was upgraded with external_updater. 147 # Usage: tools/external_updater/updater.sh update {rel_proj_path} 148 # For more info, check https://cs.android.com/android/platform/superproject/+/master:tools/external_updater/README.md 149 150 """) 151 text_metadata = usage_hint + text_format.MessageToString(metadata) 152 with get_metadata_path(proj_path).open('w') as metadata_file: 153 if metadata.third_party.license_type == metadata_pb2.LicenseType.BY_EXCEPTION_ONLY: 154 metadata_file.write(textwrap.dedent("""\ 155 # THIS PACKAGE HAS SPECIAL LICENSING CONDITIONS. PLEASE 156 # CONSULT THE OWNERS AND opensource-licensing@google.com BEFORE 157 # DEPENDING ON IT IN YOUR PROJECT. 158 159 """)) 160 metadata_file.write(text_metadata) 161