File: check_cronet_dependencies.py

package info (click to toggle)
chromium 139.0.7258.127-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 6,122,156 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (194 lines) | stat: -rwxr-xr-x 6,836 bytes parent folder | download | duplicates (5)
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())