xref: /aosp_15_r20/tools/external_updater/fileutils.py (revision 3c875a214f382db1236d28570d1304ce57138f32)
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
182
183def convert_url_to_identifier(metadata: metadata_pb2.MetaData) -> metadata_pb2.MetaData:
184    """Converts the old style METADATA to the new style"""
185    for url in metadata.third_party.url:
186        if url.type == metadata_pb2.URL.HOMEPAGE:
187            metadata.third_party.homepage = url.value
188        else:
189            identifier = metadata_pb2.Identifier()
190            identifier.type = IdentifierType[metadata_pb2.URL.Type.Name(url.type)].value
191            identifier.value = url.value
192            identifier.version = metadata.third_party.version
193            metadata.third_party.ClearField("version")
194            metadata.third_party.identifier.append(identifier)
195    metadata.third_party.ClearField("url")
196    return metadata
197
198
199def write_metadata(proj_path: Path, metadata: metadata_pb2.MetaData, keep_date: bool) -> None:
200    """Writes updated METADATA file for a project.
201
202    This function updates last_upgrade_date in metadata and write to the project
203    directory.
204
205    Args:
206      proj_path: Path to the project.
207      metadata: The MetaData proto to write.
208      keep_date: Do not change date.
209    """
210
211    if not keep_date:
212        date = metadata.third_party.last_upgrade_date
213        now = datetime.datetime.now()
214        date.year = now.year
215        date.month = now.month
216        date.day = now.day
217    try:
218        rel_proj_path = str(get_relative_project_path(proj_path))
219    except ValueError:
220        # Absolute paths to other trees will not be relative to our tree.
221        # There are no portable instructions for upgrading that project,
222        # since the path will differ between machines (or checkouts).
223        rel_proj_path = "<absolute path to project>"
224    usage_hint = textwrap.dedent(f"""\
225    # This project was upgraded with external_updater.
226    # Usage: tools/external_updater/updater.sh update external/{rel_proj_path}
227    # For more info, check https://cs.android.com/android/platform/superproject/main/+/main:tools/external_updater/README.md
228
229    """)
230    text_metadata = usage_hint + text_format.MessageToString(metadata)
231    with get_metadata_path(proj_path).open('w') as metadata_file:
232        if metadata.third_party.license_type == metadata_pb2.LicenseType.BY_EXCEPTION_ONLY:
233            metadata_file.write(textwrap.dedent("""\
234            # THIS PACKAGE HAS SPECIAL LICENSING CONDITIONS. PLEASE
235            # CONSULT THE OWNERS AND [email protected] BEFORE
236            # DEPENDING ON IT IN YOUR PROJECT.
237
238            """))
239        metadata_file.write(text_metadata)
240