1#!/usr/bin/env python 2# Copyright (C) 2018 The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16# This tool translates a collection of BUILD.gn files into a mostly equivalent 17# BUILD file for the Bazel build system. The input to the tool is a 18# JSON description of the GN build definition generated with the following 19# command: 20# 21# gn desc out --format=json --all-toolchains "//*" > desc.json 22# 23# The tool is then given a list of GN labels for which to generate Bazel 24# build rules. 25 26from __future__ import print_function 27import argparse 28import errno 29import functools 30import json 31import os 32import re 33import shutil 34import subprocess 35import sys 36import textwrap 37 38# Copyright header for generated code. 39header = """# Copyright (C) 2019 The Android Open Source Project 40# 41# Licensed under the Apache License, Version 2.0 (the "License"); 42# you may not use this file except in compliance with the License. 43# You may obtain a copy of the License at 44# 45# http://www.apache.org/licenses/LICENSE-2.0 46# 47# Unless required by applicable law or agreed to in writing, software 48# distributed under the License is distributed on an "AS IS" BASIS, 49# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 50# See the License for the specific language governing permissions and 51# limitations under the License. 52# 53# This file is automatically generated by {}. Do not edit. 54""".format(__file__) 55 56# Arguments for the GN output directory. 57# host_os="linux" is to generate the right build files from Mac OS. 58gn_args = 'target_os="linux" is_debug=false host_os="linux"' 59 60# Default targets to translate to the blueprint file. 61default_targets = [ 62 '//src/protozero:libprotozero', 63 '//src/trace_processor:trace_processor', 64 '//src/trace_processor:trace_processor_shell_host(//gn/standalone/toolchain:gcc_like_host)', 65 '//tools/trace_to_text:trace_to_text_host(//gn/standalone/toolchain:gcc_like_host)', 66 '//protos/perfetto/config:merged_config_gen', 67 '//protos/perfetto/trace:merged_trace_gen', 68] 69 70# Aliases to add to the BUILD file 71alias_targets = { 72 '//src/protozero:libprotozero': 'libprotozero', 73 '//src/trace_processor:trace_processor': 'trace_processor', 74 '//src/trace_processor:trace_processor_shell_host': 'trace_processor_shell', 75 '//tools/trace_to_text:trace_to_text_host': 'trace_to_text', 76} 77 78 79def enable_sqlite(module): 80 module.deps.add(Label('//third_party/sqlite')) 81 module.deps.add(Label('//third_party/sqlite:sqlite_ext_percentile')) 82 83 84def enable_jsoncpp(module): 85 module.deps.add(Label('//third_party/perfetto/google:jsoncpp')) 86 87 88def enable_linenoise(module): 89 module.deps.add(Label('//third_party/perfetto/google:linenoise')) 90 91 92def enable_gtest_prod(module): 93 module.deps.add(Label('//third_party/perfetto/google:gtest_prod')) 94 95 96def enable_protobuf_full(module): 97 module.deps.add(Label('//third_party/protobuf:libprotoc')) 98 module.deps.add(Label('//third_party/protobuf')) 99 100 101def enable_perfetto_version(module): 102 module.deps.add(Label('//third_party/perfetto/google:perfetto_version')) 103 104 105def disable_module(module): 106 pass 107 108 109# Internal equivalents for third-party libraries that the upstream project 110# depends on. 111builtin_deps = { 112 '//gn:jsoncpp_deps': enable_jsoncpp, 113 '//buildtools:linenoise': enable_linenoise, 114 '//buildtools:protobuf_lite': disable_module, 115 '//buildtools:protobuf_full': enable_protobuf_full, 116 '//buildtools:protoc': disable_module, 117 '//buildtools:sqlite': enable_sqlite, 118 '//gn:default_deps': disable_module, 119 '//gn:gtest_prod_config': enable_gtest_prod, 120 '//gn:protoc_lib_deps': enable_protobuf_full, 121 '//gn/standalone:gen_git_revision': enable_perfetto_version, 122} 123 124# ---------------------------------------------------------------------------- 125# End of configuration. 126# ---------------------------------------------------------------------------- 127 128 129def check_output(cmd, cwd): 130 try: 131 output = subprocess.check_output( 132 cmd, stderr=subprocess.STDOUT, cwd=cwd) 133 except subprocess.CalledProcessError as e: 134 print('Cmd "{}" failed in {}:'.format( 135 ' '.join(cmd), cwd), file=sys.stderr) 136 print(e.output) 137 exit(1) 138 else: 139 return output 140 141 142class Error(Exception): 143 pass 144 145 146def repo_root(): 147 """Returns an absolute path to the repository root.""" 148 return os.path.join( 149 os.path.realpath(os.path.dirname(__file__)), os.path.pardir) 150 151 152def create_build_description(repo_root): 153 """Creates the JSON build description by running GN.""" 154 155 out = os.path.join(repo_root, 'out', 'tmp.gen_build') 156 try: 157 try: 158 os.makedirs(out) 159 except OSError as e: 160 if e.errno != errno.EEXIST: 161 raise 162 check_output( 163 ['gn', 'gen', out, '--args=%s' % gn_args], repo_root) 164 desc = check_output( 165 ['gn', 'desc', out, '--format=json', '--all-toolchains', '//*'], 166 repo_root) 167 return json.loads(desc) 168 finally: 169 shutil.rmtree(out) 170 171 172def label_to_path(label): 173 """Turn a GN output label (e.g., //some_dir/file.cc) into a path.""" 174 assert label.startswith('//') 175 return label[2:] 176 177 178def label_to_target_name_with_path(label): 179 """ 180 Turn a GN label into a target name involving the full path. 181 e.g., //src/perfetto:tests -> src_perfetto_tests 182 """ 183 name = re.sub(r'^//:?', '', label) 184 name = re.sub(r'[^a-zA-Z0-9_]', '_', name) 185 return name 186 187 188def label_without_toolchain(label): 189 """Strips the toolchain from a GN label. 190 191 Return a GN label (e.g //buildtools:protobuf(//gn/standalone/toolchain: 192 gcc_like_host) without the parenthesised toolchain part. 193 """ 194 return label.split('(')[0] 195 196 197def is_public_header(label): 198 """ 199 Returns if this is a c++ header file that is part of the API. 200 Args: 201 label: Label to evaluate 202 """ 203 return label.endswith('.h') and label.startswith('//include/perfetto/') 204 205 206@functools.total_ordering 207class Label(object): 208 """Represents a label in BUILD file terminology. This class wraps a string 209 label to allow for correct comparision of labels for sorting. 210 211 Args: 212 label: The string rerepsentation of the label. 213 """ 214 215 def __init__(self, label): 216 self.label = label 217 218 def is_absolute(self): 219 return self.label.startswith('//') 220 221 def dirname(self): 222 return self.label.split(':')[0] if ':' in self.label else self.label 223 224 def basename(self): 225 return self.label.split(':')[1] if ':' in self.label else '' 226 227 def __eq__(self, other): 228 return self.label == other.label 229 230 def __lt__(self, other): 231 return ( 232 self.is_absolute(), 233 self.dirname(), 234 self.basename() 235 ) < ( 236 other.is_absolute(), 237 other.dirname(), 238 other.basename() 239 ) 240 241 def __str__(self): 242 return self.label 243 244 def __hash__(self): 245 return hash(self.label) 246 247 248class Writer(object): 249 def __init__(self, output, width=79): 250 self.output = output 251 self.width = width 252 253 def comment(self, text): 254 for line in textwrap.wrap(text, 255 self.width - 2, 256 break_long_words=False, 257 break_on_hyphens=False): 258 self.output.write('# {}\n'.format(line)) 259 260 def newline(self): 261 self.output.write('\n') 262 263 def line(self, s, indent=0): 264 self.output.write(' ' * indent + s + '\n') 265 266 def variable(self, key, value, sort=True): 267 if value is None: 268 return 269 if isinstance(value, set) or isinstance(value, list): 270 if len(value) == 0: 271 return 272 self.line('{} = ['.format(key), indent=1) 273 for v in sorted(list(value)) if sort else value: 274 self.line('"{}",'.format(v), indent=2) 275 self.line('],', indent=1) 276 elif isinstance(value, basestring): 277 self.line('{} = "{}",'.format(key, value), indent=1) 278 else: 279 self.line('{} = {},'.format(key, value), indent=1) 280 281 def header(self): 282 self.output.write(header) 283 284 285class Target(object): 286 """In-memory representation of a BUILD target.""" 287 288 def __init__(self, type, name, gn_name=None): 289 assert type in ('cc_binary', 'cc_library', 'cc_proto_library', 290 'proto_library', 'filegroup', 'alias', 291 'pbzero_cc_proto_library', 'genrule', ) 292 self.type = type 293 self.name = name 294 self.srcs = set() 295 self.hdrs = set() 296 self.deps = set() 297 self.visibility = set() 298 self.gn_name = gn_name 299 self.is_pbzero = False 300 self.src_proto_library = None 301 self.outs = set() 302 self.cmd = None 303 self.tools = set() 304 305 def write(self, writer): 306 if self.gn_name: 307 writer.comment('GN target: {}'.format(self.gn_name)) 308 309 writer.line('{}('.format(self.type)) 310 writer.variable('name', self.name) 311 writer.variable('srcs', self.srcs) 312 writer.variable('hdrs', self.hdrs) 313 314 if self.type == 'proto_library' and not self.is_pbzero: 315 if self.srcs: 316 writer.variable('has_services', 1) 317 writer.variable('cc_api_version', 2) 318 if self.srcs: 319 writer.variable('cc_generic_services', 1) 320 321 writer.variable('src_proto_library', self.src_proto_library) 322 323 writer.variable('outs', self.outs) 324 writer.variable('cmd', self.cmd) 325 writer.variable('tools', self.tools) 326 327 # Keep visibility and deps last. 328 writer.variable('visibility', self.visibility) 329 330 if type != 'filegroup': 331 writer.variable('deps', self.deps) 332 333 writer.line(')') 334 335 336class Build(object): 337 """In-memory representation of a BUILD file.""" 338 339 def __init__(self, public, header_lines=[]): 340 self.targets = {} 341 self.public = public 342 self.header_lines = header_lines 343 344 def add_target(self, target): 345 self.targets[target.name] = target 346 347 def write(self, writer): 348 writer.header() 349 writer.newline() 350 for line in self.header_lines: 351 writer.line(line) 352 if self.header_lines: 353 writer.newline() 354 if self.public: 355 writer.line( 356 'package(default_visibility = ["//visibility:public"])') 357 else: 358 writer.line( 359 'package(default_visibility = ["//third_party/perfetto:__subpackages__"])') 360 writer.newline() 361 writer.line('licenses(["notice"]) # Apache 2.0') 362 writer.newline() 363 writer.line('exports_files(["LICENSE"])') 364 writer.newline() 365 366 sorted_targets = sorted( 367 self.targets.itervalues(), key=lambda m: m.name) 368 for target in sorted_targets[:-1]: 369 target.write(writer) 370 writer.newline() 371 372 # BUILD files shouldn't have a trailing new line. 373 sorted_targets[-1].write(writer) 374 375 376class BuildGenerator(object): 377 def __init__(self, desc): 378 self.desc = desc 379 self.action_generated_files = set() 380 381 for target in self.desc.itervalues(): 382 if target['type'] == 'action': 383 self.action_generated_files.update(target['outputs']) 384 385 386 def create_build_for_targets(self, targets): 387 """Generate a BUILD for a list of GN targets and aliases.""" 388 self.build = Build(public=True) 389 390 proto_cc_import = 'load("//tools/build_defs/proto/cpp:cc_proto_library.bzl", "cc_proto_library")' 391 pbzero_cc_import = 'load("//third_party/perfetto/google:build_defs.bzl", "pbzero_cc_proto_library")' 392 self.proto_build = Build(public=False, header_lines=[ 393 proto_cc_import, pbzero_cc_import]) 394 395 for target in targets: 396 self.create_target(target) 397 398 return (self.build, self.proto_build) 399 400 401 def resolve_dependencies(self, target_name): 402 """Return the set of direct dependent-on targets for a GN target. 403 404 Args: 405 desc: JSON GN description. 406 target_name: Name of target 407 408 Returns: 409 A set of transitive dependencies in the form of GN targets. 410 """ 411 412 if label_without_toolchain(target_name) in builtin_deps: 413 return set() 414 target = self.desc[target_name] 415 resolved_deps = set() 416 for dep in target.get('deps', []): 417 resolved_deps.add(dep) 418 return resolved_deps 419 420 421 def apply_module_sources_to_target(self, target, module_desc): 422 """ 423 Args: 424 target: Module to which dependencies should be added. 425 module_desc: JSON GN description of the module. 426 visibility: Whether the module is visible with respect to the target. 427 """ 428 for src in module_desc['sources']: 429 label = Label(label_to_path(src)) 430 if target.type == 'cc_library' and is_public_header(src): 431 target.hdrs.add(label) 432 else: 433 target.srcs.add(label) 434 435 436 def apply_module_dependency(self, target, dep_name): 437 """ 438 Args: 439 build: BUILD instance which is being generated. 440 proto_build: BUILD instance which is being generated to hold protos. 441 desc: JSON GN description. 442 target: Module to which dependencies should be added. 443 dep_name: GN target of the dependency. 444 """ 445 # If the dependency refers to a library which we can replace with an internal 446 # equivalent, stop recursing and patch the dependency in. 447 dep_name_no_toolchain = label_without_toolchain(dep_name) 448 if dep_name_no_toolchain in builtin_deps: 449 builtin_deps[dep_name_no_toolchain](target) 450 return 451 452 dep_desc = self.desc[dep_name] 453 if dep_desc['type'] == 'source_set': 454 for inner_name in self.resolve_dependencies(dep_name): 455 self.apply_module_dependency(target, inner_name) 456 457 # Any source set which has a source generated by an action doesn't need 458 # to be depended on as we will depend on the action directly. 459 if any(src in self.action_generated_files for src in dep_desc['sources']): 460 return 461 462 self.apply_module_sources_to_target(target, dep_desc) 463 elif dep_desc['type'] == 'action': 464 args = dep_desc['args'] 465 if "gen_merged_sql_metrics" in dep_name: 466 dep_target = self.create_merged_sql_metrics_target(dep_name) 467 target.deps.add(Label("//third_party/perfetto:" + dep_target.name)) 468 469 if target.type == 'cc_library' or target.type == 'cc_binary': 470 target.srcs.update(dep_target.outs) 471 elif args[0].endswith('/protoc'): 472 (proto_target, cc_target) = self.create_proto_target(dep_name) 473 if target.type == 'proto_library': 474 dep_target_name = proto_target.name 475 else: 476 dep_target_name = cc_target.name 477 target.deps.add( 478 Label("//third_party/perfetto/protos:" + dep_target_name)) 479 else: 480 raise Error('Unsupported action in target %s: %s' % (dep_target_name, 481 args)) 482 elif dep_desc['type'] == 'static_library': 483 dep_target = self.create_target(dep_name) 484 target.deps.add(Label("//third_party/perfetto:" + dep_target.name)) 485 elif dep_desc['type'] == 'group': 486 for inner_name in self.resolve_dependencies(dep_name): 487 self.apply_module_dependency(target, inner_name) 488 elif dep_desc['type'] == 'executable': 489 # Just create the dep target but don't add it as a dep because it's an 490 # executable. 491 self.create_target(dep_name) 492 else: 493 raise Error('Unknown target name %s with type: %s' % 494 (dep_name, dep_desc['type'])) 495 496 def create_merged_sql_metrics_target(self, gn_target_name): 497 target_desc = self.desc[gn_target_name] 498 gn_target_name_no_toolchain = label_without_toolchain(gn_target_name) 499 target = Target( 500 'genrule', 501 'gen_merged_sql_metrics', 502 gn_name=gn_target_name_no_toolchain, 503 ) 504 target.outs.update( 505 Label(src[src.index('gen/') + len('gen/'):]) 506 for src in target_desc.get('outputs', []) 507 ) 508 target.cmd = '$(location gen_merged_sql_metrics_py) --cpp_out=$@ $(SRCS)' 509 target.tools.update([ 510 'gen_merged_sql_metrics_py', 511 ]) 512 target.srcs.update( 513 Label(label_to_path(src)) 514 for src in target_desc.get('inputs', []) 515 if src not in self.action_generated_files 516 ) 517 self.build.add_target(target) 518 return target 519 520 def create_proto_target(self, gn_target_name): 521 target_desc = self.desc[gn_target_name] 522 args = target_desc['args'] 523 524 gn_target_name_no_toolchain = label_without_toolchain(gn_target_name) 525 stripped_path = gn_target_name_no_toolchain.replace("protos/perfetto/", "") 526 pretty_target_name = label_to_target_name_with_path(stripped_path) 527 pretty_target_name = pretty_target_name.replace("_lite_gen", "") 528 pretty_target_name = pretty_target_name.replace("_zero_gen", "_zero") 529 530 proto_target = Target( 531 'proto_library', 532 pretty_target_name, 533 gn_name=gn_target_name_no_toolchain 534 ) 535 proto_target.is_pbzero = any("pbzero" in arg for arg in args) 536 proto_target.srcs.update([ 537 Label(label_to_path(src).replace('protos/', '')) 538 for src in target_desc.get('sources', []) 539 ]) 540 if not proto_target.is_pbzero: 541 proto_target.visibility.add("//visibility:public") 542 self.proto_build.add_target(proto_target) 543 544 for dep_name in self.resolve_dependencies(gn_target_name): 545 self.apply_module_dependency(proto_target, dep_name) 546 547 if proto_target.is_pbzero: 548 # Remove all the protozero srcs from the proto_library. 549 proto_target.srcs.difference_update( 550 [src for src in proto_target.srcs if not src.label.endswith('.proto')]) 551 552 # Remove all the non-proto deps from the proto_library and add to the cc 553 # library. 554 cc_deps = [ 555 dep for dep in proto_target.deps 556 if not dep.label.startswith('//third_party/perfetto/protos') 557 ] 558 proto_target.deps.difference_update(cc_deps) 559 560 cc_target_name = proto_target.name + "_cc_proto" 561 cc_target = Target('pbzero_cc_proto_library', cc_target_name, 562 gn_name=gn_target_name_no_toolchain) 563 564 cc_target.deps.add(Label('//third_party/perfetto:libprotozero')) 565 cc_target.deps.update(cc_deps) 566 567 # Add the proto_library to the cc_target. 568 cc_target.src_proto_library = \ 569 "//third_party/perfetto/protos:" + proto_target.name 570 571 self.proto_build.add_target(cc_target) 572 else: 573 cc_target_name = proto_target.name + "_cc_proto" 574 cc_target = Target('cc_proto_library', 575 cc_target_name, gn_name=gn_target_name_no_toolchain) 576 cc_target.visibility.add("//visibility:public") 577 cc_target.deps.add( 578 Label("//third_party/perfetto/protos:" + proto_target.name)) 579 self.proto_build.add_target(cc_target) 580 581 return (proto_target, cc_target) 582 583 584 def create_target(self, gn_target_name): 585 """Generate module(s) for a given GN target. 586 587 Given a GN target name, generate one or more corresponding modules into a 588 build file. 589 590 Args: 591 build: Build instance which is being generated. 592 desc: JSON GN description. 593 gn_target_name: GN target name for module generation. 594 """ 595 596 target_desc = self.desc[gn_target_name] 597 if target_desc['type'] == 'action': 598 args = target_desc['args'] 599 if args[0].endswith('/protoc'): 600 return self.create_proto_target(gn_target_name) 601 else: 602 raise Error('Unsupported action in target %s: %s' % (gn_target_name, 603 args)) 604 elif target_desc['type'] == 'executable': 605 target_type = 'cc_binary' 606 elif target_desc['type'] == 'static_library': 607 target_type = 'cc_library' 608 elif target_desc['type'] == 'source_set': 609 target_type = 'filegroup' 610 else: 611 raise Error('Unknown target type: %s' % target_desc['type']) 612 613 label_no_toolchain = label_without_toolchain(gn_target_name) 614 target_name_path = label_to_target_name_with_path(label_no_toolchain) 615 target_name = alias_targets.get(label_no_toolchain, target_name_path) 616 target = Target(target_type, target_name, gn_name=label_no_toolchain) 617 target.srcs.update( 618 Label(label_to_path(src)) 619 for src in target_desc.get('sources', []) 620 if src not in self.action_generated_files 621 ) 622 623 for dep_name in self.resolve_dependencies(gn_target_name): 624 self.apply_module_dependency(target, dep_name) 625 626 self.build.add_target(target) 627 return target 628 629def main(): 630 parser = argparse.ArgumentParser( 631 description='Generate BUILD from a GN description.') 632 parser.add_argument( 633 '--desc', 634 help='GN description (e.g., gn desc out --format=json --all-toolchains "//*"' 635 ) 636 parser.add_argument( 637 '--repo-root', 638 help='Standalone Perfetto repository to generate a GN description', 639 default=repo_root(), 640 ) 641 parser.add_argument( 642 '--extras', 643 help='Extra targets to include at the end of the BUILD file', 644 default=os.path.join(repo_root(), 'BUILD.extras'), 645 ) 646 parser.add_argument( 647 '--output', 648 help='BUILD file to create', 649 default=os.path.join(repo_root(), 'BUILD'), 650 ) 651 parser.add_argument( 652 '--output-proto', 653 help='Proto BUILD file to create', 654 default=os.path.join(repo_root(), 'protos', 'BUILD'), 655 ) 656 parser.add_argument( 657 'targets', 658 nargs=argparse.REMAINDER, 659 help='Targets to include in the BUILD file (e.g., "//:perfetto_tests")') 660 args = parser.parse_args() 661 662 if args.desc: 663 with open(args.desc) as f: 664 desc = json.load(f) 665 else: 666 desc = create_build_description(args.repo_root) 667 668 build_generator = BuildGenerator(desc) 669 build, proto_build = build_generator.create_build_for_targets( 670 args.targets or default_targets) 671 with open(args.output, 'w') as f: 672 writer = Writer(f) 673 build.write(writer) 674 writer.newline() 675 676 with open(args.extras, 'r') as r: 677 for line in r: 678 writer.line(line.rstrip("\n\r")) 679 680 with open(args.output_proto, 'w') as f: 681 proto_build.write(Writer(f)) 682 683 return 0 684 685 686if __name__ == '__main__': 687 sys.exit(main()) 688