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 26 from __future__ import print_function 27 import argparse 28 import errno 29 import functools 30 import json 31 import os 32 import re 33 import shutil 34 import subprocess 35 import sys 36 import textwrap 37 38 # Copyright header for generated code. 39 header = """# 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. 58 gn_args = 'target_os="linux" is_debug=false host_os="linux"' 59 60 # Default targets to translate to the blueprint file. 61 default_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 71 alias_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 79 def enable_sqlite(module): 80 module.deps.add(Label('//third_party/sqlite')) 81 module.deps.add(Label('//third_party/sqlite:sqlite_ext_percentile')) 82 83 84 def enable_jsoncpp(module): 85 module.deps.add(Label('//third_party/perfetto/google:jsoncpp')) 86 87 88 def enable_linenoise(module): 89 module.deps.add(Label('//third_party/perfetto/google:linenoise')) 90 91 92 def enable_gtest_prod(module): 93 module.deps.add(Label('//third_party/perfetto/google:gtest_prod')) 94 95 96 def enable_protobuf_full(module): 97 module.deps.add(Label('//third_party/protobuf:libprotoc')) 98 module.deps.add(Label('//third_party/protobuf')) 99 100 101 def enable_perfetto_version(module): 102 module.deps.add(Label('//third_party/perfetto/google:perfetto_version')) 103 104 105 def disable_module(module): 106 pass 107 108 109 # Internal equivalents for third-party libraries that the upstream project 110 # depends on. 111 builtin_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 129 def 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 142 class Error(Exception): 143 pass 144 145 146 def 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 152 def 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 172 def 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 178 def 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 188 def 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 197 def 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 207 class 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 248 class 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 285 class 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 336 class 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 376 class 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 629 def 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 686 if __name__ == '__main__': 687 sys.exit(main()) 688