1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
|
#!/usr/bin/env python3
# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""check_cronet_dependencies.py - Keep track of Cronet's dependencies."""
import argparse
import os
import re
import subprocess
import sys
import tempfile
from typing import List, Set
REPOSITORY_ROOT = os.path.abspath(
os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir))
sys.path.insert(0, REPOSITORY_ROOT)
import build.android.gyp.util.build_utils as build_utils # pylint: disable=wrong-import-position
import components.cronet.tools.utils as cronet_utils # pylint: disable=wrong-import-position
_THIRD_PARTY_STR = 'third_party/'
_GN_PATH = os.path.join(REPOSITORY_ROOT, 'buildtools/linux64/gn')
def _get_current_gn_args() -> List[str]:
"""Returns the GN args in the current working directory"""
return subprocess.check_output(["cat", "args.gn"]).decode('utf-8').split("\n")
def normalize_third_party_dep(dependency: str) -> str:
"""Normalizes a GN label that includes `third_party` string
Required because Chromium allows multiple libraries to live under the
same third_party directory (eg: `third_party/android_deps` contains
more than a single library), In order to decrease the failure rate
each time a dependency is added, normalize the `third_party` paths
to its root.
If more than one `third_party` string appears in the GN label, the
last one is picked for normalization. See examples below:
* "//third_party/foo" -> "//third_party/foo"
* "//third_party/foo/bar" -> "//third_party/foo"
* "//third_party/foo/bar/X" -> "//third_party/foo"
* "//third_party/foo/third_party/bar" -> "//third_party/foo/third_party/bar"
Args:
dependency: GN label that represents relative path to a dependency.
Raises:
ValueError: Raised if the dependency is not a third_party dependency.
Returns:
The normalized third_party path.
"""
if _THIRD_PARTY_STR not in dependency:
raise ValueError('Dependency is not a third_party dependency')
root_end_index = dependency.rfind(_THIRD_PARTY_STR) + len(_THIRD_PARTY_STR)
dependency_name_end_index = dependency.find("/", root_end_index)
if dependency_name_end_index == -1:
return dependency
return dependency[:dependency_name_end_index]
def _get_transitive_deps_from_root_targets(out_dir: str,
gn_targets: List[str]) -> Set[str]:
"""Executes gn desc |out_dir| |gn_target| deps --all for each gn target"""
all_deps = set()
for gn_target in gn_targets:
all_deps.update(
subprocess.check_output(
[_GN_PATH, "desc", out_dir, gn_target, "deps",
"--all"]).decode("utf-8").split("\n"))
return all_deps
def normalize_and_dedup_deps(deps: Set[str]) -> Set[str]:
"""Deduplicate after normalizing third_party dependencies
This process involve the following steps:
(1) Remove the target name from the gn label to retrieve
the proper path.
(2) If the gn label involves a third_party dependency then
normalize it according to |normalize_third_party_dep|.
(3) Add the final path after processing to the set.
AndroidX dependencies are a special case and they don't go
through any processing, they are added as is.
Args:
deps: A set of all the dependencies.
Returns:
A sorted collection of normalized deps.
"""
cleaned_deps = set()
for dep in deps:
if not dep:
# Ignore empty lines.
continue
if dep.startswith("//third_party/androidx:") and dep.endswith("_java"):
# We treat androidx dependency differently because
# Cronet MUST NOT depend on any androidx dependency except
# androidx_annotations which is compile-time only. This is
# needed because this is one of mainline restrictions.
# Java/Android targets in GN must end with _java, this is needed
# so we don't bloat the dependencies file with auto-generated targets.
# (eg: androidx_annotation_annotation_java__assetres)
# Don't do any cleaning, add the exact GN label to the dependencies.
cleaned_deps.add(dep)
else:
dep = cronet_utils.get_path_from_gn_label(dep)
if _THIRD_PARTY_STR in dep:
cleaned_deps.add(normalize_third_party_dep(dep))
else:
cleaned_deps.add(dep)
return sorted(cleaned_deps)
def main():
parser = argparse.ArgumentParser(
prog='Check cronet dependencies',
description=
"Checks whether Cronet's current dependencies match the known ones.")
parser.add_argument(
'--root-deps',
nargs="+",
help="""Those are the root dependencies which the script will
use to find the closure of all transitive dependencies.""",
required=True,
)
parser.add_argument(
'--old-dependencies',
type=str,
help='Relative path to file that contains the old dependencies',
required=True,
)
parser.add_argument(
'--stamp',
type=str,
help='Path to touch on success',
)
args = parser.parse_args()
gn_args = _get_current_gn_args()
# remove remoteexec related gn args.
gn_args = [
arg for arg in gn_args
if not re.search(r'use_(remoteexec|reclient|siso)\s*=.*', arg)
]
# make sure it doesn't use remoteexec
gn_args.append('use_remoteexec = false')
# Generate a new GN output directory in order
# not to mess with the current one.
with tempfile.TemporaryDirectory() as tmp_dir_name:
cronet_utils.gn(tmp_dir_name, " ".join(gn_args))
final_deps = normalize_and_dedup_deps(
_get_transitive_deps_from_root_targets(tmp_dir_name, args.root_deps))
golden_deps = cronet_utils.read_file(args.old_dependencies).split("\n")
if not all(dep in golden_deps for dep in final_deps):
# Only generate this text if we found a new dependency
# that does not exist in the golden text. This means
# that we will know not if a dependency gets removed
# as we don't care about that scenario and we don't
# want to block people while cleaning-up code.
print("""
Cronet Dependency check has failed. Please re-generate the golden file:
#######################################################
# #
# Run the command below to generate the file #
# #
#######################################################
########### START ###########
patch -p1 << 'END_DIFF'
%s
END_DIFF
############ END ############
""" % cronet_utils.compare_text_and_generate_diff(
'\n'.join(final_deps), cronet_utils.read_file(args.old_dependencies),
args.old_dependencies))
return -1
build_utils.Touch(args.stamp)
return 0
if __name__ == '__main__':
sys.exit(main())
|