Patrick Rohr | 92d7412 | 2022-10-21 15:50:52 -0700 | [diff] [blame] | 1 | # Copyright (C) 2022 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 | |
| 15 | # A collection of utilities for extracting build rule information from GN |
| 16 | # projects. |
| 17 | |
| 18 | from __future__ import print_function |
| 19 | import collections |
| 20 | import errno |
| 21 | import filecmp |
| 22 | import json |
Patrick Rohr | af92fa6 | 2022-11-04 14:27:04 -0700 | [diff] [blame^] | 23 | import logging as log |
Patrick Rohr | 92d7412 | 2022-10-21 15:50:52 -0700 | [diff] [blame] | 24 | import os |
| 25 | import re |
| 26 | import shutil |
| 27 | import subprocess |
| 28 | import sys |
Patrick Rohr | 92d7412 | 2022-10-21 15:50:52 -0700 | [diff] [blame] | 29 | |
| 30 | BUILDFLAGS_TARGET = '//gn:gen_buildflags' |
| 31 | GEN_VERSION_TARGET = '//src/base:version_gen_h' |
| 32 | TARGET_TOOLCHAIN = '//gn/standalone/toolchain:gcc_like_host' |
| 33 | HOST_TOOLCHAIN = '//gn/standalone/toolchain:gcc_like_host' |
| 34 | LINKER_UNIT_TYPES = ('executable', 'shared_library', 'static_library') |
| 35 | |
| 36 | # TODO(primiano): investigate these, they require further componentization. |
| 37 | ODR_VIOLATION_IGNORE_TARGETS = { |
| 38 | '//test/cts:perfetto_cts_deps', |
| 39 | '//:perfetto_integrationtests', |
| 40 | } |
| 41 | |
| 42 | |
Patrick Rohr | 92d7412 | 2022-10-21 15:50:52 -0700 | [diff] [blame] | 43 | def repo_root(): |
| 44 | """Returns an absolute path to the repository root.""" |
| 45 | return os.path.join( |
| 46 | os.path.realpath(os.path.dirname(__file__)), os.path.pardir) |
| 47 | |
| 48 | |
Patrick Rohr | 92d7412 | 2022-10-21 15:50:52 -0700 | [diff] [blame] | 49 | def label_to_path(label): |
| 50 | """Turn a GN output label (e.g., //some_dir/file.cc) into a path.""" |
| 51 | assert label.startswith('//') |
Patrick Rohr | c6331c8 | 2022-10-25 11:34:20 -0700 | [diff] [blame] | 52 | return label[2:] or "./" |
Patrick Rohr | 92d7412 | 2022-10-21 15:50:52 -0700 | [diff] [blame] | 53 | |
| 54 | |
| 55 | def label_without_toolchain(label): |
| 56 | """Strips the toolchain from a GN label. |
| 57 | |
| 58 | Return a GN label (e.g //buildtools:protobuf(//gn/standalone/toolchain: |
| 59 | gcc_like_host) without the parenthesised toolchain part. |
| 60 | """ |
| 61 | return label.split('(')[0] |
| 62 | |
| 63 | |
| 64 | def label_to_target_name_with_path(label): |
| 65 | """ |
| 66 | Turn a GN label into a target name involving the full path. |
| 67 | e.g., //src/perfetto:tests -> src_perfetto_tests |
| 68 | """ |
| 69 | name = re.sub(r'^//:?', '', label) |
| 70 | name = re.sub(r'[^a-zA-Z0-9_]', '_', name) |
| 71 | return name |
| 72 | |
| 73 | |
Patrick Rohr | 92d7412 | 2022-10-21 15:50:52 -0700 | [diff] [blame] | 74 | class GnParser(object): |
| 75 | """A parser with some cleverness for GN json desc files |
| 76 | |
| 77 | The main goals of this parser are: |
| 78 | 1) Deal with the fact that other build systems don't have an equivalent |
| 79 | notion to GN's source_set. Conversely to Bazel's and Soong's filegroups, |
| 80 | GN source_sets expect that dependencies, cflags and other source_set |
| 81 | properties propagate up to the linker unit (static_library, executable or |
| 82 | shared_library). This parser simulates the same behavior: when a |
| 83 | source_set is encountered, some of its variables (cflags and such) are |
| 84 | copied up to the dependent targets. This is to allow gen_xxx to create |
| 85 | one filegroup for each source_set and then squash all the other flags |
| 86 | onto the linker unit. |
| 87 | 2) Detect and special-case protobuf targets, figuring out the protoc-plugin |
| 88 | being used. |
| 89 | """ |
| 90 | |
| 91 | class Target(object): |
| 92 | """Reperesents A GN target. |
| 93 | |
| 94 | Maked properties are propagated up the dependency chain when a |
| 95 | source_set dependency is encountered. |
| 96 | """ |
| 97 | |
| 98 | def __init__(self, name, type): |
| 99 | self.name = name # e.g. //src/ipc:ipc |
| 100 | |
| 101 | VALID_TYPES = ('static_library', 'shared_library', 'executable', 'group', |
Patrick Rohr | da778a0 | 2022-10-25 16:17:31 -0700 | [diff] [blame] | 102 | 'action', 'source_set', 'proto_library', 'copy', 'action_foreach') |
Patrick Rohr | 92d7412 | 2022-10-21 15:50:52 -0700 | [diff] [blame] | 103 | assert (type in VALID_TYPES) |
| 104 | self.type = type |
| 105 | self.testonly = False |
| 106 | self.toolchain = None |
| 107 | |
| 108 | # These are valid only for type == proto_library. |
| 109 | # This is typically: 'proto', 'protozero', 'ipc'. |
| 110 | self.proto_plugin = None |
| 111 | self.proto_paths = set() |
| 112 | self.proto_exports = set() |
| 113 | |
| 114 | self.sources = set() |
| 115 | # TODO(primiano): consider whether the public section should be part of |
| 116 | # bubbled-up sources. |
| 117 | self.public_headers = set() # 'public' |
| 118 | |
| 119 | # These are valid only for type == 'action' |
| 120 | self.inputs = set() |
| 121 | self.outputs = set() |
| 122 | self.script = None |
| 123 | self.args = [] |
Patrick Rohr | 09716f5 | 2022-10-27 13:02:36 -0700 | [diff] [blame] | 124 | self.response_file_contents = None |
Patrick Rohr | 92d7412 | 2022-10-21 15:50:52 -0700 | [diff] [blame] | 125 | |
| 126 | # These variables are propagated up when encountering a dependency |
| 127 | # on a source_set target. |
| 128 | self.cflags = set() |
| 129 | self.defines = set() |
| 130 | self.deps = set() |
| 131 | self.libs = set() |
| 132 | self.include_dirs = set() |
| 133 | self.ldflags = set() |
| 134 | self.source_set_deps = set() # Transitive set of source_set deps. |
| 135 | self.proto_deps = set() |
| 136 | self.transitive_proto_deps = set() |
| 137 | |
| 138 | # Deps on //gn:xxx have this flag set to True. These dependencies |
| 139 | # are special because they pull third_party code from buildtools/. |
| 140 | # We don't want to keep recursing into //buildtools in generators, |
| 141 | # this flag is used to stop the recursion and create an empty |
| 142 | # placeholder target once we hit //gn:protoc or similar. |
| 143 | self.is_third_party_dep_ = False |
| 144 | |
| 145 | def __lt__(self, other): |
| 146 | if isinstance(other, self.__class__): |
| 147 | return self.name < other.name |
| 148 | raise TypeError( |
| 149 | '\'<\' not supported between instances of \'%s\' and \'%s\'' % |
| 150 | (type(self).__name__, type(other).__name__)) |
| 151 | |
| 152 | def __repr__(self): |
| 153 | return json.dumps({ |
| 154 | k: (list(sorted(v)) if isinstance(v, set) else v) |
Patrick Rohr | 23f2619 | 2022-10-25 09:45:22 -0700 | [diff] [blame] | 155 | for (k, v) in self.__dict__.items() |
Patrick Rohr | 92d7412 | 2022-10-21 15:50:52 -0700 | [diff] [blame] | 156 | }, |
| 157 | indent=4, |
| 158 | sort_keys=True) |
| 159 | |
| 160 | def update(self, other): |
| 161 | for key in ('cflags', 'defines', 'deps', 'include_dirs', 'ldflags', |
| 162 | 'source_set_deps', 'proto_deps', 'transitive_proto_deps', |
| 163 | 'libs', 'proto_paths'): |
| 164 | self.__dict__[key].update(other.__dict__.get(key, [])) |
| 165 | |
| 166 | def __init__(self, gn_desc): |
| 167 | self.gn_desc_ = gn_desc |
| 168 | self.all_targets = {} |
| 169 | self.linker_units = {} # Executables, shared or static libraries. |
| 170 | self.source_sets = {} |
| 171 | self.actions = {} |
| 172 | self.proto_libs = {} |
| 173 | |
Patrick Rohr | 09716f5 | 2022-10-27 13:02:36 -0700 | [diff] [blame] | 174 | def _get_response_file_contents(self, action_desc): |
Patrick Rohr | c20887d | 2022-10-28 12:59:20 -0700 | [diff] [blame] | 175 | # response_file_contents are formatted as: |
| 176 | # ['--flags', '--flag=true && false'] and need to be formatted as: |
| 177 | # '--flags --flag=\"true && false\"' |
| 178 | flags = action_desc.get('response_file_contents', []) |
| 179 | formatted_flags = [] |
| 180 | for flag in flags: |
| 181 | if '=' in flag: |
| 182 | key, val = flag.split('=') |
| 183 | formatted_flags.append('%s=\\"%s\\"' % (key, val)) |
| 184 | else: |
| 185 | formatted_flags.append(flag) |
| 186 | |
| 187 | return ' '.join(formatted_flags) |
Patrick Rohr | 09716f5 | 2022-10-27 13:02:36 -0700 | [diff] [blame] | 188 | |
Patrick Rohr | af92fa6 | 2022-11-04 14:27:04 -0700 | [diff] [blame^] | 189 | def _is_java_target(self, target): |
| 190 | # Per https://chromium.googlesource.com/chromium/src/build/+/HEAD/android/docs/java_toolchain.md |
| 191 | # java target names must end in "_java". |
| 192 | # TODO: There are some other possible variations we might need to support. |
| 193 | return re.match('.*_java$', target.name) |
| 194 | |
| 195 | |
Patrick Rohr | 92d7412 | 2022-10-21 15:50:52 -0700 | [diff] [blame] | 196 | def get_target(self, gn_target_name): |
| 197 | """Returns a Target object from the fully qualified GN target name. |
| 198 | |
| 199 | It bubbles up variables from source_set dependencies as described in the |
| 200 | class-level comments. |
| 201 | """ |
| 202 | target = self.all_targets.get(gn_target_name) |
| 203 | if target is not None: |
| 204 | return target # Target already processed. |
| 205 | |
| 206 | desc = self.gn_desc_[gn_target_name] |
| 207 | target = GnParser.Target(gn_target_name, desc['type']) |
| 208 | target.testonly = desc.get('testonly', False) |
| 209 | target.toolchain = desc.get('toolchain', None) |
| 210 | self.all_targets[gn_target_name] = target |
| 211 | |
Patrick Rohr | 26af1e7 | 2022-10-25 12:11:05 -0700 | [diff] [blame] | 212 | # TODO: determine if below comment should apply for cronet builds in Android. |
Patrick Rohr | 92d7412 | 2022-10-21 15:50:52 -0700 | [diff] [blame] | 213 | # We should never have GN targets directly depend on buidtools. They |
| 214 | # should hop via //gn:xxx, so we can give generators an opportunity to |
| 215 | # override them. |
Patrick Rohr | 26af1e7 | 2022-10-25 12:11:05 -0700 | [diff] [blame] | 216 | # Specifically allow targets to depend on libc++ and libunwind. |
| 217 | if not any(match in gn_target_name for match in ['libc++', 'libunwind']): |
| 218 | assert (not gn_target_name.startswith('//buildtools')) |
| 219 | |
Patrick Rohr | 92d7412 | 2022-10-21 15:50:52 -0700 | [diff] [blame] | 220 | |
| 221 | # Don't descend further into third_party targets. Genrators are supposed |
| 222 | # to either ignore them or route to other externally-provided targets. |
| 223 | if gn_target_name.startswith('//gn'): |
| 224 | target.is_third_party_dep_ = True |
| 225 | return target |
| 226 | |
| 227 | proto_target_type, proto_desc = self.get_proto_target_type(target) |
| 228 | if proto_target_type is not None: |
| 229 | self.proto_libs[target.name] = target |
| 230 | target.type = 'proto_library' |
| 231 | target.proto_plugin = proto_target_type |
| 232 | target.proto_paths.update(self.get_proto_paths(proto_desc)) |
| 233 | target.proto_exports.update(self.get_proto_exports(proto_desc)) |
| 234 | target.sources.update(proto_desc.get('sources', [])) |
| 235 | assert (all(x.endswith('.proto') for x in target.sources)) |
| 236 | elif target.type == 'source_set': |
| 237 | self.source_sets[gn_target_name] = target |
| 238 | target.sources.update(desc.get('sources', [])) |
| 239 | elif target.type in LINKER_UNIT_TYPES: |
| 240 | self.linker_units[gn_target_name] = target |
| 241 | target.sources.update(desc.get('sources', [])) |
Patrick Rohr | da778a0 | 2022-10-25 16:17:31 -0700 | [diff] [blame] | 242 | elif target.type in ['action', 'action_foreach']: |
Patrick Rohr | 92d7412 | 2022-10-21 15:50:52 -0700 | [diff] [blame] | 243 | self.actions[gn_target_name] = target |
| 244 | target.inputs.update(desc.get('inputs', [])) |
| 245 | target.sources.update(desc.get('sources', [])) |
| 246 | outs = [re.sub('^//out/.+?/gen/', '', x) for x in desc['outputs']] |
| 247 | target.outputs.update(outs) |
| 248 | target.script = desc['script'] |
Patrick Rohr | 7aa98f9 | 2022-10-28 11:16:36 -0700 | [diff] [blame] | 249 | target.args = desc['args'] |
Patrick Rohr | 09716f5 | 2022-10-27 13:02:36 -0700 | [diff] [blame] | 250 | target.response_file_contents = self._get_response_file_contents(desc) |
Patrick Rohr | da778a0 | 2022-10-25 16:17:31 -0700 | [diff] [blame] | 251 | elif target.type == 'copy': |
| 252 | # TODO: copy rules are not currently implemented. |
| 253 | self.actions[gn_target_name] = target |
Patrick Rohr | af92fa6 | 2022-11-04 14:27:04 -0700 | [diff] [blame^] | 254 | elif target.type == 'group' and self._is_java_target(target): |
| 255 | # java_group identifies the group target generated by the android_library |
| 256 | # or java_library template. A java_group must not be added as a dependency, but sources are collected |
| 257 | log.debug('Found java target %s', target.name) |
| 258 | target.type = 'java_group' |
Patrick Rohr | 92d7412 | 2022-10-21 15:50:52 -0700 | [diff] [blame] | 259 | |
| 260 | # Default for 'public' is //* - all headers in 'sources' are public. |
| 261 | # TODO(primiano): if a 'public' section is specified (even if empty), then |
| 262 | # the rest of 'sources' is considered inaccessible by gn. Consider |
| 263 | # emulating that, so that generated build files don't end up with overly |
| 264 | # accessible headers. |
| 265 | public_headers = [x for x in desc.get('public', []) if x != '*'] |
| 266 | target.public_headers.update(public_headers) |
| 267 | |
| 268 | target.cflags.update(desc.get('cflags', []) + desc.get('cflags_cc', [])) |
| 269 | target.libs.update(desc.get('libs', [])) |
| 270 | target.ldflags.update(desc.get('ldflags', [])) |
| 271 | target.defines.update(desc.get('defines', [])) |
| 272 | target.include_dirs.update(desc.get('include_dirs', [])) |
| 273 | |
| 274 | # Recurse in dependencies. |
| 275 | for dep_name in desc.get('deps', []): |
| 276 | dep = self.get_target(dep_name) |
| 277 | if dep.is_third_party_dep_: |
| 278 | target.deps.add(dep_name) |
| 279 | elif dep.type == 'proto_library': |
| 280 | target.proto_deps.add(dep_name) |
| 281 | target.transitive_proto_deps.add(dep_name) |
| 282 | target.proto_paths.update(dep.proto_paths) |
| 283 | target.transitive_proto_deps.update(dep.transitive_proto_deps) |
| 284 | elif dep.type == 'source_set': |
| 285 | target.source_set_deps.add(dep_name) |
| 286 | target.update(dep) # Bubble up source set's cflags/ldflags etc. |
| 287 | elif dep.type == 'group': |
| 288 | target.update(dep) # Bubble up groups's cflags/ldflags etc. |
Patrick Rohr | da778a0 | 2022-10-25 16:17:31 -0700 | [diff] [blame] | 289 | elif dep.type in ['action', 'action_foreach', 'copy']: |
Patrick Rohr | 92d7412 | 2022-10-21 15:50:52 -0700 | [diff] [blame] | 290 | if proto_target_type is None: |
| 291 | target.deps.add(dep_name) |
| 292 | elif dep.type in LINKER_UNIT_TYPES: |
| 293 | target.deps.add(dep_name) |
| 294 | |
| 295 | return target |
| 296 | |
| 297 | def get_proto_exports(self, proto_desc): |
| 298 | # exports in metadata will be available for source_set targets. |
| 299 | metadata = proto_desc.get('metadata', {}) |
| 300 | return metadata.get('exports', []) |
| 301 | |
| 302 | def get_proto_paths(self, proto_desc): |
| 303 | # import_dirs in metadata will be available for source_set targets. |
| 304 | metadata = proto_desc.get('metadata', {}) |
| 305 | return metadata.get('import_dirs', []) |
| 306 | |
| 307 | def get_proto_target_type(self, target): |
| 308 | """ Checks if the target is a proto library and return the plugin. |
| 309 | |
| 310 | Returns: |
| 311 | (None, None): if the target is not a proto library. |
| 312 | (plugin, proto_desc) where |plugin| is 'proto' in the default (lite) |
| 313 | case or 'protozero' or 'ipc' or 'descriptor'; |proto_desc| is the GN |
| 314 | json desc of the target with the .proto sources (_gen target for |
| 315 | non-descriptor types or the target itself for descriptor type). |
| 316 | """ |
| 317 | parts = target.name.split('(', 1) |
| 318 | name = parts[0] |
| 319 | toolchain = '(' + parts[1] if len(parts) > 1 else '' |
| 320 | |
| 321 | # Descriptor targets don't have a _gen target; instead we look for the |
| 322 | # characteristic flag in the args of the target itself. |
| 323 | desc = self.gn_desc_.get(target.name) |
| 324 | if '--descriptor_set_out' in desc.get('args', []): |
| 325 | return 'descriptor', desc |
| 326 | |
| 327 | # Source set proto targets have a non-empty proto_library_sources in the |
| 328 | # metadata of the description. |
| 329 | metadata = desc.get('metadata', {}) |
| 330 | if 'proto_library_sources' in metadata: |
| 331 | return 'source_set', desc |
| 332 | |
| 333 | # In all other cases, we want to look at the _gen target as that has the |
| 334 | # important information. |
| 335 | gen_desc = self.gn_desc_.get('%s_gen%s' % (name, toolchain)) |
| 336 | if gen_desc is None or gen_desc['type'] != 'action': |
| 337 | return None, None |
| 338 | args = gen_desc.get('args', []) |
| 339 | if '/protoc' not in args[0]: |
| 340 | return None, None |
| 341 | plugin = 'proto' |
| 342 | for arg in (arg for arg in args if arg.startswith('--plugin=')): |
| 343 | # |arg| at this point looks like: |
| 344 | # --plugin=protoc-gen-plugin=gcc_like_host/protozero_plugin |
| 345 | # or |
| 346 | # --plugin=protoc-gen-plugin=protozero_plugin |
| 347 | plugin = arg.split('=')[-1].split('/')[-1].replace('_plugin', '') |
| 348 | return plugin, gen_desc |