# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import("//build/config/clang/clang.gni")
import("//build/config/rust.gni")
import("//build/config/sysroot.gni")
import("//build/rust/rust_static_library.gni")

if (is_win) {
  import("//build/toolchain/win/win_toolchain_data.gni")
}

_rustc_base_path = rust_sysroot

# TODO(danakj): When we're using the Android prebuilt toolchain, there's no
# bindgen present. bindgen is for the host platform so using the linux one will
# work.
if (!use_chromium_rust_toolchain) {
  _rustc_base_path = "//third_party/rust-toolchain"
}

_bindgen_path = "${_rustc_base_path}/bin/bindgen"
if (is_win) {
  _bindgen_path = "${_bindgen_path}.exe"
}

# Template to build Rust/C bindings with bindgen.
#
# This template expands to a static_library containing the Rust side of the
# bindings. Simply treat it as a public dependency.
#
# Parameters:
#
# header:
#   The .h file to generate bindings for.
#
# deps: (optional)
#   C targets on which the headers depend in order to build successfully.
#
# configs: (optional)
#   C compilation targets determine the correct list of -D and -I flags based
#   on their dependencies and any configs applied. The same applies here. Set
#   any configs here as if this were a C target.
#
# bindgen_flags: (optional)
#    the additional bindgen flags which are passed to the executable
#
# Rust targets depending on the output must include! the generated file.
#
template("rust_bindgen") {
  assert(defined(invoker.header),
         "Must specify the C header file to make bindings for.")
  action(target_name) {
    # bindgen relies on knowing the {{defines}} and {{include_dirs}} required
    # to build the C++ headers which it's parsing. These are passed to the
    # script's args and are populated using deps and configs.
    forward_variables_from(invoker,
                           TESTONLY_AND_VISIBILITY + [
                                 "deps",
                                 "configs",
                               ])

    sources = [ invoker.header ]

    if (!defined(configs)) {
      configs = []
    }

    # Several important compiler flags come from default_compiler_configs
    configs += default_compiler_configs

    output_dir = "$target_gen_dir"
    out_gen_rs = "$output_dir/${target_name}.rs"

    script = rebase_path("//build/rust/run_bindgen.py")
    inputs = [ _bindgen_path ]

    depfile = "$target_out_dir/${target_name}.d"
    outputs = [ out_gen_rs ]

    lib_path = ""
    if (is_linux) {
      # Linux clang, and clang libs, use a shared libstdc++, which we must
      # point to.
      clang_ld_path = rebase_path(clang_base_path + "/lib", root_build_dir)
      lib_path += "${clang_ld_path}:"
    }
    rust_ld_path = rebase_path(_rustc_base_path + "/lib", root_build_dir)
    lib_path += "${rust_ld_path}"

    args = [
      "--exe",
      rebase_path(_bindgen_path),
      "--header",
      rebase_path(invoker.header, root_build_dir),
      "--depfile",
      rebase_path(depfile, root_build_dir),
      "--output",
      rebase_path(out_gen_rs, root_build_dir),
      "--ld-library-path",
      lib_path,
    ]

    if (defined(invoker.bindgen_flags)) {
      args += [ "--bindgen-flags" ]
      foreach(flag, invoker.bindgen_flags) {
        args += [ flag ]
      }
    }

    args += [
      "--",
      "{{defines}}",
      "{{include_dirs}}",
      "{{cflags}}",
      "{{cflags_c}}",
    ]

    # Clang ships with some headers, which are installed along side the binary,
    # and which clang itself finds by default, but libclang does not (see also
    # https://reviews.llvm.org/D95396 which would resolve this but was reverted).
    clang_headers = rebase_path(
            clang_base_path + "/lib/clang/" + clang_version + "/include",
            root_build_dir)
    if (is_win) {
      args += [ "-imsvc" + clang_headers ]
    } else {
      args += [ "-isystem" + clang_headers ]
    }

    if (is_win) {
      # On Windows we fall back to using system headers from a sysroot from
      # depot_tools. This is negotiated by python scripts and the result is
      # available in //build/toolchain/win/win_toolchain_data.gni. From there
      # we get the `include_flags_imsvc` which point to the system headers.
      if (host_cpu == "x86") {
        win_toolchain_data = win_toolchain_data_x86
      } else if (host_cpu == "x64") {
        win_toolchain_data = win_toolchain_data_x64
      } else if (host_cpu == "arm64") {
        win_toolchain_data = win_toolchain_data_arm64
      } else {
        error("Unsupported host_cpu, add it to win_toolchain_data.gni")
      }
      args += [ "${win_toolchain_data.include_flags_imsvc}" ]
    }

    # Passes C comments through as rustdoc attributes.
    if (is_win) {
      args += [ "/clang:-fparse-all-comments" ]
    } else {
      args += [ "-fparse-all-comments" ]
    }

    # Default configs include "-fvisibility=hidden", and for some reason this
    # causes bindgen not to emit function bindings. Override it.
    if (!is_win) {
      args += [ "-fvisibility=default" ]
    }

    if (is_win) {
      # We pass MSVC style flags to clang on Windows, and libclang needs to be
      # told explicitly to accept them.
      args += [ "--driver-mode=cl" ]

      # On Windows, libclang adds arguments that it then fails to understand.
      # -fno-spell-checking
      # -fallow-editor-placeholders
      # These should not cause bindgen to fail.
      args += [ "-Wno-unknown-argument" ]

      # Replace these two arguments with a version that clang-cl can parse.
      args += [
        "/clang:-fno-spell-checking",
        "/clang:-fallow-editor-placeholders",
      ]
    }

    if (!is_cfi) {
      # LLVM searches for a default CFI ignorelist at (exactly)
      # $(cwd)/lib/clang/$(llvm_version)/share/cfi_ignorelist.txt
      # Even if we provide a custom -fsanitize-ignorelist, the absence
      # of this default file will cause a fatal error. clang finds
      # it within third_party/llvm-build, but for bindgen our cwd
      # is the $out_dir. We _could_ create this file at the right
      # location within the outdir using a "copy" target, but as
      # we don't actually generate code within bindgen, the easier
      # option is to tell bindgen to ignore all CFI ignorelists.
      args += [ "-fno-sanitize-ignorelist" ]
    }
  }
}