• 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
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