text: # Copyright 2016 The Tulsi Authors. All rights reserved.
  #
  # 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.
  
  """Skylark rules supporting Tulsi.
  
  This file provides Bazel aspects used to obtain information about a given
  project and pass it back to Tulsi.
  """
  
  load(
      ":tulsi_aspects_paths.bzl",
      "AppleBundleInfo",
      "AppleTestInfo",
      "IosExtensionBundleInfo",
      "SwiftClangModuleInfo",
      "SwiftInfo",
  )
  load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
  
  # Defensive list of features that can appear in the C++ toolchain, but that we
  # definitely don't want to enable (meaning we don't want them to contribute
  # command line flags).
  UNSUPPORTED_FEATURES = [
      "thin_lto",
      "module_maps",
      "use_header_modules",
      "fdo_instrument",
      "fdo_optimize",
  ]
  
  # List of all of the attributes that can link from a Tulsi-supported rule to a
  # Tulsi-supported dependency of that rule.
  # For instance, an ios_application's "binary" attribute might link to an
  # objc_binary rule which in turn might have objc_library's in its "deps"
  # attribute.
  _TULSI_COMPILE_DEPS = [
      "binary",
      "bundles",
      "deps",
      "extension",
      "extensions",
      "frameworks",
      "settings_bundle",
      "srcs",  # To propagate down onto rules which generate source files.
      "tests",  # for test_suite when the --noexpand_test_suites flag is used.
      "_implicit_tests",  # test_suites without a `tests` attr have an '$implicit_tests' attr instead.
      "test_bundle",
      "test_host",
      # Special attribute name which serves as an escape hatch intended for custom
      # rule creators who use non-standard attribute names for rule dependencies
      # and want those dependencies to show up in Xcode.
      "tulsi_deps",
      "watch_application",
      "xctest_app",
  ]
  
  # These are attributes that contain bundles but should not be considered as
  # embedded bundles. For example, test bundles depend on app bundles
  # to be test hosts in bazel semantics, but in reality, Xcode treats the test
  # bundles as an embedded bundle of the app.
  _TULSI_NON_EMBEDDEDABLE_ATTRS = [
      "test_host",
      "xctest_app",
  ]
  
  # List of all attributes whose contents should resolve to "support" files; files
  # that are used by Bazel to build but do not need special handling in the
  # generated Xcode project. For example, Info.plist and entitlements files.
  _SUPPORTING_FILE_ATTRIBUTES = [
      "app_icons",
      "entitlements",
      "infoplist",
      "infoplists",
      "resources",
      "strings",
      "structured_resources",
      "storyboards",
      "xibs",
  ]
  
  # List of rules whose outputs should be treated as generated sources.
  _SOURCE_GENERATING_RULES = [
      "j2objc_library",
  ]
  
  # List of rules whose outputs should be treated as generated sources that do not
  # use ARC.
  _NON_ARC_SOURCE_GENERATING_RULES = [
      "objc_proto_library",
  ]
  
  # Whitelist of all extensions to include when scanning target.files for generated
  # files. This helps avoid but not prevent the following:
  #
  # _tulsi-include maps generated files from multiple configurations into one
  # directory for inclusion in the generated project. This can cause issues when
  # conflicting files are generated, e.g.
  #
  # library/subpath/foobar (an executable)
  # library/subpath/foobar/dependency/dep_lib.a
  #
  # Tulsi would fail trying to copy both of these as it would require `foobar` to
  # be both a file and directory. As a partial workaround, we only copy in files
  # that we believe to be 'source files' and thus unlikely to also be folders.
  _GENERATED_SOURCE_FILE_EXTENSIONS = [
      "c",
      "cc",
      "cpp",
      "h",
      "hpp",
      "m",
      "mm",
      "swift",
      "swiftmodule",
  ]
  
  TulsiSourcesAspectInfo = provider(
      fields = {
          "transitive_info_files": """
  The file actions used to save this rule's info and that of all of its transitive dependencies as
  well as any Info plists required for extensions.
  """,
          "inheritable_attributes": """
  The inheritable attributes of this rule, expressed as a dict instead of a struct to allow easy
  joining.
  """,
          "transitive_attributes": """
  Transitive attributes that should be applied to every rule that depends on this rule.
  """,
          "artifacts": """
  Artifacts from this rule.
  """,
          "filtering_info": """
  Filtering information for this target. Only for test target, otherwise is None.
  """,
      },
  )
  
  TulsiOutputAspectInfo = provider(
      fields = {
          "transitive_generated_files": "Depset of tulsi generated files.",
          "transitive_embedded_bundles": "Depset of all bundles embedded into this target.",
      },
  )
  
  def _dict_omitting_none(**kwargs):
      """Creates a dict from the args, dropping keys with None or [] values."""
      return {
          name: kwargs[name]
          for name in kwargs
          # Skylark doesn't support "is"; comparison is explicit for correctness.
          # pylint: disable=g-equals-none,g-explicit-bool-comparison
          if kwargs[name] != None and kwargs[name] != []
      }
  
  def _struct_omitting_none(**kwargs):
      """Creates a struct from the args, dropping keys with None or [] values."""
      return struct(**_dict_omitting_none(**kwargs))
  
  def _convert_outpath_to_symlink_path(path):
      """Converts full output paths to their tulsi-symlink equivalents.
  
      Bazel output paths are unstable, prone to change with architecture,
      platform or flag changes. Therefore we can't rely on them to supply to Xcode.
      Instead, we will root all outputs under a stable tulsi dir,
      and the bazel_build.py script will link the artifacts into the correct
      location under it.
  
      Tulsi root is located at WORKSPACE/bazel-exec-root-link/_tulsi-includes/x/x/.
      The two "x" directories are stubs to match the number of path components, so
      that relative paths work with the new location. Some Bazel outputs, like
      module maps, use relative paths to reference other files in the build.
  
      The leading underscore in _tulsi-includes is present as Bazel will clear
      all directories that don't start with '.', '_', or 'bazel-' when it builds.
      Otherwise, upon a build failure, _tulsi-includes would be removed and
      indexing and auto-completion for generated files would no longer work until
      the next successful build.
  
      In short, this method will transform
        bazel-out/ios-x86_64-min7.0/genfiles/foo
      to
        _tulsi-includes/x/x/foo
  
      This is currently enabled for everything although it will only affect
      generated files.
  
      Args:
        path: path to transform
  
      Returns:
        A string that is the original path modified according to the rules.
      """
  
      # Transform paths of the form:
      #   bazel-[whatever]/[platform-config]/symlink[/.*]
      # to:
      #   _tulsi-includes/x/x/symlink[/.*]
      first_dash = path.find("-")
      components = path.split("/")
      if (len(components) > 2 and
          first_dash >= 0 and
          first_dash < len(components[0])):
          return "_tulsi-includes/x/x/" + "/".join(components[3:])
      return path
  
  def _is_bazel_external_file(f):
      """Returns True if the given file is a Bazel external file."""
      return f.path.startswith("external/")
  
  def _file_metadata(f):
      """Returns metadata about a given File."""
      if not f:
          return None
  
      # Special case handling for Bazel external files which have a path that starts
      # with 'external/' but their short_path and root.path have no mention of being
      # external.
      out_path = f.path if _is_bazel_external_file(f) else f.short_path
      if not f.is_source:
          root_path = f.root.path
          symlink_path = _convert_outpath_to_symlink_path(root_path)
          if symlink_path == root_path:
              # The root path should always be bazel-out/... and thus is expected to be
              # updated.
              print('Unexpected root path "%s". Please report.' % root_path)
              root_execution_path_fragment = root_path
          else:
              root_execution_path_fragment = symlink_path
      else:
          root_execution_path_fragment = None
  
      # At the moment (Oct. 2016), Bazel disallows most files without extensions.
      # As a temporary hack, Tulsi treats File instances pointing at extension-less
      # paths as directories. This is extremely fragile and must be replaced with
      # logic properly homed in Bazel.
      is_dir = (f.basename.find(".") == -1)
  
      return _struct_omitting_none(
          path = out_path,
          src = f.is_source,
          root = root_execution_path_fragment,
          is_dir = is_dir,
      )
  
  def _file_metadata_by_replacing_path(f, new_path, new_is_dir = None):
      """Returns a copy of the f _file_metadata struct with the given path."""
      root_path = _get_opt_attr(f, "rootPath")
      if new_is_dir == None:
          new_is_dir = f.is_dir
  
      return _struct_omitting_none(
          path = new_path,
          src = f.src,
          root = root_path,
          is_dir = new_is_dir,
      )
  
  def _depset_to_file_metadata_list(a_depset):
      """"Converts a depset of files into a list of _file_metadata structs."""
      return [_file_metadata(f) for f in a_depset.to_list()]
  
  def _collect_artifacts(obj, attr_path):
      """Returns a list of Artifact objects for the attr_path in obj."""
      return [
          f
          for src in _getattr_as_list(obj, attr_path)
          for f in _get_opt_attr(src, "files").to_list()
      ]
  
  def _collect_files(obj, attr_path, convert_to_metadata = True):
      """Returns a list of artifact_location's for the attr_path in obj."""
      if convert_to_metadata:
          return [_file_metadata(f) for f in _collect_artifacts(obj, attr_path)]
      else:
          return _collect_artifacts(obj, attr_path)
  
  def _collect_first_file(obj, attr_path):
      """Returns a the first artifact_location for the attr_path in obj."""
      files = _collect_files(obj, attr_path)
      if not files:
          return None
      return files[0]
  
  def _collect_supporting_files(rule_attr, convert_to_metadata = True):
      """Extracts 'supporting' files from the given rule attributes."""
      all_files = []
      for attr in _SUPPORTING_FILE_ATTRIBUTES:
          all_files += _collect_files(
              rule_attr,
              attr,
              convert_to_metadata = convert_to_metadata,
          )
      return all_files
  
  def _collect_bundle_paths(rule_attr, bundle_attributes, bundle_ext):
      """Extracts subpaths with the given bundle_ext for the given attributes."""
      discovered_paths = dict()
      bundles = []
      if not bundle_ext.endswith("/"):
          bundle_ext += "/"
      bundle_ext_len = len(bundle_ext) - 1
  
      for attr in bundle_attributes:
          for f in _collect_files(rule_attr, attr):
              end = f.path.find(bundle_ext)
              if end < 0:
                  continue
              end += bundle_ext_len
  
              path = f.path[:end]
              root_path = _get_opt_attr(f, "rootPath")
              full_path = str(root_path) + ":" + path
              if full_path in discovered_paths:
                  continue
  
              # Using the 'discovered_paths' as a set, we will only be checking for the existence of a
              # key, the actual value does not matter so assign it 'None'.
              discovered_paths[full_path] = None
  
              # Generally Xcode treats bundles as special files so they should not be
              # flagged as directories.
              bundles.append(_file_metadata_by_replacing_path(f, path, False))
      return bundles
  
  def _collect_asset_catalogs(rule_attr):
      """Extracts xcassets directories from the given rule attributes."""
      attrs = ["app_asset_catalogs", "asset_catalogs"]
      bundles = _collect_bundle_paths(rule_attr, attrs, ".xcassets")
      bundles.extend(_collect_bundle_paths(rule_attr, attrs, ".xcstickers"))
  
      return bundles
  
  def _collect_bundle_imports(rule_attr):
      """Extracts bundle directories from the given rule attributes."""
      return _collect_bundle_paths(
          rule_attr,
          ["bundle_imports", "settings_bundle"],
          ".bundle",
      )
  
  def _collect_framework_imports(rule_attr):
      """Extracts framework directories from the given rule attributes."""
      return _collect_bundle_paths(
          rule_attr,
          ["framework_imports"],
          ".framework",
      )
  
  def _collect_xcdatamodeld_files(obj, attr_path):
      """Returns artifact_location's for xcdatamodeld's for attr_path in obj."""
      files = _collect_files(obj, attr_path)
      if not files:
          return []
      discovered_paths = dict()
      datamodelds = []
      for f in files:
          end = f.path.find(".xcdatamodel/")
          if end < 0:
              continue
          end += 12
  
          path = f.path[:end]
          root_path = _get_opt_attr(f, "rootPath")
          full_path = str(root_path) + ":" + path
          if full_path in discovered_paths:
              continue
  
          # Using the 'discovered_paths' as a set, we will only be checking for the existence of a
          # key, the actual value does not matter so assign it 'None'.
          discovered_paths[full_path] = None
          datamodelds.append(_file_metadata_by_replacing_path(f, path, False))
      return datamodelds
  
  def _collect_dependencies(rule_attr, attr_name):
      """Collects Bazel targets for a dependency attr.
  
      Args:
        rule_attr: The Bazel rule.attr whose dependencies should be collected.
        attr_name: attribute name to inspect for dependencies.
  
      Returns:
        A list of the Bazel target dependencies of the given rule.
      """
      return [
          dep
          for dep in _getattr_as_list(rule_attr, attr_name)
          if type(dep) == "Target" and
             (TulsiSourcesAspectInfo in dep or TulsiOutputAspectInfo in dep)
      ]
  
  def _collect_dependency_labels(rule, filter, attr_list):
      """Collects Bazel labels for a list of dependency attributes.
  
      Args:
        rule: The Bazel rule whose dependencies should be collected.
        filter: Filter to apply when gathering dependencies.
        attr_list: List of attribute names potentially containing Bazel labels for
            dependencies of the given rule.
  
      Returns:
        A list of the Bazel labels of dependencies of the given rule.
      """
      attr = rule.attr
      deps = [
          dep
          for attribute in attr_list
          for dep in _filter_deps(
              filter,
              _collect_dependencies(attr, attribute),
          )
      ]
      return [dep.label for dep in deps if hasattr(dep, "label")]
  
  def _get_opt_attr(obj, attr_path):
      """Returns the value at attr_path on the given object if it is set."""
      attr_path = attr_path.split(".")
      for a in attr_path:
          if not obj or not hasattr(obj, a):
              return None
          obj = getattr(obj, a)
      return obj
  
  def _get_label_attr(obj, attr_path):
      """Returns the value at attr_path as a label string if it is set."""
      label = _get_opt_attr(obj, attr_path)
      return str(label) if label else None
  
  def _getattr_as_list(obj, attr_path):
      """Returns the value at attr_path as a list.
  
      This handles normalization of attributes containing a single value for use in
      methods expecting a list of values.
  
      Args:
        obj: The struct whose attributes should be parsed.
        attr_path: Dotted path of attributes whose value should be returned in
            list form.
  
      Returns:
        A list of values for obj at attr_path or [] if the struct has
        no such attribute.
      """
      val = _get_opt_attr(obj, attr_path)
      if not val:
          return []
  
      if type(val) == "list":
          return val
      return [val]
  
  def _extract_defines_from_option_list(lst):
      """Extracts preprocessor defines from a list of -D strings."""
      defines = []
      for item in lst:
          if item.startswith("-D"):
              defines.append(item[2:])
      return defines
  
  def _extract_compiler_defines(ctx):
      """Extracts preprocessor defines from compiler fragments."""
      defines = []
  
      cpp_fragment = _get_opt_attr(ctx.fragments, "cpp")
  
      # Enabled in Bazel 0.17
      if hasattr(cpp_fragment, "copts"):
          cc_toolchain = find_cpp_toolchain(ctx)
          copts = cpp_fragment.copts
          cxxopts = cpp_fragment.cxxopts
          conlyopts = cpp_fragment.conlyopts
  
          feature_configuration = cc_common.configure_features(
              cc_toolchain = cc_toolchain,
              requested_features = ctx.features,
              unsupported_features = ctx.disabled_features + UNSUPPORTED_FEATURES,
          )
          c_variables = cc_common.create_compile_variables(
              feature_configuration = feature_configuration,
              cc_toolchain = cc_toolchain,
              user_compile_flags = copts + conlyopts,
          )
          cpp_variables = cc_common.create_compile_variables(
              feature_configuration = feature_configuration,
              cc_toolchain = cc_toolchain,
              add_legacy_cxx_options = True,
              user_compile_flags = copts + cxxopts,
          )
          c_options = cc_common.get_memory_inefficient_command_line(
              feature_configuration = feature_configuration,
              # TODO(hlopko): Replace with action_name once Bazel >= # 0.16 is assumed
              action_name = "c-compile",
              variables = c_variables,
          )
          cpp_options = cc_common.get_memory_inefficient_command_line(
              feature_configuration = feature_configuration,
              # TODO(hlopko): Replace with action_name once Bazel >= # 0.16 is assumed
              action_name = "c++-compile",
              variables = cpp_variables,
          )
          defines += _extract_defines_from_option_list(c_options)
          defines += _extract_defines_from_option_list(cpp_options)
      elif cpp_fragment:
          c_options = _get_opt_attr(cpp_fragment, "c_options")
          defines += _extract_defines_from_option_list(c_options)
  
          compiler_options = cpp_fragment.compiler_options([])
          defines += _extract_defines_from_option_list(compiler_options)
  
          unfiltered = cpp_fragment.unfiltered_compiler_options([])
          defines += _extract_defines_from_option_list(unfiltered)
  
          cxx = cpp_fragment.cxx_options([])
          defines += _extract_defines_from_option_list(cxx)
  
      objc_fragment = _get_opt_attr(ctx.fragments, "objc")
      if objc_fragment:
          objc_copts = _get_opt_attr(objc_fragment, "copts")
          defines += _extract_defines_from_option_list(objc_copts)
  
      return defines
  
  def _collect_secondary_artifacts(target, ctx):
      """Returns a list of file metadatas for implicit outputs of 'target'."""
      artifacts = []
      if AppleBundleInfo in target:
          infoplist = target[AppleBundleInfo].infoplist
  
          if infoplist:
              artifacts.append(_file_metadata(infoplist))
  
      return artifacts
  
  def _extract_generated_sources(target):
      """Returns (source_metadatas, includes) generated by the given target."""
      file_metadatas = []
      objc_provider = _get_opt_attr(target, "objc")
      if hasattr(objc_provider, "source") and hasattr(objc_provider, "header"):
          all_files = depset(transitive = [objc_provider.source, objc_provider.header])
          file_metadatas = _depset_to_file_metadata_list(all_files)
  
      return file_metadatas
  
  def _get_deployment_info(target, ctx):
      """Returns (platform_type, minimum_os_version) for the given target."""
      platform_type = _get_platform_type(ctx)
  
      if AppleBundleInfo in target:
          apple_bundle_provider = target[AppleBundleInfo]
          minimum_os_version = apple_bundle_provider.minimum_os_version
          return (platform_type, minimum_os_version)
      return (platform_type, _minimum_os_for_platform(ctx, platform_type))
  
  def _get_xcode_version(ctx):
      """Returns the current Xcode version as a string."""
      return str(ctx.attr._tulsi_xcode_config[apple_common.XcodeVersionConfig].xcode_version())
  
  def _get_platform_type(ctx):
      """Return the current apple_common.platform_type as a string."""
      current_platform = (_get_opt_attr(ctx, "rule.attr.platform_type") or
                          _get_opt_attr(ctx, "rule.attr._platform_type"))
      if not current_platform:
          apple_frag = _get_opt_attr(ctx.fragments, "apple")
          current_platform = str(apple_frag.single_arch_platform.platform_type)
      return current_platform
  
  def _minimum_os_for_platform(ctx, platform_type_str):
      """Extracts the minimum OS version for the given apple_common.platform."""
      min_os = _get_opt_attr(ctx, "rule.attr.minimum_os_version")
      if min_os:
          return min_os
  
      platform_type = getattr(apple_common.platform_type, platform_type_str)
      min_os = (ctx.attr._tulsi_xcode_config[apple_common.XcodeVersionConfig].minimum_os_for_platform_type(platform_type))
  
      if not min_os:
          return None
  
      # Convert the DottedVersion to a string suitable for inclusion in a struct.
      return str(min_os)
  
  def _collect_swift_modules(target):
      """Returns a depset of Swift modules found on the given target."""
      if SwiftInfo in target:
          swift_info = target[SwiftInfo]
          if hasattr(swift_info, "transitive_swiftmodules"):
              return swift_info.transitive_swiftmodules
      return depset()
  
  def _collect_module_maps(target, rule_attr):
      """Returns a depset of Clang module maps found on the given target."""
      if SwiftInfo in target and apple_common.Objc in target:
          depsets = []
          objc = target[apple_common.Objc]
          if hasattr(objc, "module_map"):
              depsets.append(objc.module_map)
  
          for dep in _collect_dependencies(rule_attr, "deps"):
              if SwiftClangModuleInfo in dep:
                  module_info = dep[SwiftClangModuleInfo]
                  if hasattr(module_info, "transitive_modulemaps"):
                      depsets.append(module_info.transitive_modulemaps)
          return depset(transitive = depsets)
      return depset()
  
  def _collect_objc_defines(objc_provider, rule_attr):
      """Returns a depset of C-compiler defines."""
      depsets = [objc_provider.define] if objc_provider else []
      for dep in _collect_dependencies(rule_attr, "deps"):
          if SwiftClangModuleInfo in dep:
              module_info = dep[SwiftClangModuleInfo]
              if hasattr(module_info, "transitive_defines"):
                  depsets.append(module_info.transitive_defines)
      return depset(transitive = depsets)
  
  # TODO(b/64490743): Add these files to the Xcode project.
  def _collect_swift_header(target):
      """Returns a depset of Swift generated headers found on the given target."""
  
      # swift_* targets put the generated header into their objc provider HEADER
      # field.
      if SwiftInfo in target and apple_common.Objc in target:
          return target[apple_common.Objc].header
      return depset()
  
  def _target_filtering_info(ctx):
      """Returns filtering information for test rules."""
      rule = ctx.rule
  
      # TODO(b/72406542): Clean this up to use a test provider if possible.
      if rule.kind.endswith("_test"):
          # Note that a test's size is considered a tag for filtering purposes.
          size = _getattr_as_list(rule, "attr.size")
          tags = _getattr_as_list(rule, "attr.tags")
          return struct(tags = tags + size)
      else:
          return None
  
  def _tulsi_sources_aspect(target, ctx):
      """Extracts information from a given rule, emitting it as a JSON struct."""
      rule = ctx.rule
      target_kind = rule.kind
      rule_attr = _get_opt_attr(rule, "attr")
      filter = _filter_for_rule(rule)
  
      transitive_info_files = []
      transitive_attributes = dict()
      for attr_name in _TULSI_COMPILE_DEPS:
          deps = _collect_dependencies(rule_attr, attr_name)
          for dep in _filter_deps(filter, deps):
              if TulsiSourcesAspectInfo in dep:
                  transitive_info_files.append(dep[TulsiSourcesAspectInfo].transitive_info_files)
                  transitive_attributes += dep[TulsiSourcesAspectInfo].transitive_attributes
  
      artifacts = _get_opt_attr(target, "files")
      if artifacts:
          # Ignore any generated Xcode projects as they are not useful to Tulsi.
          artifacts = [
              _file_metadata(f)
              for f in artifacts.to_list()
              if not f.short_path.endswith("project.pbxproj")
          ]
      else:
          # artifacts may be an empty set type, in which case it must be explicitly
          # set to None to allow Skylark's serialization to work.
          artifacts = None
  
      srcs = (_collect_files(rule, "attr.srcs") +
              _collect_files(rule, "attr.hdrs") +
              _collect_files(rule, "attr.textual_hdrs"))
      generated_files = []
      generated_non_arc_files = []
      if target_kind in _SOURCE_GENERATING_RULES:
          generated_files = _extract_generated_sources(target)
      elif target_kind in _NON_ARC_SOURCE_GENERATING_RULES:
          generated_non_arc_files = _extract_generated_sources(target)
  
      swift_transitive_modules = depset(
          _depset_to_file_metadata_list(_collect_swift_modules(target)),
      )
  
      # Collect ObjC module maps dependencies for Swift targets.
      objc_module_maps = depset(
          _depset_to_file_metadata_list(_collect_module_maps(target, rule_attr)),
      )
  
      # Collect the dependencies of this rule, dropping any .jar files (which may be
      # created as artifacts of java/j2objc rules).
      dep_labels = _collect_dependency_labels(rule, filter, _TULSI_COMPILE_DEPS)
      compile_deps = [str(d) for d in dep_labels if not d.name.endswith(".jar")]
  
      binary_rule = _get_opt_attr(rule_attr, "binary")
      if binary_rule and type(binary_rule) == "list":
          binary_rule = binary_rule[0]
  
      supporting_files = (_collect_supporting_files(rule_attr) +
                          _collect_asset_catalogs(rule_attr) +
                          _collect_bundle_imports(rule_attr))
  
      copts_attr = _get_opt_attr(rule_attr, "copts")
      is_swift_library = target_kind == "swift_library"
  
      # Keys for attribute and inheritable_attributes keys must be kept in sync
      # with defines in Tulsi's RuleEntry.
      attributes = _dict_omitting_none(
          binary = _get_label_attr(binary_rule, "label"),
          copts = None if is_swift_library else copts_attr,
          swiftc_opts = copts_attr if is_swift_library else None,
          datamodels = _collect_xcdatamodeld_files(rule_attr, "datamodels"),
          supporting_files = supporting_files,
          xctest_app = _get_label_attr(rule_attr, "xctest_app.label"),
          test_host = _get_label_attr(rule_attr, "test_host.label"),
          test_bundle = _get_label_attr(rule_attr, "test_bundle.label"),
      )
  
      # Inheritable attributes are pulled up through dependencies of type 'binary'
      # to simplify handling in Tulsi (so it appears as though bridging_header is
      # defined on an ios_application rather than its associated objc_binary, for
      # example).
      inheritable_attributes = _dict_omitting_none(
          bridging_header = _collect_first_file(rule_attr, "bridging_header"),
          compiler_defines = _extract_compiler_defines(ctx),
          enable_modules = _get_opt_attr(rule_attr, "enable_modules"),
          launch_storyboard = _collect_first_file(rule_attr, "launch_storyboard"),
          pch = _collect_first_file(rule_attr, "pch"),
      )
  
      # Merge any attributes on the "binary" dependency into this container rule.
      binary_attributes = _get_opt_attr(binary_rule, "inheritable_attributes")
      if binary_attributes:
          inheritable_attributes = binary_attributes + inheritable_attributes
  
      # Collect extensions for bundled targets.
      extensions = []
      if AppleBundleInfo in target:
          extensions = [str(t.label) for t in _getattr_as_list(rule_attr, "extensions")]
  
      # Tulsi considers WatchOS apps and extensions as an "extension"
      if target_kind == "watchos_application":
          watch_ext = _get_label_attr(rule_attr, "extension.label")
          extensions.append(watch_ext)
      if target_kind == "ios_application":
          watch_app = _get_label_attr(rule_attr, "watch_application.label")
          if watch_app:
              extensions.append(watch_app)
  
      # Record the Xcode version used for all targets, although it will only be used by bazel_build.py
      # for targets that are buildable in the xcodeproj.
      xcode_version = _get_xcode_version(ctx)
  
      # Collect bundle related information and Xcode version only for runnable targets.
      if AppleBundleInfo in target:
          apple_bundle_provider = target[AppleBundleInfo]
  
          bundle_name = apple_bundle_provider.bundle_name
          bundle_id = apple_bundle_provider.bundle_id
          product_type = apple_bundle_provider.product_type
  
          # We only need the infoplist from iOS extension targets.
          infoplist = apple_bundle_provider.infoplist if IosExtensionBundleInfo in target else None
      else:
          bundle_name = None
          product_type = None
          infoplist = None
  
          # For macos_command_line_application, which does not have a
          # AppleBundleInfo provider but does have a bundle_id attribute for use
          # in the Info.plist.
          if target_kind == "macos_command_line_application":
              bundle_id = _get_opt_attr(rule_attr, "bundle_id")
          else:
              bundle_id = None
  
      # Collect Swift related attributes.
      swift_defines = []
  
      if SwiftInfo in target:
          swift_info = target[SwiftInfo]
          attributes["has_swift_info"] = True
          transitive_attributes["swift_language_version"] = swift_info.swift_version
          transitive_attributes["has_swift_dependency"] = True
          swift_defines = swift_info.transitive_defines.to_list()
  
      all_attributes = attributes + inheritable_attributes + transitive_attributes
  
      objc_provider = _get_opt_attr(target, "objc")
      objc_defines = []
      target_includes = []
  
      if objc_provider:
          target_includes = [
              _convert_outpath_to_symlink_path(x)
              for x in objc_provider.include
          ]
      objc_defines = _collect_objc_defines(objc_provider, rule_attr).to_list()
  
      platform_type, os_deployment_target = _get_deployment_info(target, ctx)
      non_arc_srcs = _collect_files(rule, "attr.non_arc_srcs")
  
      # Collect test information.
      if AppleTestInfo in target:
          provider = target[AppleTestInfo]
          srcs = _depset_to_file_metadata_list(provider.sources)
          non_arc_srcs = _depset_to_file_metadata_list(provider.non_arc_sources)
          target_includes = [_convert_outpath_to_symlink_path(x) for x in provider.includes.to_list()]
          swift_transitive_modules = _depset_to_file_metadata_list(provider.swift_modules)
          objc_module_maps = _depset_to_file_metadata_list(provider.module_maps)
          test_deps = provider.deps.to_list()
          module_name = provider.module_name
      else:
          swift_transitive_modules = swift_transitive_modules.to_list()
          objc_module_maps = objc_module_maps.to_list()
          test_deps = None
          module_name = None
  
      info = _struct_omitting_none(
          artifacts = artifacts,
          attr = _struct_omitting_none(**all_attributes),
          build_file = ctx.build_file_path,
          bundle_id = bundle_id,
          bundle_name = bundle_name,
          objc_defines = objc_defines,
          swift_defines = swift_defines,
          deps = compile_deps,
          test_deps = test_deps,
          extensions = extensions,
          framework_imports = _collect_framework_imports(rule_attr),
          generated_files = generated_files,
          generated_non_arc_files = generated_non_arc_files,
          includes = target_includes,
          os_deployment_target = os_deployment_target,
          label = str(target.label),
          non_arc_srcs = non_arc_srcs,
          secondary_product_artifacts = _collect_secondary_artifacts(target, ctx),
          srcs = srcs,
          swift_transitive_modules = swift_transitive_modules,
          objc_module_maps = objc_module_maps,
          module_name = module_name,
          type = target_kind,
          infoplist = infoplist.path if infoplist else None,
          platform_type = platform_type,
          product_type = product_type,
          xcode_version = xcode_version,
      )
  
      # Create an action to write out this target's info.
      output = ctx.new_file(target.label.name + ".tulsiinfo")
      ctx.file_action(output, info.to_json())
      output_files = [output]
  
      if infoplist:
          output_files.append(infoplist)
  
      info_files = depset(output_files, transitive = transitive_info_files)
      artifacts_depset = depset(artifacts) if artifacts else depset()
  
      return [
          OutputGroupInfo(tulsi_info = info_files),
          TulsiSourcesAspectInfo(
              transitive_info_files = info_files,
              inheritable_attributes = inheritable_attributes,
              transitive_attributes = transitive_attributes,
              artifacts = artifacts_depset,
              filtering_info = _target_filtering_info(ctx),
          ),
      ]
  
  def _collect_bundle_info(target):
      """Returns Apple bundle info for the given target, None if not a bundle."""
      if AppleBundleInfo in target:
          apple_bundle = target[AppleBundleInfo]
          has_dsym = (apple_common.AppleDebugOutputs in target)
          return struct(
              archive_root = apple_bundle.archive_root,
              bundle_name = apple_bundle.bundle_name,
              bundle_extension = apple_bundle.bundle_extension,
              has_dsym = has_dsym,
          )
  
      return None
  
  # Due to b/71744111 we have to manually re-create tag filtering for test_suite
  # rules.
  def _tags_conform_to_filter(tags, filter):
      """Mirrors Bazel tag filtering for test_suites.
  
      This makes sure that the target has all of the required tags and none of
      the excluded tags before we include them within a test_suite.
  
      For more information on filtering inside Bazel, see
      com.google.devtools.build.lib.packages.TestTargetUtils.java.
  
      Args:
        tags: all of the tags for the test target
        filter: a struct containing excluded_tags and required_tags
  
      Returns:
        True if this target passes the filter and False otherwise.
  
      """
  
      # None of the excluded tags can be present.
      for exclude in filter.excluded_tags:
          if exclude in tags:
              return False
  
      # All of the required tags must be present.
      for required in filter.required_tags:
          if required not in tags:
              return False
  
      # All filters have been satisfied.
      return True
  
  def _filter_for_rule(rule):
      """Returns a filter for test_suite rules and None for other rules."""
      if rule.kind != "test_suite":
          return None
  
      excluded_tags = []
      required_tags = []
  
      tags = _getattr_as_list(rule, "attr.tags")
  
      for tag in tags:
          if tag.startswith("-"):
              excluded_tags.append(tag[1:])
          elif tag.startswith("+"):
              required_tags.append(tag[1:])
          elif tag == "manual":
              # The manual tag is treated specially; it is ignored for filters.
              continue
          else:
              required_tags.append(tag)
      return struct(
          excluded_tags = excluded_tags,
          required_tags = required_tags,
      )
  
  def _filter_deps(filter, deps):
      """Filters dep targets based on tags."""
      if not filter:
          return deps
  
      kept_deps = []
      for dep in deps:
          info = None
          if TulsiSourcesAspectInfo in dep:
              info = dep[TulsiSourcesAspectInfo].filtering_info
  
          # Only attempt to filter targets that support filtering.
          # test_suites in a test_suite are not filtered, but their
          # tests are.
          if not info or _tags_conform_to_filter(info.tags, filter):
              kept_deps.append(dep)
      return kept_deps
  
  def _tulsi_outputs_aspect(target, ctx):
      """Collects outputs of each build invocation."""
  
      rule = ctx.rule
      target_kind = rule.kind
      rule_attr = _get_opt_attr(rule, "attr")
      transitive_generated_files = []
  
      # A collective list of bundle infos that has been gathered by each dependency of this target.
      # We intentionally do not collect info about _current_ target to exclude the
      # root target, which will be covered by other structs in this aspect, from the
      # list.
      transitive_embedded_bundles = []
  
      # A list of bundle infos corresponding to the dependencies of this target.
      direct_embedded_bundles = []
  
      for attr_name in _TULSI_COMPILE_DEPS:
          deps = _collect_dependencies(rule_attr, attr_name)
          for dep in deps:
              if TulsiOutputAspectInfo in dep:
                  transitive_generated_files.append(dep[TulsiOutputAspectInfo].transitive_generated_files)
                  transitive_embedded_bundles.append(dep[TulsiOutputAspectInfo].transitive_embedded_bundles)
  
              # Retrieve the bundle info for embeddable attributes.
              if attr_name not in _TULSI_NON_EMBEDDEDABLE_ATTRS:
                  dep_bundle_info = _collect_bundle_info(dep)
                  if dep_bundle_info:
                      direct_embedded_bundles.append(dep_bundle_info)
  
      embedded_bundles = depset(direct_embedded_bundles, transitive = transitive_embedded_bundles)
  
      artifact = None
      bundle_name = None
      archive_root = None
      infoplist = None
      if AppleBundleInfo in target:
          bundle_info = target[AppleBundleInfo]
  
          artifact = bundle_info.archive.path
          archive_root = bundle_info.archive_root
          infoplist = bundle_info.infoplist
  
          bundle_name = bundle_info.bundle_name
      elif target_kind == "macos_command_line_application":
          # Special support for macos_command_line_application which does not have an
          # AppleBundleInfo provider.
  
          # Both the dSYM binary and executable binary don't have an extension, so
          # pick the first extension-less file not in a DWARF folder.
          artifacts = [
              x.path
              for x in target.files.to_list()
              if x.extension == "" and
                 "Contents/Resources/DWARF" not in x.path
          ]
          if len(artifacts) > 0:
              artifact = artifacts[0]
      else:
          # Special support for *_library targets, which Tulsi allows building at
          # the top-level.
          artifacts = [
              x.path
              for x in target.files.to_list()
              if x.extension == "a"
          ]
          if len(artifacts) > 0:
              artifact = artifacts[0]
  
      # Collect generated files for bazel_build.py to copy under Tulsi root.
      all_files_depsets = []
      if target_kind in _SOURCE_GENERATING_RULES + _NON_ARC_SOURCE_GENERATING_RULES:
          objc_provider = _get_opt_attr(target, "objc")
          if hasattr(objc_provider, "source") and hasattr(objc_provider, "header"):
              all_files_depsets.append(objc_provider.source)
              all_files_depsets.append(objc_provider.header)
  
      all_files_depsets.append(_collect_swift_header(target))
      all_files_depsets.append(_collect_swift_modules(target))
      all_files_depsets.append(_collect_module_maps(target, rule_attr))
  
      source_files = [
          x
          for x in target.files.to_list()
          if x.extension.lower() in _GENERATED_SOURCE_FILE_EXTENSIONS
      ]
      if infoplist:
          source_files.append(infoplist)
  
      source_files.extend(_collect_artifacts(rule, "attr.srcs"))
      source_files.extend(_collect_artifacts(rule, "attr.hdrs"))
      source_files.extend(_collect_artifacts(rule, "attr.textual_hdrs"))
      source_files.extend(_collect_supporting_files(rule_attr, convert_to_metadata = False))
  
      all_files = depset(source_files, transitive = all_files_depsets)
  
      generated_files = depset(
          [x for x in all_files.to_list() if not x.is_source],
          transitive = transitive_generated_files,
      )
  
      has_dsym = False
      if hasattr(ctx.fragments, "objc"):
          # Check the fragment directly, as macos_command_line_application does not
          # propagate apple_common.AppleDebugOutputs.
          has_dsym = ctx.fragments.objc.generate_dsym
  
      info = _struct_omitting_none(
          artifact = artifact,
          archive_root = archive_root,
          generated_sources = [(x.path, x.short_path) for x in generated_files.to_list()],
          bundle_name = bundle_name,
          embedded_bundles = embedded_bundles.to_list(),
          has_dsym = has_dsym,
      )
  
      output = ctx.new_file(target.label.name + ".tulsiouts")
      ctx.file_action(output, info.to_json())
  
      return [
          OutputGroupInfo(tulsi_outputs = [output]),
          TulsiOutputAspectInfo(
              transitive_generated_files = generated_files,
              transitive_embedded_bundles = embedded_bundles,
          ),
      ]
  
  tulsi_sources_aspect = aspect(
      attr_aspects = _TULSI_COMPILE_DEPS,
      attrs = {
          "_tulsi_xcode_config": attr.label(default = configuration_field(
              name = "xcode_config_label",
              fragment = "apple",
          )),
          "_cc_toolchain": attr.label(default = Label(
              "@bazel_tools//tools/cpp:current_cc_toolchain",
          )),
      },
      fragments = [
          "apple",
          "cpp",
          "objc",
      ],
      implementation = _tulsi_sources_aspect,
  )
  
  # This aspect does not propagate past the top-level target because we only need
  # the top target outputs.
  tulsi_outputs_aspect = aspect(
      attr_aspects = _TULSI_COMPILE_DEPS,
      attrs = {
          "_cc_toolchain": attr.label(
              default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"),
          ),
      },
      fragments = [
          "apple",
          "cpp",
          "objc",
      ],
      implementation = _tulsi_outputs_aspect,
  )
  