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