# Copyright 2022 The Pigweed Authors
#
# 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
#
#     https://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.

import("//build_overrides/pigweed.gni")

import("$dir_pw_compilation_testing/negative_compilation_test.gni")
import("$dir_pw_toolchain/universal_tools.gni")
import("$pw_external_boringssl/boringssl.gni")
import("$pw_external_chre/chre.gni")
import("$pw_external_googletest/googletest.gni")
import("$pw_external_mbedtls/mbedtls.gni")

declare_args() {
  # Regular expressions matching the paths of the source files to be excluded
  # from the analysis. clang-tidy provides no alternative option.
  #
  # For example, the following disables clang-tidy on all source files in the
  # third_party directory:
  #
  #   pw_toolchain_STATIC_ANALYSIS_SKIP_SOURCES_RES = ["third_party/.*"]
  #
  pw_toolchain_STATIC_ANALYSIS_SKIP_SOURCES_RES = []

  # Disable clang-tidy for specific include paths. In the clang-tidy command,
  # include paths that end with one of these, or match as a regular expression,
  # are switched from -I to -isystem, which causes clang-tidy to ignore them.
  # Unfortunately, clang-tidy provides no other way to filter header files.
  #
  # For example, the following ignores header files in "repo/include":
  #
  #   pw_toolchain_STATIC_ANALYSIS_SKIP_INCLUDE_PATHS = ["repo/include"]
  #
  # While the following ignores all third-party header files:
  #
  #   pw_toolchain_STATIC_ANALYSIS_SKIP_INCLUDE_PATHS = [".*/third_party/.*"]
  #
  pw_toolchain_STATIC_ANALYSIS_SKIP_INCLUDE_PATHS = []
}

# Third-party software with Pigweed-supported build files that do not pass all
# clang-tidy checks.
_excluded_third_party_dirs = [
  dir_pw_third_party_mbedtls,
  dir_pw_third_party_boringssl,
  dir_pw_third_party_googletest,
  dir_pw_third_party_chre,
]

# Creates a toolchain target for static analysis.
#
# The generated toolchain runs clang-tidy on all source files that are not
# excluded by pw_toolchain_STATIC_ANALYSIS_SKIP_SOURCES_RES or
# pw_toolchain_STATIC_ANALYSIS_SKIP_INCLUDE_PATHS.
#
# Args:
#   cc: (required) String indicating the C compiler to use.
#   cxx: (required) String indicating the C++ compiler to use.
#   static_analysis: (required) A scope defining args to apply to the
#     static_analysis toolchain.
#   static_analysis.enabled: (required) Bool used to indicate whether
#       static_analysis should be enabled for the toolchain where scope is
#       applied to. Note that static_analysis.enabled must be set in order to
#       use this toolchain.
#   static_analysis.clang_tidy_path: (optional) String indicating clang-tidy bin
#       to use.
#   static_analysis.cc_post: (optional) String defining additional commands to
#       append to cc tool's command list (i.e command(s) to run after cc command
#       chain).
#   static_analysis.cxx_post: (optional) String defining additional commands to
#       append to cxx tool's command list (i.e command(s) to run after cxx
#       command chain).
template("pw_static_analysis_toolchain") {
  invoker_toolchain_args = invoker.defaults
  assert(defined(invoker.static_analysis), "static_analysis scope missing.")
  _static_analysis_args = invoker.static_analysis
  assert(defined(_static_analysis_args.enabled),
         "static_analysis.enabled is missing")
  assert(_static_analysis_args.enabled,
         "static_analysis.enabled must be true to use this toolchain.")

  _skipped_regexps = []
  _skipped_include_paths = []
  foreach(third_party_dir, _excluded_third_party_dirs) {
    if (third_party_dir != "") {
      _skipped_include_paths += [
        third_party_dir + "/include",
        third_party_dir,
      ]
    }
  }

  _skipped_regexps += pw_toolchain_STATIC_ANALYSIS_SKIP_SOURCES_RES
  _skipped_include_paths += pw_toolchain_STATIC_ANALYSIS_SKIP_INCLUDE_PATHS

  # Clang tidy is invoked by a wrapper script which implements the missing
  # option --source-filter.
  _clang_tidy_py_path =
      rebase_path("$dir_pw_toolchain/py/pw_toolchain/clang_tidy.py",
                  root_build_dir)
  _clang_tidy_py = "${python_path} ${_clang_tidy_py_path}"
  _source_root = rebase_path("//", root_build_dir)
  _source_exclude = ""
  foreach(pattern, _skipped_regexps) {
    _source_exclude = _source_exclude + " --source-exclude '${pattern}'"
  }
  _skip_include_path = ""
  foreach(pattern, _skipped_include_paths) {
    _skip_include_path =
        _skip_include_path + " --skip-include-path '${pattern}'"
  }
  _clang_tidy_path = ""
  if (defined(_static_analysis_args.clang_tidy_path)) {
    _clang_tidy_path =
        "--clang-tidy " +
        rebase_path(_static_analysis_args.clang_tidy_path, root_build_dir)
  }

  toolchain(target_name) {
    # Uncomment this line to see which toolchains generate other toolchains.
    # print("Generating toolchain: ${target_name} by ${current_toolchain}")

    tool("asm") {
      depfile = "{{output}}.d"
      command = pw_universal_stamp.command
      depsformat = "gcc"
      description = "as {{output}}"
      outputs = [
        # Use {{source_file_part}}, which includes the extension, instead of
        # {{source_name_part}} so that object files created from <file_name>.c
        # and <file_name>.cc sources are unique.
        "{{source_out_dir}}/{{target_output_name}}.{{source_file_part}}.o",
      ]
    }

    assert(defined(invoker.cc), "toolchain is missing 'cc'")
    tool("cc") {
      _post_command_hook = ""
      if (defined(_static_analysis_args.cc_post) &&
          _static_analysis_args.cc_post != "") {
        _post_command_hook += " && " + _static_analysis_args.cc_post
      }

      depfile = "{{output}}.d"
      command = string_join(" ",
                            [
                              _clang_tidy_py,
                              _source_exclude,
                              _skip_include_path,
                              _clang_tidy_path,
                              "--source-file {{source}}",
                              "--source-root '${_source_root}'",
                              "--export-fixes {{output}}.yaml",
                              "--",
                              invoker.cc,
                              "END_OF_INVOKER",
                              "-MMD -MF $depfile",  # Write out dependencies.
                              "{{cflags}}",
                              "{{cflags_c}}",  # Must come after {{cflags}}.
                              "{{defines}}",
                              "{{include_dirs}}",
                              "-c {{source}}",
                              "-o {{output}}",
                            ]) + " && touch {{output}}" + _post_command_hook
      depsformat = "gcc"
      description = "clang-tidy {{source}}"
      outputs =
          [ "{{source_out_dir}}/{{target_output_name}}.{{source_file_part}}.o" ]
    }

    assert(defined(invoker.cxx), "toolchain is missing 'cxx'")
    tool("cxx") {
      _post_command_hook = ""
      if (defined(_static_analysis_args.cxx_post) &&
          _static_analysis_args.cxx_post != "") {
        _post_command_hook += " && " + _static_analysis_args.cxx_post
      }

      depfile = "{{output}}.d"
      command = string_join(" ",
                            [
                              _clang_tidy_py,
                              _source_exclude,
                              _skip_include_path,
                              _clang_tidy_path,
                              "--source-file {{source}}",
                              "--source-root '${_source_root}'",
                              "--export-fixes {{output}}.yaml",
                              "--",
                              invoker.cxx,
                              "END_OF_INVOKER",
                              "-MMD -MF $depfile",  # Write out dependencies.
                              "{{cflags}}",
                              "{{cflags_cc}}",  # Must come after {{cflags}}.
                              "{{defines}}",
                              "{{include_dirs}}",
                              "-c {{source}}",
                              "-o {{output}}",
                            ]) + " && touch {{output}}" + _post_command_hook
      depsformat = "gcc"
      description = "clang-tidy {{source}}"
      outputs =
          [ "{{source_out_dir}}/{{target_output_name}}.{{source_file_part}}.o" ]
    }

    tool("objc") {
      depfile = "{{output}}.d"
      command = pw_universal_stamp.command
      depsformat = "gcc"
      description = "objc {{source}}"
      outputs =
          [ "{{source_out_dir}}/{{target_output_name}}.{{source_file_part}}.o" ]
    }

    tool("objcxx") {
      depfile = "{{output}}.d"
      command = pw_universal_stamp.command
      depsformat = "gcc"
      description = "objc++ {{output}}"
      outputs =
          [ "{{source_out_dir}}/{{target_output_name}}.{{source_file_part}}.o" ]
    }

    tool("alink") {
      command = "rm -f {{output}} && touch {{output}}"
      description = "ar {{target_output_name}}{{output_extension}}"
      outputs = [ "{{output_dir}}/{{target_output_name}}{{output_extension}}" ]
      default_output_extension = ".a"
      default_output_dir = "{{target_out_dir}}/lib"
    }

    tool("link") {
      if (host_os == "win") {
        # Force the extension to '.bat', empty bat scripts are still
        # executable and will not raise errors.
        _output = "{{output_dir}}/{{target_output_name}}.bat"
        command = pw_universal_stamp.command
        default_output_extension = ".bat"
      } else {
        default_output_extension = ""
        _output = "{{output_dir}}/{{target_output_name}}{{output_extension}}"
        command = "touch {{output}} && chmod +x {{output}}"
      }
      description = "ld $_output"
      outputs = [ _output ]
      default_output_dir = "{{target_out_dir}}/bin"
    }

    tool("solink") {
      _output = "{{output_dir}}/{{target_output_name}}{{output_extension}}"
      command = pw_universal_stamp.command
      description = "ld -shared $_output"
      outputs = [ _output ]
      default_output_dir = "{{target_out_dir}}/lib"
      default_output_extension = ".so"
    }

    tool("stamp") {
      # GN-ism: GN gets mad if you directly forward the contents of
      # pw_universal_stamp.
      _stamp = pw_universal_stamp
      forward_variables_from(_stamp, "*")
    }

    tool("copy") {
      # GN-ism: GN gets mad if you directly forward the contents of
      # pw_universal_copy.
      _copy = pw_universal_copy
      forward_variables_from(_copy, "*")
    }

    # Build arguments to be overridden when compiling cross-toolchain:
    #
    #   pw_toolchain_defaults: A scope setting defaults to apply to GN targets
    #     in this toolchain. It is analogous to $pw_target_defaults in
    #     $dir_pigweed/pw_vars_default.gni.
    #
    #   pw_toolchain_SCOPE: A copy of the invoker scope that defines the
    #     toolchain. Used for generating derivative toolchains.
    #
    toolchain_args = {
      pw_toolchain_SCOPE = {
      }
      pw_toolchain_SCOPE = {
        forward_variables_from(invoker, "*")
        name = target_name
      }
      forward_variables_from(invoker_toolchain_args, "*")

      # Disable compilation testing for static analysis toolchains.
      pw_compilation_testing_NEGATIVE_COMPILATION_ENABLED = false

      # Always disable coverage generation since we will not actually run the
      # instrumented binaries to produce a profraw file.
      pw_toolchain_COVERAGE_ENABLED = false

      # Static analysis toolchains cannot run binaries.
      pw_unit_test_AUTOMATIC_RUNNER = ""
    }

    _generate_rust_tools = defined(invoker.rustc)
    if (_generate_rust_tools) {
      if (defined(invoker.ld)) {
        _rustc_linker = "-Clinker=${invoker.ld}"
      } else {
        _rustc_linker = ""
      }

      _rustc_common_flags = [
        # TODO: b/234872510 - Ensure this works with Windows.
        "RUST_BACKTRACE=1",
        "{{rustenv}}",
        invoker.rustc,
        "{{source}}",
        "--crate-name {{crate_name}}",
        _rustc_linker,
        "{{externs}}",
        "{{rustdeps}}",
        "{{rustflags}}",
        "-D warnings",
        "--color always",
        "--emit=dep-info={{output}}.d,link",
        "-o {{output_dir}}/{{target_output_name}}{{output_extension}}",
      ]

      _rustc_command =
          string_join(" ",
                      _rustc_common_flags + [ "--crate-type {{crate_type}}" ])

      _output = "{{output_dir}}/{{target_output_name}}{{output_extension}}"

      tool("rust_bin") {
        description = "rustc {{output}}"
        default_output_dir = "{{target_out_dir}}/bin"
        depfile = "{{output}}.d"
        command = string_join(
                " ",
                _rustc_common_flags + [
                      # Compile executable as rlib for static analysis, to skip
                      # the link step. Because C/C++ static analysis targets are
                      # not compiled, linking might fail if Rust depends on FFI
                      # symbols.
                      "--crate-type rlib",

                      # Allow dead_code, because `main` would be unused.
                      "-A dead_code",
                    ])
        outputs = [ _output ]
      }

      tool("rust_rlib") {
        description = "rustc {{output}}"
        default_output_dir = "{{target_out_dir}}/lib"
        depfile = "{{output}}.d"
        output_prefix = "lib"
        default_output_extension = ".rlib"
        command = _rustc_command
        outputs = [ _output ]
      }

      tool("rust_staticlib") {
        description = "rustc {{output}}"
        default_output_dir = "{{target_out_dir}}/lib"
        depfile = "{{output}}.d"
        output_prefix = "lib"
        default_output_extension = ".a"
        command = _rustc_command
        outputs = [ _output ]
      }

      if (defined(invoker.is_host_toolchain) && invoker.is_host_toolchain) {
        if (!defined(invoker_toolchain_args.current_os)) {
          toolchain_os = ""
        } else {
          # Determine OS of toolchain, which is the builtin argument "current_os".
          toolchain_os = invoker_toolchain_args.current_os
        }

        if (toolchain_os == "mac") {
          _dylib_extension = ".dylib"
        } else if (toolchain_os == "win") {
          _dylib_extension = ".dll"
        } else {
          _dylib_extension = ".so"
        }

        tool("rust_macro") {
          description = "rustc {{output}}"
          default_output_dir = "{{target_out_dir}}/lib"
          depfile = "{{output}}.d"
          output_prefix = "lib"
          default_output_extension = _dylib_extension
          command = _rustc_command
          outputs = [ _output ]
        }
      }
    }
  }
}
