blob: 10c7811063204ce4186d440245b75e340b6607b2 [file] [log] [blame]
# 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/tulsi_aspects_paths.bzl",
"AppleBinaryInfo",
"AppleBundleInfo",
"AppleTestInfo",
"IosExtensionBundleInfo",
"SwiftInfo",
)
load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
ObjcInfo = apple_common.Objc
# 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 = [
"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_host",
"additional_contents", # macos_application can specify a dict with supported rules as keys.
# 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",
]
# 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",
]
# 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",
"data",
"entitlements",
"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",
]
# List 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/bazel-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 prefix of bazel-tulsi-includes is present as Bazel will clear all
directories that don't start with 'bazel-' when it builds.
Otherwise, upon a build failure, bazel-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
bazel-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:
# bazel-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 "bazel-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, exclude_xcdatamodel = False, exclude_xcassets = False):
"""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()
if (not exclude_xcdatamodel or ".xcdatamodel" not in f.path) and
(not exclude_xcassets or ".xcassets" not in f.path) and
(not exclude_xcassets or ".xcstickers" not in f.path)
]
def _collect_files(
obj,
attr_path,
convert_to_metadata = True,
exclude_xcdatamodel = False,
exclude_xcassets = False):
"""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,
exclude_xcdatamodel = exclude_xcdatamodel,
exclude_xcassets = exclude_xcassets,
)]
else:
return _collect_artifacts(
obj,
attr_path,
exclude_xcdatamodel = exclude_xcdatamodel,
exclude_xcassets = exclude_xcassets,
)
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,
exclude_xcdatamodel = True,
exclude_xcassets = True,
)
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", "data"]
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_provider(target, provider):
"""Returns the given provider on target, if present."""
return target[provider] if provider in target else None
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
elif type(val) == "dict":
return val.keys()
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(
ctx = ctx,
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."""
transitive_depsets = []
objc_provider = _get_opt_provider(target, ObjcInfo)
if hasattr(objc_provider, "source"):
transitive_depsets.append(objc_provider.source)
cc_info = _get_opt_provider(target, CcInfo)
if cc_info:
transitive_depsets.append(cc_info.compilation_context.headers)
file_metadatas = _depset_to_file_metadata_list(depset(transitive = transitive_depsets))
return file_metadatas
def _get_deployment_info(target, ctx):
"""Returns (platform_type, minimum_os_version) for the given target."""
if AppleBundleInfo in target:
apple_bundle_provider = target[AppleBundleInfo]
minimum_os_version = apple_bundle_provider.minimum_os_version
platform_type = apple_bundle_provider.platform_type
return (platform_type, minimum_os_version)
attr_platform_type = _get_platform_type(ctx)
return (attr_platform_type, _minimum_os_for_platform(ctx, attr_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 ObjcInfo in target:
depsets = []
objc = target[ObjcInfo]
if hasattr(objc, "module_map"):
depsets.append(objc.module_map)
for dep in _collect_dependencies(rule_attr, "deps"):
if SwiftInfo in dep:
module_info = dep[SwiftInfo]
if hasattr(module_info, "transitive_modulemaps"):
depsets.append(module_info.transitive_modulemaps)
return depset(transitive = depsets)
return depset()
def _collect_objc_strict_includes(target, rule_attr):
"""Returns a depset of strict includes found on the deps of given target."""
depsets = []
for dep in _collect_dependencies(rule_attr, "deps"):
if ObjcInfo in dep:
objc = dep[ObjcInfo]
if hasattr(objc, "strict_include"):
depsets.append(objc.strict_include)
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 CcInfo.
if SwiftInfo in target and CcInfo in target:
return target[CcInfo].compilation_context.headers
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.update(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")]
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"
datamodels = _collect_xcdatamodeld_files(rule_attr, "datamodels")
datamodels.extend(_collect_xcdatamodeld_files(rule_attr, "data"))
# Keys for attribute and inheritable_attributes keys must be kept in sync
# with defines in Tulsi's RuleEntry.
attributes = _dict_omitting_none(
copts = None if is_swift_library else copts_attr,
swiftc_opts = copts_attr if is_swift_library else None,
datamodels = datamodels,
supporting_files = supporting_files,
test_host = _get_label_attr(rule_attr, "test_host.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"),
)
# 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 = dict(attributes)
all_attributes.update(inheritable_attributes)
all_attributes.update(transitive_attributes)
objc_strict_includes = _collect_objc_strict_includes(target, rule_attr)
cc_provider = _get_opt_provider(target, CcInfo)
objc_defines = []
if cc_provider:
cc_ctx = cc_provider.compilation_context
includes_depsets = [
objc_strict_includes,
cc_ctx.includes,
cc_ctx.quote_includes,
cc_ctx.system_includes,
]
else:
includes_depsets = [objc_strict_includes]
if includes_depsets:
# Use a depset here to remove duplicates which is possible since
# converting the output path can strip some path information.
target_includes = depset([
_convert_outpath_to_symlink_path(x)
for x in depset(transitive = includes_depsets).to_list()
]).to_list()
else:
target_includes = []
if cc_provider:
objc_defines = cc_provider.compilation_context.defines.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.actions.declare_file(target.label.name + ".tulsiinfo")
ctx.actions.write(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 _bundle_dsym_path(apple_bundle):
"""Compute the dSYM path for the bundle.
Due to b/110264170 dSYMs are not fully exposed via a provider. We instead
rely on the fact that `rules_apple` puts them next to the bundle just like
Xcode.
"""
bin_path = apple_bundle.archive.dirname
dsym_name = apple_bundle.bundle_name + apple_bundle.bundle_extension + ".dSYM"
return bin_path + "/" + dsym_name
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,
dsym_path = _bundle_dsym_path(apple_bundle),
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
dsym_path = None
bundle_name = None
archive_root = None
infoplist = None
if AppleBundleInfo in target:
bundle_info = target[AppleBundleInfo]
artifact = bundle_info.archive.path
dsym_path = _bundle_dsym_path(bundle_info)
archive_root = bundle_info.archive_root
infoplist = bundle_info.infoplist
bundle_name = bundle_info.bundle_name
elif AppleBinaryInfo in target:
# Support for non-bundled binary targets such as
# `macos_command_line_application`. These still have dSYMs support and
# should be located next to the binary.
artifact = target[AppleBinaryInfo].binary.path
dsym_path = artifact + ".dSYM"
elif (target_kind == "cc_binary" or target_kind == "cc_test"):
# Special support for cc_* targets which do not have AppleBinaryInfo or
# AppleBundleInfo providers.
#
# At the moment these don't have support for dSYMs (b/124859331), but
# in case they do in the future we filter out the dSYM files.
artifacts = [
x
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].path
else:
# Special support for *_library targets, which Tulsi allows building at
# the top-level.
artifacts = [
x
for x in target.files.to_list()
if x.extension == "a"
]
if len(artifacts) > 0:
artifact = artifacts[0].path
# 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_provider(target, ObjcInfo)
if hasattr(objc_provider, "source"):
all_files_depsets.append(objc_provider.source)
cc_info = _get_opt_provider(target, CcInfo)
if cc_info:
all_files_depsets.append(cc_info.compilation_context.headers)
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 = apple_common.AppleDebugOutputs in target
info = _struct_omitting_none(
artifact = artifact,
archive_root = archive_root,
dsym_path = dsym_path,
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.actions.declare_file(target.label.name + ".tulsiouts")
ctx.actions.write(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",
)),
},
toolchains = ["@bazel_tools//tools/cpp:toolchain_type"],
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,
)