# Copyright 2013 The Flutter Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import("//flutter/tools/fuchsia/dart/dart.gni")
import("//flutter/tools/fuchsia/dart/dart_package_config.gni")
import("//flutter/tools/fuchsia/dart/toolchain.gni")

# Defines a Dart library
#
# Parameters
#
#   sources
#     The list of all sources in this library.
#     These sources must be within source_dir.
#
#   package_root (optional)
#     Path to the directory hosting the library.
#     This is useful for generated content, and can be ignored otherwise.
#     Defaults to ".".
#
#   package_name (optional)
#     Name of the Dart package. This is used as an identifier in code that
#     depends on this library.
#
#   language_version (optional)
#     Specify the Dart language version to use for this package.
#     If language_version is not specified but pubspec is then the language
#     version will be read from the pubspec. If no language version can be
#     determined then we will default to version "2.8".
#     It is recommended to specify a language_version if it is well known
#     instead of relying on the pubspec file since this will improve compilation
#     times.
#
#   infer_package_name (optional)
#     Infer the package name based on the path to the package.
#
#     NOTE: Exactly one of package_name or infer_package_name must be set.
#
#   source_dir (optional)
#     Path to the directory containing the package sources, relative to
#     package_root. All non third-party dart files under source_dir must be
#     included in sources.
#     Defaults to "lib".
#
#   deps (optional)
#     List of labels this library depends on.
#
#   TODO(fxb/63133): non_dart_deps is deprecated. Use deps instead.
#   non_dart_deps (optional, deprecated)
#     List of labels this library depends on that are not Dart libraries. This
#     includes things like actions that generate Dart code. It typically doesn't
#     need to be set.
#     Note that these labels *must* have an explicit toolchain attached.
#
#   TODO(fxbug.dev/71902): set up allowlist for disable_source_verification when
#   dart_test no longer depends on dart_library.
#   NOTE: Do NOT disable source verification unless you are 100% sure it is
#   absolutely necessary.
#   disable_source_verification (optional)
#     Prevents source verification from being run on this target.
#
#   extra_sources (optional)
#     Additional sources to consider for analysis.
#
#   pubspec (optional)
#     Path to the pubspec.yaml. If not provided, will default to looking for
#     the pubspec.yaml in the package root. It is not common that this value will
#     need to be set but can be useful for generated code.
#
#   options_file (optional)
#     Path to the analysis_options.yaml file. If not provided, will default to
#     looking for the analysis_options.yaml in the package root. It is not common
#     that this value needs to be set but can be useful for generated code.
#
#   disable_metadata_entry (optional)
#     Prevents metedata entry from being written to the dart_packag_config json file.
#
#   null_safe (optional)
#     A flag that enables null safety check in dart libraries.
#
# Example of usage:
#
#   dart_library("baz") {
#     package_name = "foo.bar.baz"
#
#     sources = [
#       "blah.dart",
#     ]
#
#     deps = [
#       "//foo/bar/owl",
#     ]
#   }
if (current_toolchain == dart_toolchain) {
  template("dart_library") {
    forward_variables_from(invoker,
                           [
                             "visibility",
                             "hermetic_deps",
                           ])

    if (defined(invoker.package_name)) {
      package_name = invoker.package_name
    } else if (defined(invoker.infer_package_name) &&
               invoker.infer_package_name) {
      # Compute a package name from the label:
      #   //foo/bar --> foo.bar
      #   //foo/bar:blah --> foo.bar._blah
      #   //garnet/public/foo/bar --> foo.bar
      # Strip public directories.
      full_dir = get_label_info(":$target_name", "dir")
      package_name = full_dir
      package_name = string_replace(package_name, "//", "")
      package_name = string_replace(package_name, "/", ".")

      # If the last directory name does not match the target name, add the
      # target name to the resulting package name.
      name = get_label_info(":$target_name", "name")
      last_dir = get_path_info(full_dir, "name")
      if (last_dir != name) {
        package_name = "$package_name._$name"
      }
    } else {
      assert(false, "Must specify either a package_name or infer_package_name")
    }

    _dart_deps = []
    if (defined(invoker.deps)) {
      foreach(dep, invoker.deps) {
        _dart_deps += [ get_label_info(dep, "label_no_toolchain") ]
      }
    }

    _non_dart_deps = []
    if (defined(invoker.non_dart_deps)) {
      _non_dart_deps += invoker.non_dart_deps
    }

    package_root = "."
    if (defined(invoker.package_root)) {
      package_root = invoker.package_root
    }

    source_dir = "$package_root/lib"
    if (defined(invoker.source_dir)) {
      source_dir = "$package_root/${invoker.source_dir}"
    }

    assert(defined(invoker.sources), "Sources must be defined")

    disable_source_verification =
        defined(invoker.disable_source_verification) &&
        invoker.disable_source_verification
    if (disable_source_verification && invoker.sources == []) {
      not_needed([ source_dir ])
    }

    rebased_sources = []
    foreach(source, invoker.sources) {
      rebased_source_dir = rebase_path(source_dir)
      rebased_sources += [ "$rebased_source_dir/$source" ]
    }
    if (defined(invoker.extra_sources)) {
      foreach(source, invoker.extra_sources) {
        rebased_sources += [ rebase_path(source) ]
      }
    }
    source_file = "$target_gen_dir/$target_name.sources"
    write_file(source_file, rebased_sources, "list lines")

    # Dependencies of the umbrella group for the targets in this file.
    group_deps = []

    _public_deps = []
    if (defined(invoker.public_deps)) {
      _public_deps = invoker.public_deps
    }

    _metadata = {
      package_config_entries = [
        {
          name = package_name
          if (defined(invoker.language_version)) {
            language_version = invoker.language_version
          } else if (defined(invoker.null_safe) && invoker.null_safe) {
            language_version = "2.12"
          } else if (defined(invoker.pubspec)) {
            pubspec_path = rebase_path(invoker.pubspec, root_build_dir)
          }
          root_uri = rebase_path(package_root, root_build_dir)
          if (defined(invoker.source_dir)) {
            package_uri = invoker.source_dir
          } else {
            package_uri = "lib"
          }
        },
      ]
      dart_build_info = [
        {
          __is_current_target = false
          __package_name = package_name
          __deps = _dart_deps + _non_dart_deps
          __public_deps = _public_deps
          __rebased_sources = rebased_sources
        },
      ]

      dart_build_info_barrier = []
    }

    # When we generate a package_config for the analyzer we need to make sure
    # that we are including this library in that file. The dart_package_config
    # collects metadata from its dependencies so we create this group to expose
    # that data. We also expose this in the group target below so that users of
    # the dart_package_config target can just add the targets to the deps list.
    _publish_metadata_target_name = "${target_name}_package_metadata"
    group(_publish_metadata_target_name) {
      metadata = _metadata
    }

    _dart_package_config_target_name = "${target_name}_dart_package"
    _packages_path = "$target_gen_dir/${target_name}_package_config.json"
    dart_package_config(_dart_package_config_target_name) {
      # Do not publish the metadata to the dart_package_config json file if the
      # disable_metadata_entry flag is enabled in the dart_tool. The reason this is here
      # is to avoid entries that may have identical rootUris as fxb/58781 has highlighted.
      deps = _dart_deps
      if (!defined(invoker.disable_metadata_entry) ||
          !invoker.disable_metadata_entry) {
        deps += [ ":$_publish_metadata_target_name" ]
      }
      public_deps = _non_dart_deps
      outputs = [ _packages_path ]
      forward_variables_from(invoker, [ "testonly" ])
    }
    group_deps += [ ":$_dart_package_config_target_name" ]

    ################################
    # Dart source "verification"
    #
    # Warn if there are dart sources from the source directory that are
    # not explicitly part of sources. This may cause a failure when syncing to
    # another repository, as they will be excluded from the resulting BUILD
    # file.
    #
    # Also warn if nonexistent files are included in sources.
    if (!disable_source_verification) {
      source_verification_target_name = "${target_name}_source_verification"
      action(source_verification_target_name) {
        script = "//flutter/tools/fuchsia/dart/verify_sources.py"
        output_file = "$target_gen_dir/$target_name.missing"

        sources = rebased_sources
        outputs = [ output_file ]

        args = [
                 "--source_dir",
                 rebase_path(source_dir),
                 "--stamp",
                 rebase_path(output_file),
               ] + invoker.sources

        forward_variables_from(invoker, [ "testonly" ])

        # Deps may include codegen dependencies that generate dart sources.
        deps = _dart_deps + _non_dart_deps
      }
      group_deps += [ ":$source_verification_target_name" ]
    }

    # Generate a file that lists files containing full (including all direct and
    # transitive dependencies) sources for this target's dependencies.
    _all_deps_sources_list_target = "${target_name}.all_deps_sources.list"
    _all_deps_sources_list_file =
        "${target_gen_dir}/${target_name}.all_deps_sources.list"
    generated_file(_all_deps_sources_list_target) {
      forward_variables_from(invoker, [ "testonly" ])
      outputs = [ _all_deps_sources_list_file ]
      data_keys = [ "all_deps_sources" ]
      walk_keys = [ "all_deps_sources_barrier" ]
      deps = _dart_deps
    }

    # Generate full sources for this target by combining sources of this target
    # with full sources of all dependencies.
    #
    # The generated file contains sources of this target and all of its direct
    # and transitive dependencies.
    #
    # The output file is useful when writing depfiles for actions like dart
    # analyzer, which recursively reads all sources.
    _all_deps_sources_target = "${target_name}.all_deps_sources"
    _all_deps_sources_file = "${target_gen_dir}/${target_name}.all_deps_sources"
    action(_all_deps_sources_target) {
      forward_variables_from(invoker, [ "testonly" ])

      script = "//flutter/tools/fuchsia/dart/merge_deps_sources.py"

      outputs = [ _all_deps_sources_file ]
      depfile = "${_all_deps_sources_file}.d"
      args = [
               "--output",
               rebase_path(outputs[0], root_build_dir),
               "--depfile",
               rebase_path(depfile, root_build_dir),
               "--source_lists",
               "@" + rebase_path(_all_deps_sources_list_file, root_build_dir),
               "--sources",
             ] + rebase_path(rebased_sources, root_build_dir)

      inputs = [ _all_deps_sources_list_file ]
      deps = [ ":${_all_deps_sources_list_target}" ]
      metadata = {
        all_deps_sources = [ rebase_path(outputs[0], root_build_dir) ]
        all_deps_sources_barrier = []
      }
    }
    group_deps += [ ":${_all_deps_sources_target}" ]

    not_needed(invoker, [ "options_file" ])

    group(target_name) {
      # _dart_deps are added here to ensure they are fully built.
      # Up to this point, only the targets generating .packages had been
      # depended on.
      deps = _dart_deps + _non_dart_deps

      public_deps = group_deps

      metadata = _metadata
      forward_variables_from(invoker, [ "testonly" ])
    }
  }
} else {  # Not the Dart toolchain.
  template("dart_library") {
    group(target_name) {
      forward_variables_from(invoker, [ "testonly" ])
      not_needed(invoker, "*")

      public_deps = [ ":$target_name($dart_toolchain)" ]
    }
  }
}
