blob: 6b6e34e42e54e07e2a4da51861edb689daf061b6 [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.
"""
# 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',
'extensions',
'non_propagated_deps',
'test_bundle',
'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 = [
# apple_watch1_extension
'app_entitlements',
'app_infoplists',
'app_resources',
'app_storyboards',
'app_strings',
'app_structured_resources',
'ext_entitlements',
'ext_infoplists',
'ext_resources',
'ext_strings',
'ext_structured_resources',
'entitlements',
'infoplist',
'infoplists',
'resources',
'strings',
'structured_resources',
'storyboards',
'xibs',
]
# Set of rules with implicit <label>.ipa IPA outputs.
_IPA_GENERATING_RULES = set([
'apple_watch1_extension',
# apple_watch2_extension also generates an implicit IPA but the naming
# cannot be derived from the target label alone so it is special cased.
'ios_application',
'ios_extension',
'ios_test',
'objc_binary',
'tvos_application',
'_ios_application',
'_tvos_application',
])
# Set of rules that generate MergedInfo.plist files as part of the build.
_MERGEDINFOPLIST_GENERATING_RULES = set([
'apple_watch1_extension',
'apple_watch2_extension',
'ios_application',
'tvos_application',
'_ios_application',
'_tvos_application',
])
# Set of rules whose outputs should be treated as generated sources.
_SOURCE_GENERATING_RULES = set([
'j2objc_library',
])
# Set of rules whose outputs should be treated as generated sources that do not
# use ARC.
_NON_ARC_SOURCE_GENERATING_RULES = set([
'objc_proto_library',
])
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
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 bazel-symlink equivalents."""
# The path will be of the form
# bazel-[whatever]/[platform-config]/symlink[/.*]
first_dash = path.find('-')
components = path.split('/')
if (len(components) > 2 and
first_dash >= 0 and
first_dash < len(components[0])):
return path[:first_dash + 1] + '/'.join(components[2:])
return path
def _file_metadata(f):
"""Returns metadata about a given File."""
if not f:
return None
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
# TODO(abaire): Remove once Skylark File objects can reference directories.
# 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=f.short_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 _collect_files(obj, attr_path):
"""Returns a list of artifact_location's for the attr_path in obj."""
return [_file_metadata(f)
for src in _getattr_as_list(obj, attr_path)
for f in _get_opt_attr(src, 'files')]
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):
"""Extracts 'supporting' files from the given rule attributes."""
all_files = []
for attr in _SUPPORTING_FILE_ATTRIBUTES:
all_files += _collect_files(rule_attr, attr)
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 = set()
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
discovered_paths += [full_path]
# 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."""
return _collect_bundle_paths(rule_attr,
['app_asset_catalogs', 'asset_catalogs'],
'.xcassets')
def _collect_bundle_imports(rule_attr):
"""Extracts bundle directories from the given rule attributes."""
return _collect_bundle_paths(rule_attr,
['bundle_imports'],
'.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 = set()
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
discovered_paths += [full_path]
datamodelds.append(_file_metadata_by_replacing_path(f, path, False))
return datamodelds
def _collect_dependency_labels(rule, attr_list):
"""Collects Bazel labels for a list of dependency attributes.
Args:
rule: The Bazel rule whose dependencies should be collected.
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.
"""
rule_attrs = rule.attr
deps = [dep
for attribute in attr_list
for dep in _getattr_as_list(rule_attrs, 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')
if 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 'rule'."""
artifacts = []
rule = ctx.rule
if rule.kind in _MERGEDINFOPLIST_GENERATING_RULES:
bin_dir = _convert_outpath_to_symlink_path(ctx.bin_dir.path)
package = target.label.package
basename = target.label.name
artifacts.append(_struct_omitting_none(
path='%s/%s-MergedInfo.plist' % (package, basename),
src=False,
root=bin_dir
))
return artifacts
def _extract_generated_sources_and_includes(target):
"""Returns (source_metadatas, includes) generated by the given target."""
file_metadatas = []
includes = []
objc_provider = _get_opt_attr(target, 'objc')
if hasattr(objc_provider, 'source') and hasattr(objc_provider, 'header'):
all_files = set(objc_provider.source)
all_files += objc_provider.header
file_metadatas = [_file_metadata(f) for f in all_files]
if hasattr(objc_provider, 'include'):
includes = [_convert_outpath_to_symlink_path(x)
for x in objc_provider.include]
return file_metadatas, includes
def _extract_minimum_os_for_platform(ctx, platform):
"""Extracts the minimum OS version for the given apple_common.platform."""
apple_frag = _get_opt_attr(ctx.fragments, 'apple')
min_os = apple_frag.minimum_os_for_platform_type(platform)
if not min_os:
return None
# Convert the DottedVersion to a string suitable for inclusion in a struct.
return str(min_os)
def _extract_swift_language_version(ctx):
"""Returns the Swift version set by the xcode_toolchain option for ctx."""
swift_toolchain = _get_opt_attr(ctx, 'fragments.apple.xcode_toolchain')
if swift_toolchain == 'com.apple.dt.toolchain.Swift_2_3':
return ('2.3', swift_toolchain)
elif swift_toolchain:
# TODO(abaire): Adjust as necessary once versions > 3.0 come out.
return ('3.0', swift_toolchain)
# TODO(abaire): Remove the fallback check for swift_library once
# xcode_toolchain is available everywhere.
if ctx.rule.kind == 'swift_library':
return ('3.0', 'com.apple.dt.toolchain.XcodeDefault')
return (None, 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')
tulsi_info_files = set()
transitive_attributes = dict()
for attr_name in _TULSI_COMPILE_DEPS:
deps = _getattr_as_list(rule_attr, attr_name)
for dep in deps:
if hasattr(dep, 'tulsi_info_files'):
tulsi_info_files += dep.tulsi_info_files
if hasattr(dep, 'transitive_attributes'):
transitive_attributes += dep.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
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 = []
generated_includes = []
if target_kind in _SOURCE_GENERATING_RULES:
generated_files, generated_includes = (
_extract_generated_sources_and_includes(target))
elif target_kind in _NON_ARC_SOURCE_GENERATING_RULES:
generated_non_arc_files, generated_includes = (
_extract_generated_sources_and_includes(target))
raw_swift_transitive_modules = _getattr_as_list(target,
'swift.transitive_modules')
swift_transitive_modules = [_file_metadata(f)
for f in raw_swift_transitive_modules]
# Collect ObjC module maps dependencies for Swift targets.
objc_module_maps = set()
if hasattr(target, 'swift'):
for module_maps in _getattr_as_list(target, 'objc.module_map'):
objc_module_maps += set([_file_metadata(f) for f in module_maps])
# 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, _TULSI_COMPILE_DEPS)
compile_deps = [str(l) for l in dep_labels if not l.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))
# 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=_get_opt_attr(rule_attr, 'copts'),
datamodels=_collect_xcdatamodeld_files(rule_attr, 'datamodels'),
supporting_files=supporting_files,
xctest=_get_opt_attr(rule_attr, 'xctest'),
xctest_app=_get_label_attr(rule_attr, 'xctest_app.label'),
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),
defines=_getattr_as_list(rule_attr, 'defines'),
enable_modules=_get_opt_attr(rule_attr, 'enable_modules'),
includes=_getattr_as_list(rule_attr, 'includes'),
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
ipa_output_label = None
if target_kind == 'apple_watch2_extension':
# watch2 extensions need to use the IPA produced for the app_name attribute.
ipa_name = _get_opt_attr(rule_attr, 'app_name') + '.ipa'
ipa_output_label = '//' + target.label.package + ':' + ipa_name
elif target_kind in _IPA_GENERATING_RULES:
ipa_output_label = str(target.label) + '.ipa'
extensions = [str(t.label) for t in _getattr_as_list(rule_attr, 'extensions')]
bundle_id = _get_opt_attr(rule_attr, 'bundle_id')
if not bundle_id:
bundle_id = _get_opt_attr(rule_attr, 'app_bundle_id')
# Build up any local transitive attributes and apply them.
swift_language_version, swift_toolchain = _extract_swift_language_version(ctx)
if swift_language_version:
transitive_attributes['swift_language_version'] = swift_language_version
transitive_attributes['has_swift_dependency'] = True
if swift_toolchain:
transitive_attributes['swift_toolchain'] = swift_toolchain
transitive_attributes['has_swift_dependency'] = True
all_attributes = attributes + inheritable_attributes + transitive_attributes
info = _struct_omitting_none(
artifacts=artifacts,
attr=_struct_omitting_none(**all_attributes),
build_file=ctx.build_file_path,
bundle_id=bundle_id,
deps=compile_deps,
ext_bundle_id=_get_opt_attr(rule_attr, 'ext_bundle_id'),
extensions=extensions,
framework_imports=_collect_framework_imports(rule_attr),
generated_files=generated_files,
generated_non_arc_files=generated_non_arc_files,
generated_includes=generated_includes,
ipa_output_label=ipa_output_label,
iphoneos_deployment_target=_extract_minimum_os_for_platform(
ctx, apple_common.platform_type.ios),
# TODO(abaire): Uncomment if/when Bazel supports macOS.
# macos_deployment_target=_extract_minimum_os_for_platform(
# ctx, apple_common.platform_type.macosx),
tvos_deployment_target=_extract_minimum_os_for_platform(
ctx, apple_common.platform_type.tvos),
watchos_deployment_target=_extract_minimum_os_for_platform(
ctx, apple_common.platform_type.watchos),
label=str(target.label),
non_arc_srcs=_collect_files(rule, 'attr.non_arc_srcs'),
secondary_product_artifacts=_collect_secondary_artifacts(target, ctx),
srcs=srcs,
swift_transitive_modules=swift_transitive_modules,
objc_module_maps=list(objc_module_maps),
type=target_kind,
)
# 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())
tulsi_info_files += set([output])
return struct(
# Matches the --output_groups on the bazel commandline.
output_groups={
'tulsi-info': tulsi_info_files,
},
# The file actions used to save this rule's info and that of all of its
# transitive dependencies.
tulsi_info_files=tulsi_info_files,
# The inheritable attributes of this rule, expressed as a dict instead of
# a struct to allow easy joining.
inheritable_attributes=inheritable_attributes,
# Transitive info that should be applied to every rule that depends on
# this rule.
transitive_attributes=transitive_attributes,
)
def _tulsi_outputs_aspect(target, ctx):
"""Collects outputs of each build invocation."""
# TODO(b/35322727): Move apple_watch2_extension into _IPA_GENERATING_RULES
# when dynamic outputs is the default strategy and it does need to be
# special-cased above.
if ctx.rule.kind not in _IPA_GENERATING_RULES + ['apple_watch2_extension']:
return
# An IPA output is guaranteed to exist for rules in _IPA_GENERATING_RULES
ipa_output = [x.path for x in target.files if x.path.endswith('.ipa')][0]
info = _struct_omitting_none(ipa=ipa_output)
output = ctx.new_file(target.label.name + '.tulsiouts')
ctx.file_action(output, info.to_json())
return struct(
output_groups={
'tulsi-outputs': [output],
},
)
tulsi_sources_aspect = aspect(
implementation=_tulsi_sources_aspect,
attr_aspects=_TULSI_COMPILE_DEPS,
fragments=['apple', 'cpp', 'objc'],
)
# This aspect does not propagate past the top-level target because we only need
# the IPA, which is at top level.
tulsi_outputs_aspect = aspect(
implementation=_tulsi_outputs_aspect,
)