#!/usr/bin/env python
# Copyright (C) 2018 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This tool translates a collection of BUILD.gn files into a mostly equivalent
# BUILD file for the Bazel build system. The input to the tool is a
# JSON description of the GN build definition generated with the following
# command:
#
#   gn desc out --format=json --all-toolchains "//*" > desc.json
#
# The tool is then given a list of GN labels for which to generate Bazel
# build rules.

from __future__ import print_function
import argparse
import errno
import functools
import json
import os
import re
import shutil
import subprocess
import sys
import textwrap

# Copyright header for generated code.
header = """# Copyright (C) 2019 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# This file is automatically generated by {}. Do not edit.
""".format(__file__)

# Arguments for the GN output directory.
# host_os="linux" is to generate the right build files from Mac OS.
gn_args = 'target_os="linux" is_debug=false host_os="linux"'

# Default targets to translate to the blueprint file.
default_targets = [
  '//src/protozero:libprotozero',
  '//src/trace_processor:trace_processor',
  '//src/trace_processor:trace_processor_shell_host(//gn/standalone/toolchain:gcc_like_host)',
  '//tools/trace_to_text:trace_to_text_host(//gn/standalone/toolchain:gcc_like_host)',
  '//protos/perfetto/config:merged_config_gen',
  '//protos/perfetto/trace:merged_trace_gen',
]

# Aliases to add to the BUILD file
alias_targets = {
  '//src/protozero:libprotozero': 'libprotozero',
  '//src/trace_processor:trace_processor': 'trace_processor',
  '//src/trace_processor:trace_processor_shell_host': 'trace_processor_shell',
  '//tools/trace_to_text:trace_to_text_host': 'trace_to_text',
}


def enable_sqlite(module):
  module.deps.add(Label('//third_party/sqlite'))
  module.deps.add(Label('//third_party/sqlite:sqlite_ext_percentile'))


def enable_jsoncpp(module):
  module.deps.add(Label('//third_party/perfetto/google:jsoncpp'))


def enable_linenoise(module):
  module.deps.add(Label('//third_party/perfetto/google:linenoise'))


def enable_gtest_prod(module):
  module.deps.add(Label('//third_party/perfetto/google:gtest_prod'))


def enable_protobuf_full(module):
  module.deps.add(Label('//third_party/protobuf:libprotoc'))
  module.deps.add(Label('//third_party/protobuf'))


def enable_perfetto_version(module):
  module.deps.add(Label('//third_party/perfetto/google:perfetto_version'))


def disable_module(module):
  pass


# Internal equivalents for third-party libraries that the upstream project
# depends on.
builtin_deps = {
    '//gn:jsoncpp_deps': enable_jsoncpp,
    '//buildtools:linenoise': enable_linenoise,
    '//buildtools:protobuf_lite': disable_module,
    '//buildtools:protobuf_full': enable_protobuf_full,
    '//buildtools:protoc': disable_module,
    '//buildtools:sqlite': enable_sqlite,
    '//gn:default_deps': disable_module,
    '//gn:gtest_prod_config': enable_gtest_prod,
    '//gn:protoc_lib_deps': enable_protobuf_full,
    '//gn/standalone:gen_git_revision': enable_perfetto_version,
}

# ----------------------------------------------------------------------------
# End of configuration.
# ----------------------------------------------------------------------------


def check_output(cmd, cwd):
  try:
    output = subprocess.check_output(
        cmd, stderr=subprocess.STDOUT, cwd=cwd)
  except subprocess.CalledProcessError as e:
    print('Cmd "{}" failed in {}:'.format(
        ' '.join(cmd), cwd), file=sys.stderr)
    print(e.output)
    exit(1)
  else:
    return output


class Error(Exception):
  pass


def repo_root():
  """Returns an absolute path to the repository root."""
  return os.path.join(
      os.path.realpath(os.path.dirname(__file__)), os.path.pardir)


def create_build_description(repo_root):
  """Creates the JSON build description by running GN."""

  out = os.path.join(repo_root, 'out', 'tmp.gen_build')
  try:
    try:
      os.makedirs(out)
    except OSError as e:
      if e.errno != errno.EEXIST:
        raise
    check_output(
        ['gn', 'gen', out, '--args=%s' % gn_args], repo_root)
    desc = check_output(
        ['gn', 'desc', out, '--format=json', '--all-toolchains', '//*'],
        repo_root)
    return json.loads(desc)
  finally:
    shutil.rmtree(out)


def label_to_path(label):
  """Turn a GN output label (e.g., //some_dir/file.cc) into a path."""
  assert label.startswith('//')
  return label[2:]


def label_to_target_name_with_path(label):
  """
  Turn a GN label into a target name involving the full path.
  e.g., //src/perfetto:tests -> src_perfetto_tests
  """
  name = re.sub(r'^//:?', '', label)
  name = re.sub(r'[^a-zA-Z0-9_]', '_', name)
  return name


def label_without_toolchain(label):
  """Strips the toolchain from a GN label.

  Return a GN label (e.g //buildtools:protobuf(//gn/standalone/toolchain:
  gcc_like_host) without the parenthesised toolchain part.
  """
  return label.split('(')[0]


def is_public_header(label):
  """
  Returns if this is a c++ header file that is part of the API.
  Args:
      label: Label to evaluate
  """
  return label.endswith('.h') and label.startswith('//include/perfetto/')


@functools.total_ordering
class Label(object):
  """Represents a label in BUILD file terminology. This class wraps a string
  label to allow for correct comparision of labels for sorting.

  Args:
      label: The string rerepsentation of the label.
  """

  def __init__(self, label):
    self.label = label

  def is_absolute(self):
    return self.label.startswith('//')

  def dirname(self):
    return self.label.split(':')[0] if ':' in self.label else self.label

  def basename(self):
    return self.label.split(':')[1] if ':' in self.label else ''

  def __eq__(self, other):
    return self.label == other.label

  def __lt__(self, other):
    return (
        self.is_absolute(),
        self.dirname(),
        self.basename()
    ) < (
        other.is_absolute(),
        other.dirname(),
        other.basename()
    )

  def __str__(self):
    return self.label

  def __hash__(self):
    return hash(self.label)


class Writer(object):
  def __init__(self, output, width=79):
    self.output = output
    self.width = width

  def comment(self, text):
    for line in textwrap.wrap(text,
                              self.width - 2,
                              break_long_words=False,
                              break_on_hyphens=False):
      self.output.write('# {}\n'.format(line))

  def newline(self):
    self.output.write('\n')

  def line(self, s, indent=0):
    self.output.write('    ' * indent + s + '\n')

  def variable(self, key, value, sort=True):
    if value is None:
      return
    if isinstance(value, set) or isinstance(value, list):
      if len(value) == 0:
        return
      self.line('{} = ['.format(key), indent=1)
      for v in sorted(list(value)) if sort else value:
        self.line('"{}",'.format(v), indent=2)
      self.line('],', indent=1)
    elif isinstance(value, basestring):
      self.line('{} = "{}",'.format(key, value), indent=1)
    else:
      self.line('{} = {},'.format(key, value), indent=1)

  def header(self):
    self.output.write(header)


class Target(object):
  """In-memory representation of a BUILD target."""

  def __init__(self, type, name, gn_name=None):
    assert type in ('cc_binary', 'cc_library', 'cc_proto_library',
                    'proto_library', 'filegroup', 'alias',
                    'pbzero_cc_proto_library', 'genrule', )
    self.type = type
    self.name = name
    self.srcs = set()
    self.hdrs = set()
    self.deps = set()
    self.visibility = set()
    self.gn_name = gn_name
    self.is_pbzero = False
    self.src_proto_library = None
    self.outs = set()
    self.cmd = None
    self.tools = set()

  def write(self, writer):
    if self.gn_name:
      writer.comment('GN target: {}'.format(self.gn_name))

    writer.line('{}('.format(self.type))
    writer.variable('name', self.name)
    writer.variable('srcs', self.srcs)
    writer.variable('hdrs', self.hdrs)

    if self.type == 'proto_library' and not self.is_pbzero:
      if self.srcs:
        writer.variable('has_services', 1)
      writer.variable('cc_api_version', 2)
      if self.srcs:
        writer.variable('cc_generic_services', 1)

    writer.variable('src_proto_library', self.src_proto_library)

    writer.variable('outs', self.outs)
    writer.variable('cmd', self.cmd)
    writer.variable('tools', self.tools)

    # Keep visibility and deps last.
    writer.variable('visibility', self.visibility)

    if type != 'filegroup':
      writer.variable('deps', self.deps)

    writer.line(')')


class Build(object):
  """In-memory representation of a BUILD file."""

  def __init__(self, public, header_lines=[]):
    self.targets = {}
    self.public = public
    self.header_lines = header_lines

  def add_target(self, target):
    self.targets[target.name] = target

  def write(self, writer):
    writer.header()
    writer.newline()
    for line in self.header_lines:
      writer.line(line)
    if self.header_lines:
      writer.newline()
    if self.public:
      writer.line(
          'package(default_visibility = ["//visibility:public"])')
    else:
      writer.line(
          'package(default_visibility = ["//third_party/perfetto:__subpackages__"])')
    writer.newline()
    writer.line('licenses(["notice"])  # Apache 2.0')
    writer.newline()
    writer.line('exports_files(["LICENSE"])')
    writer.newline()

    sorted_targets = sorted(
        self.targets.itervalues(), key=lambda m: m.name)
    for target in sorted_targets[:-1]:
      target.write(writer)
      writer.newline()

    # BUILD files shouldn't have a trailing new line.
    sorted_targets[-1].write(writer)


class BuildGenerator(object):
  def __init__(self, desc):
    self.desc = desc
    self.action_generated_files = set()

    for target in self.desc.itervalues():
      if target['type'] == 'action':
        self.action_generated_files.update(target['outputs'])


  def create_build_for_targets(self, targets):
    """Generate a BUILD for a list of GN targets and aliases."""
    self.build = Build(public=True)

    proto_cc_import = 'load("//tools/build_defs/proto/cpp:cc_proto_library.bzl", "cc_proto_library")'
    pbzero_cc_import = 'load("//third_party/perfetto/google:build_defs.bzl", "pbzero_cc_proto_library")'
    self.proto_build = Build(public=False, header_lines=[
                        proto_cc_import, pbzero_cc_import])

    for target in targets:
      self.create_target(target)

    return (self.build, self.proto_build)


  def resolve_dependencies(self, target_name):
    """Return the set of direct dependent-on targets for a GN target.

    Args:
        desc: JSON GN description.
        target_name: Name of target

    Returns:
        A set of transitive dependencies in the form of GN targets.
    """

    if label_without_toolchain(target_name) in builtin_deps:
      return set()
    target = self.desc[target_name]
    resolved_deps = set()
    for dep in target.get('deps', []):
      resolved_deps.add(dep)
    return resolved_deps


  def apply_module_sources_to_target(self, target, module_desc):
    """
    Args:
        target: Module to which dependencies should be added.
        module_desc: JSON GN description of the module.
        visibility: Whether the module is visible with respect to the target.
    """
    for src in module_desc['sources']:
      label = Label(label_to_path(src))
      if target.type == 'cc_library' and is_public_header(src):
        target.hdrs.add(label)
      else:
        target.srcs.add(label)


  def apply_module_dependency(self, target, dep_name):
    """
    Args:
        build: BUILD instance which is being generated.
        proto_build: BUILD instance which is being generated to hold protos.
        desc: JSON GN description.
        target: Module to which dependencies should be added.
        dep_name: GN target of the dependency.
    """
    # If the dependency refers to a library which we can replace with an internal
    # equivalent, stop recursing and patch the dependency in.
    dep_name_no_toolchain = label_without_toolchain(dep_name)
    if dep_name_no_toolchain in builtin_deps:
      builtin_deps[dep_name_no_toolchain](target)
      return

    dep_desc = self.desc[dep_name]
    if dep_desc['type'] == 'source_set':
      for inner_name in self.resolve_dependencies(dep_name):
        self.apply_module_dependency(target, inner_name)

      # Any source set which has a source generated by an action doesn't need
      # to be depended on as we will depend on the action directly.
      if any(src in self.action_generated_files for src in dep_desc['sources']):
        return

      self.apply_module_sources_to_target(target, dep_desc)
    elif dep_desc['type'] == 'action':
      args = dep_desc['args']
      if "gen_merged_sql_metrics" in dep_name:
        dep_target = self.create_merged_sql_metrics_target(dep_name)
        target.deps.add(Label("//third_party/perfetto:" + dep_target.name))

        if target.type == 'cc_library' or target.type == 'cc_binary':
          target.srcs.update(dep_target.outs)
      elif args[0].endswith('/protoc'):
        (proto_target, cc_target) = self.create_proto_target(dep_name)
        if target.type == 'proto_library':
          dep_target_name = proto_target.name
        else:
          dep_target_name = cc_target.name
        target.deps.add(
            Label("//third_party/perfetto/protos:" + dep_target_name))
      else:
        raise Error('Unsupported action in target %s: %s' % (dep_target_name,
                                                            args))
    elif dep_desc['type'] == 'static_library':
      dep_target = self.create_target(dep_name)
      target.deps.add(Label("//third_party/perfetto:" + dep_target.name))
    elif dep_desc['type'] == 'group':
      for inner_name in self.resolve_dependencies(dep_name):
        self.apply_module_dependency(target, inner_name)
    elif dep_desc['type'] == 'executable':
      # Just create the dep target but don't add it as a dep because it's an
      # executable.
      self.create_target(dep_name)
    else:
      raise Error('Unknown target name %s with type: %s' %
                  (dep_name, dep_desc['type']))

  def create_merged_sql_metrics_target(self, gn_target_name):
    target_desc = self.desc[gn_target_name]
    gn_target_name_no_toolchain = label_without_toolchain(gn_target_name)
    target = Target(
      'genrule',
      'gen_merged_sql_metrics',
      gn_name=gn_target_name_no_toolchain,
    )
    target.outs.update(
      Label(src[src.index('gen/') + len('gen/'):])
      for src in target_desc.get('outputs', [])
    )
    target.cmd = '$(location gen_merged_sql_metrics_py) --cpp_out=$@ $(SRCS)'
    target.tools.update([
      'gen_merged_sql_metrics_py',
    ])
    target.srcs.update(
      Label(label_to_path(src))
      for src in target_desc.get('inputs', [])
      if src not in self.action_generated_files
    )
    self.build.add_target(target)
    return target

  def create_proto_target(self, gn_target_name):
    target_desc = self.desc[gn_target_name]
    args = target_desc['args']

    gn_target_name_no_toolchain = label_without_toolchain(gn_target_name)
    stripped_path = gn_target_name_no_toolchain.replace("protos/perfetto/", "")
    pretty_target_name = label_to_target_name_with_path(stripped_path)
    pretty_target_name = pretty_target_name.replace("_lite_gen", "")
    pretty_target_name = pretty_target_name.replace("_zero_gen", "_zero")

    proto_target = Target(
      'proto_library',
      pretty_target_name,
      gn_name=gn_target_name_no_toolchain
    )
    proto_target.is_pbzero = any("pbzero" in arg for arg in args)
    proto_target.srcs.update([
      Label(label_to_path(src).replace('protos/', ''))
      for src in target_desc.get('sources', [])
    ])
    if not proto_target.is_pbzero:
      proto_target.visibility.add("//visibility:public")
    self.proto_build.add_target(proto_target)

    for dep_name in self.resolve_dependencies(gn_target_name):
      self.apply_module_dependency(proto_target, dep_name)

    if proto_target.is_pbzero:
      # Remove all the protozero srcs from the proto_library.
      proto_target.srcs.difference_update(
          [src for src in proto_target.srcs if not src.label.endswith('.proto')])

      # Remove all the non-proto deps from the proto_library and add to the cc
      # library.
      cc_deps = [
        dep for dep in proto_target.deps
        if not dep.label.startswith('//third_party/perfetto/protos')
      ]
      proto_target.deps.difference_update(cc_deps)

      cc_target_name = proto_target.name + "_cc_proto"
      cc_target = Target('pbzero_cc_proto_library', cc_target_name,
                         gn_name=gn_target_name_no_toolchain)

      cc_target.deps.add(Label('//third_party/perfetto:libprotozero'))
      cc_target.deps.update(cc_deps)

      # Add the proto_library to the cc_target.
      cc_target.src_proto_library = \
          "//third_party/perfetto/protos:" + proto_target.name

      self.proto_build.add_target(cc_target)
    else:
      cc_target_name = proto_target.name + "_cc_proto"
      cc_target = Target('cc_proto_library',
                        cc_target_name, gn_name=gn_target_name_no_toolchain)
      cc_target.visibility.add("//visibility:public")
      cc_target.deps.add(
          Label("//third_party/perfetto/protos:" + proto_target.name))
      self.proto_build.add_target(cc_target)

    return (proto_target, cc_target)


  def create_target(self, gn_target_name):
    """Generate module(s) for a given GN target.

    Given a GN target name, generate one or more corresponding modules into a
    build file.

    Args:
        build: Build instance which is being generated.
        desc: JSON GN description.
        gn_target_name: GN target name for module generation.
    """

    target_desc = self.desc[gn_target_name]
    if target_desc['type'] == 'action':
      args = target_desc['args']
      if args[0].endswith('/protoc'):
        return self.create_proto_target(gn_target_name)
      else:
        raise Error('Unsupported action in target %s: %s' % (gn_target_name,
                                                            args))
    elif target_desc['type'] == 'executable':
      target_type = 'cc_binary'
    elif target_desc['type'] == 'static_library':
      target_type = 'cc_library'
    elif target_desc['type'] == 'source_set':
      target_type = 'filegroup'
    else:
      raise Error('Unknown target type: %s' % target_desc['type'])

    label_no_toolchain = label_without_toolchain(gn_target_name)
    target_name_path = label_to_target_name_with_path(label_no_toolchain)
    target_name = alias_targets.get(label_no_toolchain, target_name_path)
    target = Target(target_type, target_name, gn_name=label_no_toolchain)
    target.srcs.update(
        Label(label_to_path(src))
        for src in target_desc.get('sources', [])
        if src not in self.action_generated_files
    )

    for dep_name in self.resolve_dependencies(gn_target_name):
      self.apply_module_dependency(target, dep_name)

    self.build.add_target(target)
    return target

def main():
  parser = argparse.ArgumentParser(
      description='Generate BUILD from a GN description.')
  parser.add_argument(
      '--desc',
      help='GN description (e.g., gn desc out --format=json --all-toolchains "//*"'
  )
  parser.add_argument(
      '--repo-root',
      help='Standalone Perfetto repository to generate a GN description',
      default=repo_root(),
  )
  parser.add_argument(
      '--extras',
      help='Extra targets to include at the end of the BUILD file',
      default=os.path.join(repo_root(), 'BUILD.extras'),
  )
  parser.add_argument(
      '--output',
      help='BUILD file to create',
      default=os.path.join(repo_root(), 'BUILD'),
  )
  parser.add_argument(
      '--output-proto',
      help='Proto BUILD file to create',
      default=os.path.join(repo_root(), 'protos', 'BUILD'),
  )
  parser.add_argument(
      'targets',
      nargs=argparse.REMAINDER,
      help='Targets to include in the BUILD file (e.g., "//:perfetto_tests")')
  args = parser.parse_args()

  if args.desc:
    with open(args.desc) as f:
      desc = json.load(f)
  else:
    desc = create_build_description(args.repo_root)

  build_generator = BuildGenerator(desc)
  build, proto_build = build_generator.create_build_for_targets(
      args.targets or default_targets)
  with open(args.output, 'w') as f:
    writer = Writer(f)
    build.write(writer)
    writer.newline()

    with open(args.extras, 'r') as r:
      for line in r:
        writer.line(line.rstrip("\n\r"))

  with open(args.output_proto, 'w') as f:
    proto_build.write(Writer(f))

  return 0


if __name__ == '__main__':
  sys.exit(main())