1# Copyright (C) 2019 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 18from __future__ import print_function 19import collections 20from compat import iteritems 21import errno 22import filecmp 23import json 24import os 25import re 26import shutil 27import subprocess 28import sys 29from typing import Dict 30from typing import Optional 31from typing import Set 32from typing import Tuple 33 34BUILDFLAGS_TARGET = '//gn:gen_buildflags' 35GEN_VERSION_TARGET = '//src/base:version_gen_h' 36TARGET_TOOLCHAIN = '//gn/standalone/toolchain:gcc_like_host' 37HOST_TOOLCHAIN = '//gn/standalone/toolchain:gcc_like_host' 38LINKER_UNIT_TYPES = ('executable', 'shared_library', 'static_library') 39 40# TODO(primiano): investigate these, they require further componentization. 41ODR_VIOLATION_IGNORE_TARGETS = { 42 '//test/cts:perfetto_cts_deps', 43 '//:perfetto_integrationtests', 44} 45 46 47def _check_command_output(cmd, cwd): 48 try: 49 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, cwd=cwd) 50 except subprocess.CalledProcessError as e: 51 print( 52 'Command "{}" failed in {}:'.format(' '.join(cmd), cwd), 53 file=sys.stderr) 54 print(e.output.decode(), file=sys.stderr) 55 sys.exit(1) 56 else: 57 return output.decode() 58 59 60def repo_root(): 61 """Returns an absolute path to the repository root.""" 62 return os.path.join( 63 os.path.realpath(os.path.dirname(__file__)), os.path.pardir) 64 65 66def _tool_path(name, system_buildtools=False): 67 # Pass-through to use name if the caller requests to use the system 68 # toolchain. 69 if system_buildtools: 70 return [name] 71 wrapper = os.path.abspath( 72 os.path.join(repo_root(), 'tools', 'run_buildtools_binary.py')) 73 return ['python3', wrapper, name] 74 75 76def prepare_out_directory(gn_args, 77 name, 78 root=repo_root(), 79 system_buildtools=False): 80 """Creates the JSON build description by running GN. 81 82 Returns (path, desc) where |path| is the location of the output directory 83 and |desc| is the JSON build description. 84 """ 85 out = os.path.join(root, 'out', name) 86 try: 87 os.makedirs(out) 88 except OSError as e: 89 if e.errno != errno.EEXIST: 90 raise 91 _check_command_output( 92 _tool_path('gn', system_buildtools) + 93 ['gen', out, '--args=%s' % gn_args], 94 cwd=repo_root()) 95 return out 96 97 98def load_build_description(out, system_buildtools=False): 99 """Creates the JSON build description by running GN.""" 100 desc = _check_command_output( 101 _tool_path('gn', system_buildtools) + 102 ['desc', out, '--format=json', '--all-toolchains', '//*'], 103 cwd=repo_root()) 104 return json.loads(desc) 105 106 107def create_build_description(gn_args, root=repo_root()): 108 """Prepares a GN out directory and loads the build description from it. 109 110 The temporary out directory is automatically deleted. 111 """ 112 out = prepare_out_directory(gn_args, 'tmp.gn_utils', root=root) 113 try: 114 return load_build_description(out) 115 finally: 116 shutil.rmtree(out) 117 118 119def build_targets(out, targets, quiet=False, system_buildtools=False): 120 """Runs ninja to build a list of GN targets in the given out directory. 121 122 Compiling these targets is required so that we can include any generated 123 source files in the amalgamated result. 124 """ 125 targets = [t.replace('//', '') for t in targets] 126 with open(os.devnull, 'w') as devnull: 127 stdout = devnull if quiet else None 128 cmd = _tool_path('ninja', system_buildtools) + targets 129 subprocess.check_call(cmd, cwd=os.path.abspath(out), stdout=stdout) 130 131 132def compute_source_dependencies(out, system_buildtools=False): 133 """For each source file, computes a set of headers it depends on.""" 134 ninja_deps = _check_command_output( 135 _tool_path('ninja', system_buildtools) + ['-t', 'deps'], cwd=out) 136 deps = {} 137 current_source = None 138 for line in ninja_deps.split('\n'): 139 filename = os.path.relpath(os.path.join(out, line.strip()), repo_root()) 140 if not line or line[0] != ' ': 141 current_source = None 142 continue 143 elif not current_source: 144 # We're assuming the source file is always listed before the 145 # headers. 146 assert os.path.splitext(line)[1] in ['.c', '.cc', '.cpp', '.S'] 147 current_source = filename 148 deps[current_source] = [] 149 else: 150 assert current_source 151 deps[current_source].append(filename) 152 return deps 153 154 155def label_to_path(label): 156 """Turn a GN output label (e.g., //some_dir/file.cc) into a path.""" 157 assert label.startswith('//') 158 return label[2:] 159 160 161def label_without_toolchain(label): 162 """Strips the toolchain from a GN label. 163 164 Return a GN label (e.g //buildtools:protobuf(//gn/standalone/toolchain: 165 gcc_like_host) without the parenthesised toolchain part. 166 """ 167 return label.split('(')[0] 168 169 170def label_to_target_name_with_path(label): 171 """ 172 Turn a GN label into a target name involving the full path. 173 e.g., //src/perfetto:tests -> src_perfetto_tests 174 """ 175 name = re.sub(r'^//:?', '', label) 176 name = re.sub(r'[^a-zA-Z0-9_]', '_', name) 177 return name 178 179 180def gen_buildflags(gn_args, target_file): 181 """Generates the perfetto_build_flags.h for the given config. 182 183 target_file: the path, relative to the repo root, where the generated 184 buildflag header will be copied into. 185 """ 186 tmp_out = prepare_out_directory(gn_args, 'tmp.gen_buildflags') 187 build_targets(tmp_out, [BUILDFLAGS_TARGET], quiet=True) 188 src = os.path.join(tmp_out, 'gen', 'build_config', 'perfetto_build_flags.h') 189 shutil.copy(src, os.path.join(repo_root(), target_file)) 190 shutil.rmtree(tmp_out) 191 192 193def check_or_commit_generated_files(tmp_files, check): 194 """Checks that gen files are unchanged or renames them to the final location 195 196 Takes in input a list of 'xxx.swp' files that have been written. 197 If check == False, it renames xxx.swp -> xxx. 198 If check == True, it just checks that the contents of 'xxx.swp' == 'xxx'. 199 Returns 0 if no diff was detected, 1 otherwise (to be used as exit code). 200 """ 201 res = 0 202 for tmp_file in tmp_files: 203 assert (tmp_file.endswith('.swp')) 204 target_file = os.path.relpath(tmp_file[:-4]) 205 if check: 206 if not filecmp.cmp(tmp_file, target_file): 207 sys.stderr.write('%s needs to be regenerated\n' % target_file) 208 res = 1 209 os.unlink(tmp_file) 210 else: 211 os.rename(tmp_file, target_file) 212 return res 213 214 215class ODRChecker(object): 216 """Detects ODR violations in linker units 217 218 When we turn GN source sets into Soong & Bazel file groups, there is the risk 219 to create ODR violations by including the same file group into different 220 linker unit (this is because other build systems don't have a concept 221 equivalent to GN's source_set). This class navigates the transitive 222 dependencies (mostly static libraries) of a target and detects if multiple 223 paths end up including the same file group. This is to avoid situations like: 224 225 traced.exe -> base(file group) 226 traced.exe -> libperfetto(static lib) -> base(file group) 227 """ 228 229 def __init__(self, gn: 'GnParser', target_name: str): 230 self.gn = gn 231 self.root = gn.get_target(target_name) 232 self.source_sets: Dict[str, Set[str]] = collections.defaultdict(set) 233 self.deps_visited = set() 234 self.source_set_hdr_only = {} 235 236 self._visit(target_name) 237 num_violations = 0 238 if target_name in ODR_VIOLATION_IGNORE_TARGETS: 239 return 240 for sset, paths in self.source_sets.items(): 241 if self.is_header_only(sset): 242 continue 243 if len(paths) != 1: 244 num_violations += 1 245 print( 246 'ODR violation in target %s, multiple paths include %s:\n %s' % 247 (target_name, sset, '\n '.join(paths)), 248 file=sys.stderr) 249 if num_violations > 0: 250 raise Exception('%d ODR violations detected. Build generation aborted' % 251 num_violations) 252 253 def _visit(self, target_name: str, parent_path=''): 254 target = self.gn.get_target(target_name) 255 path = ((parent_path + ' > ') if parent_path else '') + target_name 256 if not target: 257 raise Exception('Cannot find target %s' % target_name) 258 for ssdep in target.transitive_source_set_deps(): 259 name_and_path = '%s (via %s)' % (target_name, path) 260 self.source_sets[ssdep.name].add(name_and_path) 261 deps = set(target.non_proto_or_source_set_deps()).union( 262 target.transitive_proto_deps()) - self.deps_visited 263 for dep in deps: 264 if dep.type == 'executable': 265 continue # Execs are strong boundaries and don't cause ODR violations. 266 # static_library dependencies should reset the path. It doesn't matter if 267 # we get to a source file via: 268 # source_set1 > static_lib > source.cc OR 269 # source_set1 > source_set2 > static_lib > source.cc 270 # This is NOT an ODR violation because source.cc is linked from the same 271 # static library 272 next_parent_path = path if dep.type != 'static_library' else '' 273 self.deps_visited.add(dep.name) 274 self._visit(dep.name, next_parent_path) 275 276 def is_header_only(self, source_set_name: str): 277 cached = self.source_set_hdr_only.get(source_set_name) 278 if cached is not None: 279 return cached 280 target = self.gn.get_target(source_set_name) 281 if target.type != 'source_set': 282 raise TypeError('%s is not a source_set' % source_set_name) 283 res = all(src.endswith('.h') for src in target.sources) 284 self.source_set_hdr_only[source_set_name] = res 285 return res 286 287 288class GnParser(object): 289 """A parser with some cleverness for GN json desc files 290 291 The main goals of this parser are: 292 1) Deal with the fact that other build systems don't have an equivalent 293 notion to GN's source_set. Conversely to Bazel's and Soong's filegroups, 294 GN source_sets expect that dependencies, cflags and other source_set 295 properties propagate up to the linker unit (static_library, executable or 296 shared_library). This parser simulates the same behavior: when a 297 source_set is encountered, some of its variables (cflags and such) are 298 copied up to the dependent targets. This is to allow gen_xxx to create 299 one filegroup for each source_set and then squash all the other flags 300 onto the linker unit. 301 2) Detect and special-case protobuf targets, figuring out the protoc-plugin 302 being used. 303 """ 304 305 class Target(object): 306 """Reperesents A GN target. 307 308 Maked properties are propagated up the dependency chain when a 309 source_set dependency is encountered. 310 """ 311 312 def __init__(self, name, type): 313 self.name = name # e.g. //src/ipc:ipc 314 315 VALID_TYPES = ('static_library', 'shared_library', 'executable', 'group', 316 'action', 'source_set', 'proto_library', 'generated_file') 317 assert (type in VALID_TYPES) 318 self.type = type 319 self.testonly = False 320 self.toolchain = None 321 322 # These are valid only for type == proto_library. 323 # This is typically: 'proto', 'protozero', 'ipc'. 324 self.proto_plugin: Optional[str] = None 325 self.proto_paths = set() 326 self.proto_exports = set() 327 328 self.sources = set() 329 # TODO(primiano): consider whether the public section should be part of 330 # bubbled-up sources. 331 self.public_headers = set() # 'public' 332 333 # These are valid only for type == 'action' 334 self.data = set() 335 self.inputs = set() 336 self.outputs = set() 337 self.script = None 338 self.args = [] 339 self.custom_action_type = None 340 self.python_main = None 341 342 # These variables are propagated up when encountering a dependency 343 # on a source_set target. 344 self.cflags = set() 345 self.defines = set() 346 self.deps: Set[GnParser.Target] = set() 347 self.transitive_deps: Set[GnParser.Target] = set() 348 self.libs = set() 349 self.include_dirs = set() 350 self.ldflags = set() 351 352 # Deps on //gn:xxx have this flag set to True. These dependencies 353 # are special because they pull third_party code from buildtools/. 354 # We don't want to keep recursing into //buildtools in generators, 355 # this flag is used to stop the recursion and create an empty 356 # placeholder target once we hit //gn:protoc or similar. 357 self.is_third_party_dep_ = False 358 359 def non_proto_or_source_set_deps(self): 360 return set(d for d in self.deps 361 if d.type != 'proto_library' and d.type != 'source_set') 362 363 def proto_deps(self): 364 return set(d for d in self.deps if d.type == 'proto_library') 365 366 def transitive_proto_deps(self): 367 return set(d for d in self.transitive_deps if d.type == 'proto_library') 368 369 def transitive_cpp_proto_deps(self): 370 return set( 371 d for d in self.transitive_deps if d.type == 'proto_library' and 372 d.proto_plugin != 'descriptor' and d.proto_plugin != 'source_set') 373 374 def transitive_source_set_deps(self): 375 return set(d for d in self.transitive_deps if d.type == 'source_set') 376 377 def __lt__(self, other): 378 if isinstance(other, self.__class__): 379 return self.name < other.name 380 raise TypeError( 381 '\'<\' not supported between instances of \'%s\' and \'%s\'' % 382 (type(self).__name__, type(other).__name__)) 383 384 def __repr__(self): 385 return json.dumps({ 386 k: (list(sorted(v)) if isinstance(v, set) else v) 387 for (k, v) in iteritems(self.__dict__) 388 }, 389 indent=4, 390 sort_keys=True) 391 392 def update(self, other): 393 for key in ('cflags', 'data', 'defines', 'deps', 'include_dirs', 394 'ldflags', 'transitive_deps', 'libs', 'proto_paths'): 395 self.__dict__[key].update(other.__dict__.get(key, [])) 396 397 def __init__(self, gn_desc): 398 self.gn_desc_ = gn_desc 399 self.all_targets = {} 400 self.linker_units = {} # Executables, shared or static libraries. 401 self.source_sets = {} 402 self.actions = {} 403 self.proto_libs = {} 404 405 def get_target(self, gn_target_name: str) -> Target: 406 """Returns a Target object from the fully qualified GN target name. 407 408 It bubbles up variables from source_set dependencies as described in the 409 class-level comments. 410 """ 411 target = self.all_targets.get(gn_target_name) 412 if target is not None: 413 return target # Target already processed. 414 415 desc = self.gn_desc_[gn_target_name] 416 target = GnParser.Target(gn_target_name, desc['type']) 417 target.testonly = desc.get('testonly', False) 418 target.toolchain = desc.get('toolchain', None) 419 self.all_targets[gn_target_name] = target 420 421 # We should never have GN targets directly depend on buidtools. They 422 # should hop via //gn:xxx, so we can give generators an opportunity to 423 # override them. 424 assert (not gn_target_name.startswith('//buildtools')) 425 426 # Don't descend further into third_party targets. Genrators are supposed 427 # to either ignore them or route to other externally-provided targets. 428 if gn_target_name.startswith('//gn'): 429 target.is_third_party_dep_ = True 430 return target 431 432 proto_target_type, proto_desc = self.get_proto_target_type(target) 433 if proto_target_type: 434 assert proto_desc 435 self.proto_libs[target.name] = target 436 target.type = 'proto_library' 437 target.proto_plugin = proto_target_type 438 target.proto_paths.update(self.get_proto_paths(proto_desc)) 439 target.proto_exports.update(self.get_proto_exports(proto_desc)) 440 target.sources.update(proto_desc.get('sources', [])) 441 assert (all(x.endswith('.proto') for x in target.sources)) 442 elif target.type == 'source_set': 443 self.source_sets[gn_target_name] = target 444 target.sources.update(desc.get('sources', [])) 445 target.inputs.update(desc.get('inputs', [])) 446 elif target.type in LINKER_UNIT_TYPES: 447 self.linker_units[gn_target_name] = target 448 target.sources.update(desc.get('sources', [])) 449 elif target.type == 'action': 450 self.actions[gn_target_name] = target 451 target.data.update(desc.get('metadata', {}).get('perfetto_data', [])) 452 target.inputs.update(desc.get('inputs', [])) 453 target.sources.update(desc.get('sources', [])) 454 outs = [re.sub('^//out/.+?/gen/', '', x) for x in desc['outputs']] 455 target.outputs.update(outs) 456 target.script = desc['script'] 457 # Args are typically relative to the root build dir (../../xxx) 458 # because root build dir is typically out/xxx/). 459 target.args = [re.sub('^../../', '//', x) for x in desc['args']] 460 action_types = desc.get('metadata', 461 {}).get('perfetto_action_type_for_generator', []) 462 target.custom_action_type = action_types[0] if len( 463 action_types) > 0 else None 464 python_main = desc.get('metadata', {}).get('perfetto_python_main', []) 465 target.python_main = python_main[0] if python_main else None 466 467 # Default for 'public' is //* - all headers in 'sources' are public. 468 # TODO(primiano): if a 'public' section is specified (even if empty), then 469 # the rest of 'sources' is considered inaccessible by gn. Consider 470 # emulating that, so that generated build files don't end up with overly 471 # accessible headers. 472 public_headers = [x for x in desc.get('public', []) if x != '*'] 473 target.public_headers.update(public_headers) 474 475 target.cflags.update(desc.get('cflags', []) + desc.get('cflags_cc', [])) 476 target.libs.update(desc.get('libs', [])) 477 target.ldflags.update(desc.get('ldflags', [])) 478 target.defines.update(desc.get('defines', [])) 479 target.include_dirs.update(desc.get('include_dirs', [])) 480 481 # Recurse in dependencies. 482 for dep_name in desc.get('deps', []): 483 dep = self.get_target(dep_name) 484 485 # generated_file targets only exist for GN builds: we can safely ignore 486 # them. 487 if dep.type == 'generated_file': 488 continue 489 490 # When a proto_library depends on an action, that is always the "_gen" 491 # rule of the action which is "private" to the proto_library rule. 492 # therefore, just ignore it for dep tracking purposes. 493 if dep.type == 'action' and proto_target_type is not None: 494 target_no_toolchain = label_without_toolchain(target.name) 495 dep_no_toolchain = label_without_toolchain(dep.name) 496 assert (dep_no_toolchain == f'{target_no_toolchain}_gen') 497 continue 498 499 # Non-third party groups are only used for bubbling cflags etc so don't 500 # add a dep. 501 if dep.type == 'group' and not dep.is_third_party_dep_: 502 target.update(dep) # Bubble up groups's cflags/ldflags etc. 503 continue 504 505 # Linker units act as a hard boundary making all their internal deps 506 # opaque to the outside world. For this reason, do not propogate deps 507 # transitively across them. 508 if dep.type in LINKER_UNIT_TYPES: 509 target.deps.add(dep) 510 continue 511 512 if dep.type == 'source_set': 513 target.update(dep) # Bubble up source set's cflags/ldflags etc. 514 elif dep.type == 'proto_library': 515 target.proto_paths.update(dep.proto_paths) 516 517 target.deps.add(dep) 518 target.transitive_deps.add(dep) 519 target.transitive_deps.update(dep.transitive_deps) 520 521 return target 522 523 def get_proto_exports(self, proto_desc): 524 # exports in metadata will be available for source_set targets. 525 metadata = proto_desc.get('metadata', {}) 526 return metadata.get('exports', []) 527 528 def get_proto_paths(self, proto_desc): 529 # import_dirs in metadata will be available for source_set targets. 530 metadata = proto_desc.get('metadata', {}) 531 return metadata.get('import_dirs', []) 532 533 def get_proto_target_type(self, target: Target 534 ) -> Tuple[Optional[str], Optional[Dict]]: 535 """ Checks if the target is a proto library and return the plugin. 536 537 Returns: 538 (None, None): if the target is not a proto library. 539 (plugin, proto_desc) where |plugin| is 'proto' in the default (lite) 540 case or 'protozero' or 'ipc' or 'descriptor'; |proto_desc| is the GN 541 json desc of the target with the .proto sources (_gen target for 542 non-descriptor types or the target itself for descriptor type). 543 """ 544 parts = target.name.split('(', 1) 545 name = parts[0] 546 toolchain = '(' + parts[1] if len(parts) > 1 else '' 547 548 # Descriptor targets don't have a _gen target; instead we look for the 549 # characteristic flag in the args of the target itself. 550 desc = self.gn_desc_.get(target.name) 551 if '--descriptor_set_out' in desc.get('args', []): 552 return 'descriptor', desc 553 554 # Source set proto targets have a non-empty proto_library_sources in the 555 # metadata of the description. 556 metadata = desc.get('metadata', {}) 557 if 'proto_library_sources' in metadata: 558 return 'source_set', desc 559 560 # In all other cases, we want to look at the _gen target as that has the 561 # important information. 562 gen_desc = self.gn_desc_.get('%s_gen%s' % (name, toolchain)) 563 if gen_desc is None or gen_desc['type'] != 'action': 564 return None, None 565 args = gen_desc.get('args', []) 566 if '/protoc' not in args[0]: 567 return None, None 568 plugin = 'proto' 569 for arg in (arg for arg in args if arg.startswith('--plugin=')): 570 # |arg| at this point looks like: 571 # --plugin=protoc-gen-plugin=gcc_like_host/protozero_plugin 572 # or 573 # --plugin=protoc-gen-plugin=protozero_plugin 574 plugin = arg.split('=')[-1].split('/')[-1].replace('_plugin', '') 575 return plugin, gen_desc 576